WENOPAL

Lua para mods de Project Zomboid: guia prático

Cliente, servidor e shared; eventos; como fazer algo funcionar no multijogador; e as armadilhas do Lua de Project Zomboid.

Nível intermediário · Atualizado em

Os scripts .txt definem o que existe. O Lua define o que acontece: o loot, um menu novo no clique direito, uma ação com animação, uma luz que segue o jogador, um canhão de água num caminhão.

O Lua de Project Zomboid é Lua 5.1

O jogo usa o Kahlua, uma versão do Lua 5.1 que roda dentro do Java. Se você aprendeu Lua em outro lugar, fique de olho nestas diferenças:

  • Não existe goto nem rótulos ::algo::. Para pular um passo dentro de um for, use um if.
  • Não existe a divisão inteira //. Use math.floor(a / b).
  • Não existem &, |, ~, << nem >>.
  • As palavras reservadas (end, function, repeat...) não podem ser chaves soltas: escreva t["end"], não t.end.
  • Quando você chama um método que não existe, o erro é um enigmático tried to call nil, e ele aparece ao carregar a partida, não ao carregar o mod.

Cliente, servidor e shared

text
media/lua/
├── client/   <- só no PC do jogador: menus, interface, efeitos visuais
├── server/   <- só no servidor: loot, regras, o que "manda"
└── shared/   <- nos dois: dados, utilitários e ações com tempo

No single player o seu PC faz o papel de cliente e de servidor ao mesmo tempo, então tudo roda. A diferença importa no multijogador, e é melhor pensar assim desde o começo: o mod funciona nos dois modos sem mudanças.

Um namespace por mod

Todos os mods compartilham o mesmo Lua global. Para não atropelar os outros, coloque tudo o que é seu em uma única tabela:

lua
MiMod = MiMod or {}

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

Nunca deixe variáveis globais soltas como contador = 0: cedo ou tarde elas batem de frente com as de outro mod.

Eventos: quando o seu código roda

O seu código se "pendura" em eventos do jogo:

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

A lista completa da B42 está na referência de eventos de Lua. Alguns que usamos em mods publicados:

EventoPara que usamos
OnPostDistributionMergeAdicionar loot (veja Loot)
OnTickCoisas contínuas, como uma luz que segue o jogador. Limite ele: roda a cada frame.
OnClientCommandO servidor recebe pedidos dos jogadores
OnSaveLimpar coisas temporárias antes de salvar
OnPlayerDeathLimpar o que o jogador tinha
OnWeaponSwingReagir ao golpe de uma arma (a pistola de gravidade)

Para o OnTick não pesar, faça o trabalho de tempos em tempos:

lua
local ultimo = 0
Events.OnTick.Add(function()
    local ahora = getTimestampMs()
    if ahora - ultimo < 1000 then return end   -- uma vez por segundo
    ultimo = ahora
    -- trabalho pesado aqui
end)

Multijogador: o cliente pede, o servidor decide

Se um jogador faz algo que muda o mundo, o cliente não aplica isso diretamente: ele pede ao servidor que faça. O servidor confere se faz sentido e aplica para todo mundo. É assim que funciona o canhão de água do Wanaco:

lua
-- client/: o jogador aperta o botão
sendClientCommand(player, "Wanaco", "toggleCannon", { on = true })
lua
-- server/: o servidor confere e 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 confie no cliente
    -- ... ligar o canhão e avisar todo mundo
end)
  • O primeiro texto ("Wanaco") é o canal do seu mod.
  • O servidor valida tudo de novo. Um cliente modificado poderia mandar qualquer coisa.
  • Isso também funciona no single player, então você não precisa de duas versões do código.

Ações com tempo (timed actions)

As ações com barra de progresso e animação (comer, construir, dançar) são timed actions. Elas são criadas 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

Armadilhas de API que encontramos na B42

  • getCell():getVehicles() agora é um Set, sem :get(i). Para percorrer objetos, use getCell():getObjectListForLua().
  • As propriedades dos tiles são verificadas com :has(IsoFlagType.water), em minúscula. :Is(...) não existe e dá tried to call nil.
  • HaloTextHelper.addText(player, texto, color) não existe. Use addGoodText (verde), addBadText (vermelho) ou addText(player, texto).