The .txt scripts define what exists. Lua defines what happens: loot, a new right-click menu, an action with an animation, a light that follows the player, a water cannon on a truck.
Project Zomboid's Lua is Lua 5.1
The game uses Kahlua, a version of Lua 5.1 that runs inside Java. If you learned Lua somewhere else, watch out for these differences:
- There's no
gotoand no::label::labels. To skip a step inside afor, use anif. - There's no
//integer division. Usemath.floor(a / b). - There's no
&,|,~,<<or>>. - Reserved words (
end,function,repeat...) can't be used as bare keys: writet["end"], nott.end. - When you call a method that doesn't exist, the error is a cryptic
tried to call nil, and it shows up when the save loads, not when the mod loads.
Client, server and shared
media/lua/
├── client/ <- only on the player's PC: menus, UI, visual effects
├── server/ <- only on the server: loot, rules, whatever is "in charge"
└── shared/ <- on both: data, utilities and timed actionsIn single player your PC is both client and server at the same time, so everything runs. The difference matters in multiplayer, and it's best to think this way from the start: then the mod works in both modes without changes.
One namespace per mod
All mods share the same global Lua. To avoid stepping on anyone else's toes, put everything of yours in a single table:
MiMod = MiMod or {}
function MiMod.saludar(player)
player:Say("Weno pa'l leseo")
end("Weno pa'l leseo" is Chilean slang, roughly "always up for some fun".)
Never leave loose global variables like contador = 0 lying around: sooner or later they'll clash with another mod's.
Events: when your code runs
Your code "hooks into" game events:
Events.OnGameStart.Add(function()
print("[MiMod] la partida empezó")
end)The full B42 list is in the Lua events reference. Some we use in published mods:
| Event | What we use it for |
|---|---|
OnPostDistributionMerge | Adding loot (see Loot) |
OnTick | Continuous stuff, like a light that follows the player. Throttle it: it runs every frame. |
OnClientCommand | The server receives requests from players |
OnSave | Cleaning up temporary things before saving |
OnPlayerDeath | Cleaning up whatever the player had |
OnWeaponSwing | Reacting to a weapon swing (the gravity gun) |
To keep OnTick from getting heavy, only do the work every so often:
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: the client asks, the server decides
If a player does something that changes the world, the client doesn't apply it directly: it asks the server to do it. The server checks that it makes sense and applies it for everyone. This is how the water cannon in Wanaco works:
-- 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)- The first string (
"Wanaco") is your mod's channel. - The server validates everything again. A modified client could send anything.
- This also works in single player, so you don't need two versions of the code.
Timed actions
Actions with a progress bar and an animation (eating, building, dancing) are timed actions. You create them by deriving from ISBaseTimedAction:
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")
endAPI pitfalls we ran into in B42
getCell():getVehicles()is now a Set, with no:get(i). To iterate over objects, usegetCell():getObjectListForLua().- Tile properties are checked with
:has(IsoFlagType.water), in lowercase.:Is(...)doesn't exist and gives youtried to call nil. HaloTextHelper.addText(player, texto, color)doesn't exist. UseaddGoodText(green),addBadText(red) oraddText(player, texto).