WENOPAL

17 · Publicar y mantener

Publicar un mod en el Workshop entendiendo cada archivo, y mantenerlo cuando el juego se actualiza: versiones, dependencias y migración de datos.

Curso técnico · Build 42 · Actualizado el

Objetivo: publicar un mod en el Workshop sabiendo qué hace cada archivo, y mantenerlo vivo cuando el juego se actualiza: detectar qué se rompió, migrar datos guardados y declarar bien las dependencias. Nosotros publicamos los mods de Wenopal con nuestro script de publicación; aquí va el porqué de cada pieza.

1. Anatomía de un item del Workshop

CursoLab/                          ← carpeta del item (en ~/Zomboid/Workshop/)
├── workshop.txt                   ← título, descripción, visibilidad, id del item
├── preview.png                    ← portada del Workshop (nuestros mods usan GIF animado)
└── Contents/
    └── mods/
        └── CursoLab/              ← un item puede traer varios mods
            ├── common/            ← se carga siempre
            │   └── media/...
            └── 42/                ← carpeta versionada
                ├── mod.info
                ├── poster.png
                └── media/...

Carpetas versionadas (comprobado en el .jar)

  • ZomboidFileSystem.getModVersionDirName mira las subcarpetas con nombre de versión y elige la más alta que sea ≥ 42 y ≤ la versión del juego. Si ninguna sirve, usa 42.
  • El nombre es 42, 42.12... (el tercer número se ignora).
  • common/ se carga siempre, debajo de la carpeta versionada: si un archivo está en las dos, gana la versionada.
  • El mínimo es B42: ChooseGameInfo.minRequiredVersion = 42. Un mod con versionMin=41.x no carga.

Esto te permite mantener una sola publicación para varias versiones del juego: lo común en common/, y en 42.15/ solo lo que cambió desde esa versión (como hace Catimbas, uno de nuestros mods, con common/ para todo el contenido).

mod.info

Claves reconocidas (ChooseGameInfo, distinguen mayúsculas): id, name, description, author, modversion, versionMin, versionMax, require, incompatible, loadModAfter, loadModBefore, poster (repetible), icon, category, url, pack, tiledef, más las de mapas.

  • require=OtroMod hace que el juego exija el otro mod. Varios ids separados por coma.
  • loadModAfter/loadModBefore ordenan la carga sin exigir el otro mod: es la herramienta para la compatibilidad del módulo 09 (asegurar que tu envoltura se aplica después de la de otro mod).
  • versionMax bloquea el mod en versiones más nuevas que no probaste. Úsalo con cuidado: también bloquea a quien quiera probar.
  • source= y workshopId= no se leen: el juego deduce de dónde viene el mod por la carpeta en que lo encontró.

2. Publicar

Dos caminos:

  1. El subidor del juego (menú principal → Workshop). Sirve para la primera vez y para entender el flujo.
  2. Un script de publicación propio, como el nuestro, que usa la misma API de Steam (ISteamUGC) que el juego, con el cliente de Steam abierto. El nuestro sube solo los archivos cambiados, pone las etiquetas y crea el item si es nuevo.

Visibilidad

visibility=Quién lo veQuién lo descarga
publictodostodos
unlistedquien tenga el enlace (no sale en búsquedas)todos, incluido un servidor dedicado
privatesolo túsolo tú: los jugadores no pueden entrar a un servidor que lo use

Para un mod "solo de mi servidor", unlisted, nunca private.

Antes de subir

  • Compara tu carpeta con la copia publicada que tienes suscrita: diff -rq <Mod>/Contents <Steam>/steamapps/workshop/content/108600/<id>/ (en macOS, <Steam> es ~/Library/Application Support/Steam). Todo lo que salga distinto se va a publicar: confirma que es a propósito.
  • Corre tus herramientas de revisión: pzlint (02), pzicons (04), pztrad (13), pzaudit (16).
  • Prueba en un jugador y en el servidor local (12).

3. Cuando el juego se actualiza

