Lua в модах Project Zomboid: события, client/server и общая структура
Скрипты .txt объявляют игровые сущности, а Lua задаёт поведение: добычу, контекстное меню, действия с анимацией и особые эффекты. В этом учебном обзоре разобраны структура client/server/shared, общий namespace, события, пример OnTick и схема запроса клиента серверу.
Уровень в источнике: intermediate; страница отмечена как обновлённая 10 октября 2026 года. В руководстве указан Build 42 без точного минорного билда.
Диалект Lua в Project Zomboid
Источник описывает Kahlua как вариант Lua 5.1, работающий внутри Java. По его списку отличий:
- Нет goto и меток ::label::; для пропуска шага цикла for автор предлагает условие if.
- Нет целочисленного оператора деления //; в примере используется math.floor(a / b).
- Не поддерживаются операторы &, |, ~, << и >>.
- Зарезервированные слова, например end, function и repeat, нельзя использовать как bare key: пишите t["end"], а не t.end.
- Вызов отсутствующего метода может дать сообщение tried to call nil; автор отмечает, что в его опыте ошибка проявлялась при загрузке сохранения, а не при загрузке мода.
Клиент, сервер и общий код
Расположение папок из источника:
media/lua/
├── client/ <- только компьютер игрока: меню, UI, визуальные эффекты
├── server/ <- только сервер: добыча, правила и серверная логика
└── shared/ <- обе стороны: данные, утилиты и timed actions
Автор говорит, что в одиночной игре компьютер одновременно выполняет роли клиента и сервера, поэтому запускается код обеих сторон; разделение особенно важно для multiplayer. Это описание поведения необходимо проверить на целевом Build 42 и реальном multiplayer-сервере.
Один namespace для каждого мода
Поскольку моды используют общее глобальное пространство Lua, автор предлагает складывать собственные имена в одну таблицу namespace:
Код из руководства (имя таблицы и функция сохранены):
MiMod = MiMod or {}
function MiMod.saludar(player)
player:Say("Weno pa'l leseo")
end
Фраза Weno pa'l leseo — чилийский сленг; автор поясняет её примерно как «всегда готов повеселиться».
Избегайте несвязанных глобальных переменных вроде contador = 0: они могут конфликтовать с другими модами.
События: когда запускается код
Код подписывается на события игры. В источнике приведён короткий OnGameStart пример:
Пример регистрации события из источника:
Events.OnGameStart.Add(function()
print("[MiMod] la partida empezó")
end)
Полный список Build 42 автор предлагает смотреть в справочнике событий Lua. Примеры применения из опубликованных модов:
| Событие | Пример использования в источнике |
|---|---|
| OnPostDistributionMerge | Добавление предмета в списки добычи (см. материал Loot) |
| OnTick | Повторяющиеся действия, например свет рядом с игроком; событие срабатывает каждый кадр, поэтому автор советует ограничивать частоту. |
| OnClientCommand | Сервер принимает запросы от игроков. |
| OnSave | Очистка временных данных перед сохранением. |
| OnPlayerDeath | Очистка данных, связанных с игроком. |
| OnWeaponSwing | Реакция на взмах оружия, например в моде gravity gun. |
Чтобы обработчик OnTick не выполнял тяжёлую работу каждый кадр, автор показывает временной интервал в одну секунду:
Фрагмент throttling из источника (не проверялся):
local ultimo = 0
Events.OnTick.Add(function()
local ahora = getTimestampMs()
if ahora - ultimo < 1000 then return end -- once per second
ultimo = ahora
-- heavy work goes here
end)
Multiplayer: клиент просит, сервер решает
Для действия, меняющего игровой мир, источник предлагает схему: клиент отправляет запрос, сервер проверяет его и применяет изменение для всех. Далее приведены именно иллюстративные фрагменты с водомётом Wanaco, а не готовая механика.
Клиентский фрагмент из источника:
-- client/: the player presses the button
sendClientCommand(player, "Wanaco", "toggleCannon", { on = true })
Серверный фрагмент из источника. Реальное действие намеренно опущено автором:
-- server/: the server checks and applies
Events.OnClientCommand.Add(function(module, command, player, args)
if module ~= "Wanaco" or command ~= "toggleCannon" then return end
local vehicle = player:getVehicle()
if not vehicle then return end -- never trust the client
-- ... turn on the cannon and notify everyone
end)
Серверный пример заканчивается многоточием: фактическое изменение мира и синхронизация не реализованы. Не копируйте его как готовую функцию переключения водомёта.
- Первая строка "Wanaco" задаёт канал мода.
- Сервер повторно проверяет входные данные: модифицированный клиент может отправить произвольный запрос.
- Автор утверждает, что тот же подход работает в одиночной игре, поэтому не нужны отдельные версии логики. Это поведение требует проверки.
Действия с таймером и анимацией
Действия с полосой прогресса и анимацией — например, еда, строительство или танец — источник называет timed actions. Класс создаётся через наследование от ISBaseTimedAction:
Неполный пример класса из источника; он не является готовым timed action:
require "TimedActions/ISBaseTimedAction"
MiMod.BailarAction = ISBaseTimedAction:derive("MiModBailarAction")
function MiMod.BailarAction:isValid()
return self.character:getVehicle() == nil
end
function MiMod.BailarAction:start()
self:setActionAnim("MiBaile")
end
Автор рекомендует размещать такой класс в shared/, а не только client/: согласно его объяснению, в multiplayer сервер также создаёт действие по имени. Это утверждение и необходимость конкретной стороны требуют проверки в Build 42 и реальном multiplayer. Фрагмент не содержит полного жизненного цикла действия.
Замечания автора об API Build 42
- getCell():getVehicles() в примере автора возвращает Set без метода :get(i); для обхода объектов он предлагает getCell():getObjectListForLua().
- Свойства тайла автор предлагает проверять через :has(IsoFlagType.water), с именем water в нижнем регистре. Метод :Is(...) он считает отсутствующим и связывает с ошибкой tried to call nil.
- В примере автора HaloTextHelper.addText(player, texto, color) отсутствует; предлагаются addGoodText (зелёный), addBadText (красный) или addText(player, texto).
Совет источника — искать незнакомые вызовы в Lua-файлах игры в media/lua/. Это полезная проверка, но отсутствие функции в Lua-файлах само по себе не доказывает, что её нет среди API, доступных через Java; проверяйте также документацию/API целевого билда.