WENOPAL

01 · Anatomía del juego y de un mod

Entiende cómo se reparten Java y media/, cómo se juntan los mods con el juego, el orden de carga del Lua y cómo rastrear cualquier texto o ítem.

Curso técnico · Build 42 · Actualizado el

Objetivo: saber de dónde sale cada cosa que ves en el juego, y poder encontrarla en los archivos en menos de un minuto. Esta habilidad vale más que cualquier guía: cuando no sepas hacer algo, vas a buscar cómo lo hace el juego.

1. Dos mundos: Java y media/

Project Zomboid tiene dos capas:

  • El motor, en Java (projectzomboid.jar): render, física, zombis, red, guardado, el parser de scripts, la máquina virtual de Lua. Todo lo que tiene que ser rápido.
  • El contenido, en media/: datos (scripts .txt, traducciones .json, texturas, modelos, sonidos) y lógica de juego en Lua (casi toda la interfaz, los menús contextuales, las acciones, el crafteo de alto nivel).

Regla práctica: si algo se ve en un menú o es una acción del jugador, casi seguro está en Lua y lo puedes leer y cambiar. Si es cómo se mueve un zombi o cómo se calcula el daño, está en Java: se lee (módulo 08) pero se cambia con más límites (módulo 09).

2. Qué hay en media/

CarpetaContenidoFormato
scripts/Ítems, recetas, modelos, vehículos, sonidos, entidades.txt con sintaxis propia (módulo 02)
lua/shared/Código que corre en cliente y servidor; traducciones en Translate/<IDIOMA>/.lua, .json
lua/client/Interfaz, menús, input, todo lo que ve un jugador.lua
lua/server/Lógica del mundo: loot (Items/), comandos de servidor.lua
textures/Íconos (Item_<Nombre>.png) y texturas de modelos.png
models_X/Modelos 3D.fbx, .x
clothing/Definición de prendas.xml
fileGuidTable.xmlTabla que une los GUID de la ropa con su archivo.xml
sound/, ui/, maps/, anims_X/, AnimSets/Sonidos, imágenes de UI, mapas, animacionesvarios

Además, muchas texturas del juego base vienen empaquetadas en .pack (atlas): por eso no encuentras Item_Apple.png suelto. Lo vemos en el módulo 04.

3. Cómo se juntan los mods con el juego

El juego arma un sistema de archivos virtual: primero media/ del juego y encima la carpeta media/ de cada mod activo, en el orden de carga.

Consecuencias que tienes que tener claras:

  1. Mismo camino = reemplazo. Si tu mod trae media/lua/client/ISUI/ISInventoryPaneContextMenu.lua, reemplaza el archivo del juego entero. Funciona, pero rompe con cada parche y con cualquier otro mod que haga lo mismo. Casi nunca es la respuesta: en el módulo 09 vemos cómo parchear funciones sin reemplazar archivos.
  2. Nombres únicos. Pon el nombre del mod en tus archivos (CursoLab_Core.lua, no Core.lua) para no pisar archivos de otros mods sin querer.
  3. Los scripts .txt se suman, no se reemplazan por archivo. Todos los .txt de scripts/ (del juego y de los mods) se leen y se juntan por module. Si dos definen el mismo item, gana el último en cargar.

4. Orden de carga del Lua

Dentro de cada mod y del juego:

  1. shared/, después client/, después server/ (en una partida local se cargan los tres; en un servidor dedicado el servidor no carga client/, y los clientes no ejecutan server/).
  2. Dentro de cada carpeta, por orden alfabético de la ruta. Por eso el juego tiene archivos llamados a_requires.lua: para que carguen primero.
  3. require "Carpeta/Archivo" fuerza a cargar otro archivo antes (sin .lua, relativo a shared/, client/ o server/). Se ejecuta una sola vez aunque lo pidas muchas.

Todo esto pasa al arrancar el juego, no al cargar la partida. Por eso un print suelto en tu archivo sale en el menú principal. Lo que dependa del mundo (el jugador, el mapa) tiene que esperar a un evento.

La línea de tiempo (eventos principales)

arranque del juego
  ├─ se cargan los .lua (shared → client → server)
  ├─ OnGameBoot                     ← el juego terminó de cargar el Lua
  └─ OnMainMenuEnter                ← estás en el menú
