Перейти к содержанию

Lua в модах Project Zomboid: события, client/server и общая структура

Учебный обзор Lua 5.1/Kahlua, каталогов client/server/shared, namespace, событий и throttling, а также иллюстративных запросов клиент-сервер и timed action. Фрагменты неполны и не представляют готовую механику.

Скрипты .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 целевого билда.