WENOPAL

Lua para mods de Project Zomboid: guía práctica

Cliente, servidor y shared; eventos; cómo hacer que algo funcione en multijugador; y las trampas del Lua de Project Zomboid.

Nivel intermedio · Actualizada el

Los scripts .txt definen qué existe. El Lua define qué pasa: el loot, un menú nuevo con clic derecho, una acción con animación, una luz que sigue al jugador, un cañón de agua en un camión.

El Lua de Project Zomboid es Lua 5.1

El juego usa Kahlua, una versión de Lua 5.1 que corre dentro de Java. Si aprendiste Lua en otro lado, ojo con estas diferencias:

  • No existe goto ni etiquetas ::algo::. Para saltar un paso dentro de un for, usa un if.
  • No existe la división entera //. Usa math.floor(a / b).
  • No existen &, |, ~, << ni >>.
  • Las palabras reservadas (end, function, repeat...) no pueden ser claves sueltas: escribe t["end"], no t.end.
  • Cuando llamas un método que no existe, el error es un críptico tried to call nil, y aparece al cargar la partida, no al cargar el mod.

Cliente, servidor y shared

text
media/lua/
├── client/   <- solo en la PC del jugador: menús, interfaz, efectos visuales
├── server/   <- solo en el servidor: loot, reglas, lo que "manda"
└── shared/   <- en los dos: datos, utilidades y acciones con tiempo

En un jugador tu PC hace de cliente y de servidor a la vez, así que todo corre. La diferencia importa en multijugador, y es mejor pensar así desde el principio: el mod funciona en los dos modos sin cambios.

Un espacio de nombres por mod

Todos los mods comparten el mismo Lua global. Para no pisar a otros, mete todo lo tuyo en una sola tabla:

lua
MiMod = MiMod or {}

function MiMod.saludar(player)
    player:Say("Weno pa'l leseo")
end

Nunca dejes variables globales sueltas como contador = 0: tarde o temprano chocan con las de otro mod.

Eventos: cuándo se ejecuta tu código

Tu código se "cuelga" de eventos del juego:

lua
Events.OnGameStart.Add(function()
    print("[MiMod] la partida empezó")
end)

La lista completa de B42 está en la referencia de eventos de Lua. Algunos que usamos en mods publicados:

EventoPara qué lo usamos
OnPostDistributionMergeAgregar loot (ver Loot)
OnTickCosas continuas, como una luz que sigue al jugador. Limítalo: corre en cada frame.
OnClientCommandEl servidor recibe pedidos de los jugadores
OnSaveLimpiar cosas temporales antes de guardar
OnPlayerDeathLimpiar lo que tenía el jugador
OnWeaponSwingReaccionar al golpe de un arma (la pistola de gravedad)

Para que OnTick no pese, haz el trabajo cada cierto tiempo:

lua
local ultimo = 0
Events.OnTick.Add(function()
    local ahora = getTimestampMs()
    if ahora - ultimo < 1000 then return end   -- una vez por segundo
    ultimo = ahora
    -- trabajo pesado aquí
end)

Multijugador: el cliente pide, el servidor decide

Si un jugador hace algo que cambia el mundo, el cliente no lo aplica directamente: le pide al servidor que lo haga. El servidor revisa que tenga sentido y lo aplica para todos. Así funciona el cañón de agua de Wanaco:

lua
-- client/: el jugador aprieta el botón
sendClientCommand(player, "Wanaco", "toggleCannon", { on = true })
lua
-- server/: el servidor revisa y aplica
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               -- nunca confíes en el cliente
    -- ... encender el cañón y avisar a todos
end)
  • El primer texto ("Wanaco") es el canal de tu mod.
  • El servidor vuelve a validar todo. Un cliente modificado podría mandar cualquier cosa.
  • Esto también funciona en un jugador, así que no necesitas dos versiones del código.

Acciones con tiempo (timed actions)

Las acciones con barra de progreso y animación (comer, construir, bailar) son timed actions. Se crean derivando de ISBaseTimedAction:

lua
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

Trampas de API que encontramos en B42

  • getCell():getVehicles() ahora es un Set, sin :get(i). Para recorrer objetos usa getCell():getObjectListForLua().
  • Las propiedades de tiles se revisan con :has(IsoFlagType.water), en minúscula. :Is(...) no existe y da tried to call nil.
  • HaloTextHelper.addText(player, texto, color) no existe. Usa addGoodText (verde), addBadText (rojo) o addText(player, texto).