cargar una partida
  ├─ OnInitGlobalModData            ← los datos guardados de los mods están disponibles
  ├─ OnPreDistributionMerge / OnDistributionMerge / OnPostDistributionMerge   ← loot
  ├─ OnInitWorld, OnLoad
  ├─ OnCreatePlayer                 ← existe el jugador (uno por jugador local)
  ├─ OnGameStart                    ← la partida empezó
  └─ OnTick, OnPlayerUpdate, EveryOneMinute, EveryTenMinutes...   ← mientras juegas

La lista completa de eventos (252 en B42) está en la referencia del sitio: wenopal.work/referencia/eventos.html, con ejemplos de dónde los usa el juego.

5. Nombres completos e identificadores

  • Un ítem se llama por módulo + nombre: Base.Apple, CompleteItaliano.CompletoItaliano (Tocomples Food, uno de nuestros mods, usa el módulo CompleteItaliano).
  • Su nombre visible no está en el script sino en la traducción: Translate/ES/ItemName.json → "Base.Apple": "Manzana".
  • Los textos de la interfaz son claves (ContextMenu_Eat, IGUI_..., Tooltip_...) que se resuelven con getText("Clave").

Con esto, todo texto que ves en el juego se puede rastrear hacia atrás: texto → clave en un .json → getText("clave") en un .lua → la función que lo muestra.


Ejercicios

Para todos usa rg (ripgrep) o la búsqueda de VS Code sobre ~/Zomboid/Workshop/_juego/.

Ejercicio 1.1 · Rastrear un ítem

Para la lupa (Loupe), encuentra:

  1. el archivo y la línea donde se define el ítem,
  2. su nombre en español y portugués,
  3. todos los archivos .lua que hacen algo con lo que la lupa es (pista: mira sus Tags).

Listo cuando: puedes explicar en una frase para qué sirve la lupa según el código, no según una wiki.

Respuesta
  1. scripts/generated/items/normal.txt, item Loupe, con Tags = base:hasmetal;base:magnifier.
  2. Translate/ES/ItemName.json y Translate/PTBR/ItemName.json, clave Base.Loupe.
  3. rg -n "MAGNIFIER" lua/ → shared/TimedActions/ISResearchRecipe.lua (investigar recetas es 10 % más rápido con una lupa en la mano) y client/Farming/ISUI/ISFarmingMenu.lua + ISFarmingInfo.lua (suma 2 niveles de agricultura para ver plagas). En Lua el tag aparece como ItemTag.MAGNIFIER, no como "base:magnifier": es un enum que viene de Java.

Ejercicio 1.2 · Rastrear un texto de menú

Haz clic derecho sobre una comida en el inventario: aparece "Comer" (o Eat). Encuentra:

  1. la clave de traducción de ese texto,
  2. el archivo Lua y la función que agrega esa opción al menú,
  3. la acción (ISxxxAction) que se ejecuta al elegirla.

Listo cuando: tienes las tres respuestas con ruta y número de línea.

Ejercicio 1.3 · Orden de carga en la práctica

En CursoLab, crea tres archivos con un CursoLab.log(...) cada uno: shared/CursoLab_A.lua, client/CursoLab_B.lua, server/CursoLab_C.lua. Agrega Events.OnGameBoot.Add(...) en uno de ellos.

  1. Antes de abrir el juego, escribe en qué orden crees que van a salir los mensajes.
  2. Ábrelo y compara con console.txt.
  3. Renombra CursoLab_A.lua a ZZ_CursoLab_A.lua. ¿Cambia algo? ¿Por qué no rompe nada que client/ use CursoLab.log si CursoLab_Core.lua está en shared/?

Ejercicio 1.4 · Reemplazar un archivo (y ver por qué no se hace)

  1. Copia _juego/lua/client/ISUI/ISInventoryPaneContextMenu.lua a CursoLab (misma ruta) y cambia una línea para que el texto "Comer" sea "Comer ya".
  2. Comprueba que funciona.
  3. Ahora borra la copia y escribe en tus notas: ¿qué pasaría con este mod después del próximo parche del juego? ¿Y si otro mod hace lo mismo con el mismo archivo?

El módulo 09 resuelve esto bien.

Desafío 1

Escribe un script (Python o shell) pzfind que reciba un texto visible del juego en español (por ejemplo "Investigar") y muestre: la clave de traducción, los .lua que usan esa clave con número de línea, y la función donde aparece cada uso. Lo vas a usar en todo el curso.


Anterior: 00 · Entorno · Siguiente: 02 · Scripts: ítems