WENOPAL

15 · Depuración en serio

Cómo encontrar la causa de un bug en PZ con método: dónde está el log, cómo leer los errores de Kahlua y cómo aislar el problema.

Curso técnico · Build 42 · Actualizado el

Objetivo: cuando algo falla (o peor, cuando "no pasa nada"), encontrar la causa con método y no a tientas. Este módulo junta las herramientas de todo el curso y los errores que nos pasaron en nuestros mods.

1. Dónde está la evidencia

QuéDónde
Log de la sesión actual~/Zomboid/Logs/<fecha>_DebugLog.txt
Sesiones anteriores~/Zomboid/Logs/logs_<fecha-hora>/ (cada vez que abres el juego, la anterior se archiva ahí)
Consola~/Zomboid/console.txt (otro flujo: puede no tener la sesión que buscas)
Servidor (multijugador)server-console.txt en la carpeta del servidor; el Lua de server/ escribe ahí, no en tu cliente

Lo que vas a buscar:

  • Tus print salen como LOG : Lua f:0> [CursoLab] .... Ponle siempre prefijo con el nombre del mod.
  • Un error de tu mod sale como ERROR: General ... Lua((MOD:<nombre>))... seguido de STACK TRACE con archivo y línea.
  • player:Say(...) no queda en ningún log. Para depurar, print además.
