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.getModVersionDirNamemira 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, usa42.- 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 conversionMin=41.xno 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=OtroModhace que el juego exija el otro mod. Varios ids separados por coma.loadModAfter/loadModBeforeordenan 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).versionMaxbloquea el mod en versiones más nuevas que no probaste. Úsalo con cuidado: también bloquea a quien quiera probar.source=yworkshopId=no se leen: el juego deduce de dónde viene el mod por la carpeta en que lo encontró.
2. Publicar
Dos caminos:
- El subidor del juego (menú principal → Workshop). Sirve para la primera vez y para entender el flujo.
- 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 ve | Quién lo descarga |
|---|---|---|
public | todos | todos |
unlisted | quien tenga el enlace (no sale en búsquedas) | todos, incluido un servidor dedicado |
private | solo 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:
- Guarda una copia del juego por versión. Antes de actualizar, mueve
_juego/a_juego_42.21/(por ejemplo); después vuelve a copiarlua,scriptsy el.jar. - Compara el Lua:
diff -rq _juego_42.21/lua _juego/luate da los archivos que cambiaron. Cruza esa lista con los archivos que tu mod envuelve (buscarequire "y los nombres de funciones que envuelves). - Compara la API de Java de las clases que usas:
javap -publicde cada versión ydiff. Un método que desapareció es un "tried to call nil" esperando. - 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: ... }→craftRecipeconinputs/outputs(módulo 03). - Traducciones:
.txt→ JSON (módulo 13). - Timed actions: efecto en
complete(), clases enshared/(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).sendSquareChangeno existe.PrintMediaDefinitions.Fliersno 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:
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