Un parche puede cambiar el Lua del juego, el Java, o los dos. Tu mod se rompe cuando algo que usabas cambió. El método:

  1. Guarda una copia del juego por versión. Antes de actualizar, mueve _juego/ a _juego_42.21/ (por ejemplo); después vuelve a copiar lua, scripts y el .jar.
  2. Compara el Lua: diff -rq _juego_42.21/lua _juego/lua te da los archivos que cambiaron. Cruza esa lista con los archivos que tu mod envuelve (busca require " y los nombres de funciones que envuelves).
  3. Compara la API de Java de las clases que usas: javap -public de cada versión y diff. Un método que desapareció es un "tried to call nil" esperando.
  4. Corre tu mod con el log abierto (módulo 15) y arregla desde el primer error.

Cosas que ya cambiaron de B41 a B42 (para leer mods viejos)

  • Recetas: recipe { Result: ... } → craftRecipe con inputs/outputs (módulo 03).
  • Traducciones: .txt → JSON (módulo 13).
  • Timed actions: efecto en complete(), clases en shared/ (módulo 11).
  • getCell():getVehicles() ya no es una lista con :get(i); para iterar desde Lua, getObjectListForLua() (comentario en Wanaco, uno de nuestros mods).
  • sendSquareChange no existe.
  • PrintMediaDefinitions.Fliers no existe (por eso retiramos un mod nuestro, Poster18).

4. Migrar datos guardados

Una partida guardada con tu versión 1.0 tiene ModData con el formato de la 1.0. Si la 2.0 lo cambia, el jugador que actualiza trae los datos viejos. Por eso el campo v del módulo 14:

lua
local ACTUAL = 3
local migraciones = {
    [1] = function(d) d.comidas = { total = d.comidas or 0, porJugador = {} } end,   -- 1 → 2
    [2] = function(d) d.opciones = d.opciones or {} end,                              -- 2 → 3
}
Events.OnInitGlobalModData.Add(function()
    local d = ModData.getOrCreate("CursoLab")
    d.v = d.v or 1
    while d.v < ACTUAL do
        migraciones[d.v](d)
        d.v = d.v + 1
        print("[CursoLab] datos migrados a v" .. d.v)
    end
end)

Lo mismo para el ModData de objetos e ítems, que se migra cuando lo lees (los objetos se cargan de a poco, no todos al empezar).

Y para los ítems: si renombras o borras un ítem, las partidas que lo tenían pierden esos ítems. Antes de renombrar, piensa si vale la pena; si es necesario, deja el ítem viejo como obsoleto (oculto, sin loot ni recetas) durante algunas versiones.


Ejercicios

Ejercicio 17.1 · Publicar CursoLab oculto

Publica CursoLab como unlisted con el subidor del juego (o con tu propio script de publicación). Comprueba con la API pública de Steam (GetPublishedFileDetails) que la visibilidad es la correcta, y suscríbete desde otra cuenta o pídele a alguien que lo pruebe.

Ejercicio 17.2 · Una carpeta por versión

Mueve todo el contenido de CursoLab a common/ y deja en 42/ solo mod.info y el póster. Después crea una carpeta 42.99/ con un archivo Lua que imprima "versión futura". ¿Carga? ¿Por qué? Explícalo con la regla de getModVersionDirName.

Ejercicio 17.3 · Simular una actualización

Copia _juego/lua a _juego_antes/lua y cambia a mano, en la copia "nueva", la firma de una función que CursoLab envuelve (por ejemplo, agrega un parámetro a ISEatFoodAction:complete). Escribe un script que detecte qué funciones envueltas por tu mod cambiaron entre las dos copias.

Ejercicio 17.4 · Migración

Con el contador global del 12.5: publica (en tu carpeta local) una "versión 1" con un formato, juega y guarda; cambia a una "versión 2" con otro formato y la migración. Carga la partida vieja y comprueba que no se perdió nada.

Desafío 17

Integra todas tus herramientas (pzlint, pzicons, pztrad, pzaudit, la comparación de versiones del 17.3) en un solo pzcheck.py <Mod> que corras antes de cada publicación, y agrégalo como paso de tu script de publicación, si tienes uno (que se niegue a subir si hay errores graves).


Proyecto final

Un mod completo de tu elección que use al menos:

  • ítems y recetas propios (02, 03), con íconos y un modelo (04, 05);
  • Lua organizado con eventos (06) y al menos una envoltura compatible (09);
  • un menú contextual y una ventana (10);
  • una timed action (11);
  • funcionamiento verificado en multijugador (12);
  • una opción de sandbox, loot y traducciones a ES, EN y PT (13);
  • datos persistentes con versión de formato (14, 17);
  • una entrada en tus notas con al menos un bug que encontraste y cómo lo resolviste (15, 16).

Publícalo primero como unlisted, pruébalo con otra persona en el servidor local, y después hazlo público.


Anterior: 16 · Casos reales · Volver al temario