sh
grep -n -A14 "MOD:CursoLab" ~/Zomboid/Logs/*_DebugLog.txt          # todo lo de tu mod con contexto
grep -c "Exception thrown" ~/Zomboid/Logs/*_DebugLog.txt            # cuántos errores hubo

2. Leer los errores de Kahlua

Los mensajes de Kahlua son cortos y engañosos. El diccionario:

MensajeQué significa casi siempre
Object tried to call nil in <función>Llamaste un método que no existe en ese objeto: mal escrito (:Is en vez de :has), o existe en otra clase (setWorldZRotation es de InventoryItem, no de IsoWorldInventoryItem)
attempted index: X of non-table: nullalgo.X donde algo es nil: un getVehicle() que devolvió nil, una tabla global que todavía no cargó
attempted index: derive of non-table: null en archivos del juegoUn archivo tuyo falló al cargar y rompió el estado del intérprete. El error real está más arriba (el caso de WarFactions, un mod nuestro sin publicar, en la sección 4)
UnknownFormatConversionException ... Formatting "Sandbox_..."Un % suelto en un texto de traducción: el traductor lo pasa por String.format
Nada, pero no funcionaSospecha de un nil silencioso: Kahlua descarta sin error una clave nil en un constructor de tabla ({ [X] = 1 } con X == nil da una tabla vacía)

Dos verdades que te ahorran horas:

  • El primer error es la causa; el resto son consecuencias. Un error al cargar un archivo produce cientos de errores en archivos del juego. Siempre lee desde el primero.
  • pcall no es silencioso en PZ. Atrapa el error para tu código, pero igual lo escribe en el log con STACK TRACE y sube el contador rojo. No sirve para "probar si un método existe". Para eso: local f = obj.metodo; if f then f(obj, ...) end (indexar un método inexistente en un objeto Java devuelve nil sin error).

3. El método

1. Reproducir     → pasos exactos para que pase siempre. Sin esto, no sabes si lo arreglaste.
2. Aislar         → ¿es tu mod? ¿qué archivo? ¿qué línea?
3. Primer error   → en el log, el primero de la sesión que nombra tu mod.
4. Hipótesis      → una explicación concreta y comprobable.
5. Verificar      → en el Lua del juego (_juego/lua) o en el .jar (módulos 07 y 08), no en la memoria.
6. Arreglar       → el cambio mínimo.
7. Comprobar      → los pasos del 1 ya no fallan, y lo que funcionaba sigue funcionando.
8. Anotar         → una entrada en tus notas: síntoma, causa, arreglo, lección.

Aislar: bisectar mods

Si no sabes qué mod causa un problema: desactiva la mitad, prueba, y sigue con la mitad culpable. Con 32 mods son 5 pruebas. Lo mismo dentro de un mod: comenta la mitad de los archivos que registran eventos.

Herramientas

HerramientaPara
Consola Lua (tecla ` `` en debug)Probar una línea en el juego vivo; inspeccionar objetos con tu CursoLab.dump (desafío 6)
Depurador de Lua (F11 en debug)Puntos de quiebre, ver variables, recargar un archivo
luajit -bl archivo.luaComprobar sintaxis sin abrir el juego (LuaJIT es Lua 5.1, igual que Kahlua; en macOS se instala con Homebrew)
Un simulador offlineProbar lógica pura sin el juego. Finish Him, uno de nuestros mods, tiene uno que reemplaza pcall para contar errores como el juego
javap / VineflowerConfirmar que un método existe y qué hace (módulo 08)
Un servidor localReproducir bugs que solo pasan con dos jugadores (módulo 12)

4. Programar para que los bugs se vean

  • Nada que pueda fallar en el nivel superior de un archivo. Envuélvelo en una función que corre en un evento. Un error al cargar no solo rompe tu archivo: deja el intérprete en mal estado y los require siguientes del juego fallan (nos pasó en WarFactions, donde uuid.uuid5 no existía en Kahlua).
  • require lo que usas al cargar. Los archivos de una carpeta cargan en orden alfabético: en un mod nuestro sin publicar (WowArmory), WowArmory antes que WowConstants dejó una tabla vacía sin ningún error.
  • Un log que siempre está. Una línea con prefijo por cada cosa importante que hace tu mod, siempre activa, y uno detallado detrás de una opción de sandbox de depuración. Otro mod nuestro sin publicar (Zombie Piensa) lo explica en un comentario: "sin esto los errores quedan ocultos".
  • Verifica los nombres contra el juego, no contra la memoria: rg -n ":has\(IsoFlagType" _juego/lua antes de escribir :Is(.

Ejercicios: bugs plantados

Cada ejercicio es un archivo de CursoLab con un bug real (todos nos pasaron en nuestros mods). Para cada uno: pégalo en CursoLab, reprodúcelo, encuentra la causa con el log y arréglalo. No mires la pista hasta tener una hipótesis.

Bug 15.1 · "No pasa nada"

shared/CursoLab_Armas.lua:

lua
CursoLab = CursoLab or {}
CursoLab.armasPorFaccion = {
    [CursoLab.F_HUMANO] = { "Base.Axe", "Base.Bat" },
    [CursoLab.F_ORCO]   = { "Base.Machete" },
}

shared/CursoLab_Constantes.lua:

lua
CursoLab = CursoLab or {}
CursoLab.F_HUMANO = "humano"
CursoLab.F_ORCO = "orco"

Síntoma: CursoLab.armasPorFaccion.humano es nil. No hay ningún error en el log.

Pista

Orden alfabético de carga y claves nil en un constructor. Arreglo: require "CursoLab_Constantes" al principio del primer archivo.

Bug 15.2 · El agua

server/CursoLab_Agua.lua:

lua
local function esAgua(sq)
    local floor = sq:getFloor()
    return floor and floor:getProperties():Is(IsoFlagType.water)
end
Events.LoadGridsquare.Add(function(sq)
    if esAgua(sq) then CursoLab.log("agua en", sq:getX(), sq:getY()) end
end)

Síntoma: el log se llena de errores al moverte por el mapa.

Pista

Object tried to call nil in esAgua. Busca en _juego/lua cómo pregunta el juego por IsoFlagType.water.

Bug 15.3 · El pcall que "no falla"

lua
Events.OnTick.Add(function()
    local p = getPlayer()
    local ok, loc = pcall(function() return p:getBodyLocation() end)
    if ok then CursoLab.log(loc) end
end)

Síntoma: el contador rojo de errores sube sin parar, aunque todo está en pcall.

Pista

pcall no es silencioso en PZ, y este se ejecuta cada tick. Comprueba si el método existe una sola vez (p.getBodyLocation) en vez de tantear.

Bug 15.4 · El archivo que rompe el juego

shared/CursoLab_Ids.lua:

lua
CursoLab = CursoLab or {}
CursoLab.ID_MOD = uuid.uuid5("cursolab")
function CursoLab.idDe(nombre) return CursoLab.ID_MOD .. ":" .. nombre end

Síntoma: al entrar a la partida, cientos de errores en ISPanel.lua, ISUIElement.lua y otros archivos del juego. Algunas ventanas del juego no abren.

Pista

Busca el primer error del log. Kahlua no tiene librerías externas. Arreglo: un hash propio (por ejemplo djb2) y nunca código que pueda fallar en el nivel superior.

Bug 15.5 · El texto que no carga

media/lua/shared/Translate/ES/Sandbox.json:

json
{ "Sandbox_CursoLab_Peso_tooltip": "Probabilidad de loot, en %. 100% = normal." }

Síntoma: un error de formato al abrir las opciones de sandbox.

Pista

El traductor pasa los textos por String.format. Cambia el texto para no usar %.

Bug 15.6 · Solo en multijugador

Tu ejercicio 12.2 funciona en un jugador. En el servidor local, el cliente pide la sopaipilla y no recibe nada. En el log del cliente no hay errores. (Este no tiene pista: usa el método completo, incluyendo el log del servidor.)

Desafío 15

Escribe pzlog.py: lee un DebugLog.txt, agrupa los errores por mod y por mensaje, cuenta cuántas veces se repite cada uno, y para cada grupo muestra el primer stack trace completo. Úsalo con un log real de una sesión con todos los mods que usas activos y arregla el error más repetido.


Anterior: 14 · Datos persistentes y rendimiento · Siguiente: 16 · Casos reales