Objetivo: repartir tu mod en varios archivos que carguen en el orden correcto, se usen entre sí con require y no choquen con el juego ni con otros mods. Un mod ordenado es más fácil de arreglar, y un error en un archivo no tumba todo lo demás.
1. Tres carpetas: shared, client, server
Todo el Lua de tu mod va dentro de media/lua/ (en Build 42, 42/media/lua/ de tu mod), repartido en tres carpetas:
| Carpeta | Qué va ahí | Ejemplos |
|---|---|---|
shared/ | Lo que necesitan todos: la tabla del mod, configuración, utilidades, definiciones, clases de acciones | CursoLab_Core.lua, tablas de datos |
client/ | Lo que ve y toca un jugador: menús contextuales, ventanas, teclas, sonidos | CursoLab_Menu.lua |
server/ | Lo que decide el mundo: loot, respuestas a comandos de los clientes | CursoLab_Server.lua |
Cuándo carga cada una (esto sale de zombie.Lua.LuaManager y sus llamadas en Java):
- Al arrancar el juego, antes del menú principal: primero
shared/, despuésclient/. - Al cargar una partida:
server/. - En un servidor dedicado:
shared/yserver/. Declient/solo se calcula la suma de verificación (para comprobar que los clientes tienen los mismos archivos), pero no se ejecuta.
Ojo con un detalle: la carga de server/ al entrar a una partida ocurre también en los clientes de multijugador (GameLoadingState.enter() la pide sin preguntar si eres cliente). Por eso los archivos de server/ del juego que solo deben correr en el servidor empiezan cortando la ejecución:
if isClient() then return endUn return en el nivel superior de un archivo termina ese archivo ahí mismo: lo que viene después no se define. Es la forma más simple de decir "este archivo no es para esta máquina". El juego hace lo mismo al revés en media/lua/client/Foraging/forageClient.lua:
if isServer() then return; end;El chequeo al inicio de un archivo de server/ funciona porque esa carpeta carga cuando ya se sabe si eres cliente. Para todo lo demás, pregunta isClient() / isServer() dentro de tus funciones, cuando se ejecutan: es lo seguro en cualquier carpeta. El detalle de quién corre qué en multijugador está en 12 · Multijugador.
2. El orden de carga dentro de una carpeta
Dentro de cada carpeta (shared/, client/ o server/), LuaManager.LoadDirBase arma la lista así:
- Primero todos los archivos del juego de esa carpeta.
- Después los de cada mod, en el orden de carga de los mods.
- Dentro del juego y dentro de cada mod, por orden alfabético de la ruta completa, sin distinguir mayúsculas (Java pasa las rutas a minúsculas y las ordena con
CASE_INSENSITIVE_ORDER). Las subcarpetas cuentan como parte de la ruta.
Consecuencia útil: cuando carga cualquier archivo client/ de tu mod, todo el client/ del juego ya cargó (y todo shared/). Puedes usar ISPanel o ISContextMenu sin miedo.
Consecuencia peligrosa: el orden alfabético tiene sorpresas. Mira estos cuatro archivos de client/:
client/CursoLab_Menu.lua
client/CursoLab/Ventana.lua
client/cursolab_Atajos.lua
client/CursoLab_Core.luaCargan en este orden:
client/CursoLab/Ventana.lua ← "/" va antes que "_" en la tabla de caracteres
client/cursolab_Atajos.lua ← las mayúsculas no importan: "atajos" < "core"
client/CursoLab_Core.lua
client/CursoLab_Menu.luaSi Ventana.lua usa algo de CursoLab_Core.lua al cargar, falla. No dependas del orden alfabético para nada importante: para eso existe require.
3. require: "carga este archivo antes de seguir"
require "CursoLab/Util" -- sin .lua; relativo a shared/, client/ o server/
require "ISUI/ISPanel" -- funciona igual con archivos del juegoLo que hace require en PZ (es LuaManager.GlobalObject.require y LuaManager.RunLuaInternal, en Java):
- Agrega
.luay busca el archivo en las carpetas que ya se cargaron o se están cargando, en orden:shared/,client/,server/. - Si el archivo ya se cargó, no lo vuelve a ejecutar: devuelve lo mismo que devolvió la primera vez. Da igual cuántos archivos lo pidan.
- Si no se cargó todavía, lo ejecuta ahora, y después sigue con tu archivo.
Y sus trampas, que no avisan con un error:
| Situación | Qué pasa |
|---|---|
Escribes mal la ruta (require "CursoLab/Utill") | Solo una advertencia en console.txt: require("CursoLab/Utill") failed. Devuelve nil y tu archivo sigue. El error sale después, donde uses lo que no cargó |
Desde shared/ pides un archivo de client/ | Mismo resultado: cuando carga shared/, la carpeta client/ todavía no está en la lista de búsqueda. Lo mismo desde client/ hacia server/ al arrancar |
| A pide B y B pide A | La segunda vuelta no se ejecuta: advertencia recursive require() y nil |
| El archivo pedido falla al ejecutarse (un error en su nivel superior) | Queda marcado como cargado igual. Los siguientes require de ese archivo devuelven nil sin volver a ejecutarlo |
Regla práctica: shared/ no puede pedir nada de client/ ni de server/. Si algo lo necesitan dos lados, va en shared/.
4. El patrón módulo: un archivo que devuelve una tabla
Un archivo Lua puede terminar con return valor, y ese valor es lo que te entrega require. Así se arma un módulo: un archivo con funciones que no ensucia el entorno global.
-- shared/CursoLab/Util.lua
local Util = {}
local function limitar(x, min, max) -- privada: nadie fuera de este archivo la ve
return math.max(min, math.min(max, x))
end
function Util.porcentaje(parte, total)
if total == 0 then return 0 end
return limitar(math.floor(parte / total * 100 + 0.5), 0, 100)
end
function Util.plural(n, uno, varios)
if n == 1 then return n .. " " .. uno end
return n .. " " .. varios
end
return Util-- client/CursoLab_Menu.lua
local Util = require "CursoLab/Util"
local function avisarManzanas(n)
getPlayer():Say("Tengo " .. Util.plural(n, "manzana", "manzanas"))
endComo require guarda lo que devolvió la primera vez, todos los archivos que piden el módulo reciben la misma tabla: si uno le cambia algo, los demás lo ven. Y si olvidas el return Util al final, require te devuelve nil.
return {
Version = 6,
Zombies = 4,
Distribution = 1,SandboxVars = require "Sandbox/Apocalypse"5. Una sola tabla global por mod
El entorno global de Lua es uno solo, compartido por el juego y por todos los mods activos. Cada global que creas puede chocar con la de otro.
La regla del curso (la misma de 06 · Lua en PZ): una sola global, con el nombre del mod, y todo lo demás dentro de ella o como local.
-- shared/CursoLab_Core.lua
CursoLab = CursoLab or {}
CursoLab.VERSION = "0.2.0"
CursoLab.config = { maxSopaipillas = 4 }
function CursoLab.log(...)
local partes = {}
for i = 1, select("#", ...) do
partes[#partes + 1] = tostring((select(i, ...)))
end
print("[CursoLab] " .. table.concat(partes, " "))
end¿Por qué CursoLab = CursoLab or {} y no CursoLab = {}? Porque así cualquier archivo del mod puede ser el primero: el que llegue primero crea la tabla y los demás le agregan cosas. Con = {}, un archivo que cargue después borraría lo que pusieron los anteriores. El juego usa este mismo patrón en sus propios archivos, por ejemplo la primera línea de media/lua/client/Hotbar/ISHotbarAttachDefinition.lua:
ISHotbarAttachDefinition = ISHotbarAttachDefinition or {};¿Global o módulo? Las dos cosas conviven bien:
| Usa | Para |
|---|---|
La tabla global CursoLab | Lo que otros archivos o otros mods tienen que poder usar (tu API), y lo que inspeccionas desde la consola |
Un módulo con require | Utilidades internas que solo usa tu mod |
local en el archivo | Todo lo que solo usa ese archivo |
Las globales sin querer
La causa más común de una global no es decidirlo: es olvidar el local.
function CursoLab.contar()
total = 0 -- sin local: acabas de crear la global "total"
for i = 1, 10 do total = total + i end
return total
endSi otro mod también usa una global total sin querer, los dos se pisan y el bug aparece "al azar", según qué se ejecutó último. Cada variable nueva dentro de una función: local. Cada función auxiliar de un archivo: local function.
6. No pises las globales del juego
El juego define cientos de globales: todas las clases de interfaz (ISPanel, ISContextMenu...), las de acciones, utilidades como luautils, y también funciones con nombres muy genéricos. En media/lua/shared/env.lua:
function round(num, idp)
local mult = 10^(idp or 0)
return math.floor(num * mult + 0.5) / mult
endround se usa en casi 190 líneas del Lua del juego. Si tu mod define su propia función global round (por ejemplo, una que redondea siempre hacia abajo), cambias el comportamiento de todo el juego y de todos los mods que la usan. Lo mismo con xpairs o strsplit (también en env.lua), o con luautils, que el juego crea con luautils = {}; en media/lua/shared/luautils.lua: un mod que escriba luautils = {} borra todas sus funciones.
Antes de inventar un nombre global, pregúntale a la consola si ya existe:
print(round) -- si imprime algo distinto de nil, el nombre ya está tomado
print(CursoLab)Y si de verdad quieres cambiar una función del juego, hay una forma correcta que no rompe a nadie (guardar la original y llamarla desde la tuya): la ves en 09 · Sobrescribir comportamiento.
7. Poco código en el nivel superior: lo demás, en funciones
El nivel superior de un archivo es lo que no está dentro de ninguna función: se ejecuta en el momento en que el archivo carga. Para shared/ y client/ ese momento es al arrancar el juego, cuando no hay partida: no hay jugador ni mundo.
-- client/CursoLab_Mal.lua
local inv = getPlayer():getInventory() -- ERROR al arrancar: getPlayer() es nil
CursoLab.config.dificil = SandboxVars.CursoLab.Dificil -- al arrancar todavía no son los valores de la partidaPeor que el error en sí es lo que deja: un error al cargar un archivo deja el intérprete de Lua en mal estado, y después pueden fallar require y archivos del propio juego que no tienen nada que ver con tu mod. Un error dentro de una función que corre en un evento, en cambio, solo corta esa función.
La regla: en el nivel superior deja solo definiciones (la tabla del mod, constantes, local function, require) y registros de eventos. Todo lo que haga algo, dentro de funciones:
-- client/CursoLab_Bien.lua
require "CursoLab_Core"
local function alEntrar(playerNum, player)
CursoLab.log("jugador listo:", player:getUsername())
end
local function alEmpezar()
CursoLab.config.dificil = SandboxVars.CursoLab and SandboxVars.CursoLab.Dificil or false
end
Events.OnCreatePlayer.Add(alEntrar)
Events.OnGameStart.Add(alEmpezar)if isServer() then return; end;
forageClient = {};
forageData = {};
function forageClient.init()
forageData = ModData.getOrCreate("forageData");
end
Events.OnInitGlobalModData.Add(forageClient.init)(Fíjate también en lo que no te conviene copiar: forageData es una segunda global suelta. En tu mod iría dentro de la tabla del mod.)
8. La forma de CursoLab desde ahora
Con todo lo anterior, esta es la estructura que vamos a usar en el resto del curso:
42/media/lua/
├── shared/
│ ├── CursoLab_Core.lua ← CursoLab = CursoLab or {}, log, config
│ └── CursoLab/
│ └── Util.lua ← módulo: return Util
├── client/
│ ├── CursoLab_Menu.lua ← require "CursoLab_Core"; menús, eventos de cliente
│ └── CursoLab_Ventana.lua
└── server/
└── CursoLab_Server.lua ← if isClient() then return end; comandos y loot- Los nombres de archivo llevan el nombre del mod (
CursoLab_...o la carpetaCursoLab/), para no reemplazar sin querer un archivo de otro mod con la misma ruta (lo viste en 01 · Anatomía). - Cada archivo que usa algo de otro lo pide con
require, aunque "ya debería estar cargado". - La única global es
CursoLab.
Ejercicios
Ejercicio L7.1 · Adivina el orden
En la carpeta client/ de CursoLab crea estos archivos, cada uno con una sola línea print("[CursoLab] cargó " .. "NOMBRE") (con su nombre):
client/CursoLab_Zeta.lua
client/cursolab_alfa.lua
client/CursoLab/Medio.lua
client/CursoLab_beta.luaAntes de abrir el juego, escribe en qué orden crees que van a salir. Después ábrelo y compara con console.txt.
Listo cuando: tu predicción coincide, y puedes explicar por qué CursoLab/Medio.lua no sale al medio.
Solución
Las rutas se comparan en minúsculas: cursolab/medio.lua, cursolab_alfa.lua, cursolab_beta.lua, cursolab_zeta.lua. El primer carácter distinto entre cursolab/ y cursolab_ es / contra _, y en la tabla de caracteres / (47) va antes que _ (95). Por eso todo lo que está en la subcarpeta CursoLab/ carga antes que cualquier CursoLab_*.lua de la misma carpeta.
[CursoLab] cargó Medio
[CursoLab] cargó alfa
[CursoLab] cargó beta
[CursoLab] cargó ZetaY todos salen después de los archivos client/ del juego, que cargan primero.
Ejercicio L7.2 · Tu primer módulo
- Crea
shared/CursoLab/Util.luacon las funcionesporcentajeypluralde la sección 4, más una tercera:Util.redondear(x, decimales). - Pídelo con
requiredesde dos archivos distintos declient/, y en cada uno guarda la tabla enCursoLab._utilAyCursoLab._utilB. - En la consola, compara
CursoLab._utilA == CursoLab._utilB. - Comenta el
return Utildel final, reinicia el juego y mira qué pasa.
Listo cuando: puedes explicar por qué el paso 3 da true y qué error ves en el paso 4 (y en qué línea aparece: no es en Util.lua).
Solución
-- shared/CursoLab/Util.lua
local Util = {}
function Util.porcentaje(parte, total)
if total == 0 then return 0 end
return math.floor(parte / total * 100 + 0.5)
end
function Util.plural(n, uno, varios)
if n == 1 then return n .. " " .. uno end
return n .. " " .. varios
end
function Util.redondear(x, decimales)
local m = 10 ^ (decimales or 0)
return math.floor(x * m + 0.5) / m
end
return UtilEl paso 3 da true porque require ejecuta Util.lua una sola vez y entrega la misma tabla a todos los que la piden. Sin el return, require devuelve nil: el error aparece en el archivo de client/ que intenta usar Util.plural(...) (algo como "attempted index: plural of non-table: null"), no en Util.lua, que cargó sin problemas.
Ejercicio L7.3 · Caza de globales
Este archivo funciona, pero tiene cuatro problemas de organización. Encuéntralos y corrígelos.
-- client/CursoLab_Contador.lua
CursoLab = {}
contador = 0
function round(x)
return math.floor(x)
end
function sumar()
contador = contador + 1
getPlayer():Say("Van " .. round(contador / 2))
end
getPlayer():Say("CursoLab listo")
Events.OnKeyPressed.Add(sumar)Listo cuando: tu versión no crea ninguna global salvo CursoLab, no pisa nada del juego y no hace nada en el nivel superior salvo definir y registrar.
Solución
CursoLab = {}borra lo que hayan puesto los archivos que cargaron antes (por ejemploCursoLab_Core.luaenshared/). EsCursoLab = CursoLab or {}.contadorysumarson globales sueltas: pueden chocar con otro mod.function round(x)reemplaza la globalrounddel juego (shared/env.lua) por una que no redondea: rompe todo lo que la usa.getPlayer():Say(...)en el nivel superior se ejecuta al arrancar el juego, cuando no hay jugador: error al cargar.
-- client/CursoLab_Contador.lua
require "CursoLab_Core"
CursoLab = CursoLab or {}
local contador = 0
local function sumar(tecla)
contador = contador + 1
local p = getPlayer()
if p then
p:Say("Van " .. math.floor(contador / 2))
end
end
local function alEmpezar()
getPlayer():Say("CursoLab listo")
end
Events.OnKeyPressed.Add(sumar)
Events.OnGameStart.Add(alEmpezar)Ejercicio L7.4 · El archivo de servidor
Crea server/CursoLab_Server.lua con la guarda if isClient() then return end en la primera línea, y debajo un CursoLab.log("server cargó") y un Events.OnGameStart.Add(...) que también escriba un mensaje.
- Entra a una partida de un jugador. ¿Salen los dos mensajes? ¿Antes o después de los de
client/? - Sin abrir un servidor: si un amigo se conecta a tu servidor dedicado con este mod, ¿qué mensajes salen en su
console.txty cuáles en elserver-console.txt? Escribe tu respuesta con el porqué.
Listo cuando: el paso 1 coincide con lo que predijiste y el paso 2 está justificado con la sección 1.
Solución
- En un jugador
isClient()esfalse, así que el archivo corre entero: salen los dos mensajes.server cargósale al cargar la partida, mucho después de los mensajes declient/, que salieron al arrancar el juego. - El cliente de tu amigo también carga
server/al entrar a la partida, pero ahíisClient()estruey el archivo se corta en la primera línea: en suconsole.txtno sale nada de este archivo. En el servidor dedicado sí corre: los dos mensajes van aserver-console.txt.
Desafío L7
Reorganiza todo lo que llevas de CursoLab con la estructura de la sección 8:
- Una sola global,
CursoLab, creada conCursoLab = CursoLab or {}enshared/CursoLab_Core.lua. - Al menos un módulo en
shared/CursoLab/que se use conrequiredesdeclient/. - Ningún archivo hace nada en el nivel superior salvo
require, definiciones yEvents.X.Add. server/tiene su guarda.
Para comprobar que no se te escapó ninguna global, agrega a CursoLab_Core.lua una función que se ejecute en OnGameStart y recorra _G con pairs, comparando contra una foto de las claves de _G que tomaste al principio de ese archivo (CursoLab_Core.lua carga en shared/, después de todo el shared/ del juego). Que imprima todas las globales nuevas. ¿Cuáles son de tu mod y cuáles del juego o de otros mods? (Pista: las de otros mods y las que crea el juego después de shared/ también van a aparecer; filtra las que se crearon en archivos tuyos buscándolas con rg en tu carpeta.)
Anterior: L06 · Lua y objetos de Java · Siguiente: L08 · Eventos y estado