WENOPAL

07 · La frontera Lua ↔ Java

Cómo llega un objeto Java a Lua, qué puedes hacer con él y cómo averiguar la API real del motor con javap, sin depender de wikis.

Curso técnico · Build 42 · Actualizado el

Objetivo: entender cómo un objeto Java llega a Lua, qué puedes hacer con él y qué no, y cómo averiguar la API real del motor sin depender de wikis. Este es el módulo que más te va a servir cuando "no hay documentación".

cp ~/Library/Application\ Support/Steam/steamapps/common/ProjectZomboid/Project\ Zomboid.app/Contents/Java/projectzomboid.jar ~/Zomboid/Workshop/_juego/

1. El modelo mental

┌──────────── JVM (projectzomboid.jar) ────────────┐
│  IsoPlayer, IsoZombie, InventoryItem, ...         │
│  LuaManager  ── expone clases y funciones ──┐     │
│                                              ▼     │
│  Kahlua (intérprete Lua escrito en Java)           │
│     tu Lua  ──  llama métodos por reflexión  ──►  │
└────────────────────────────────────────────────────┘
  • Kahlua es Java. Un objeto Java no se "copia" a Lua: Lua recibe una referencia y Kahlua traduce cada llamada obj:metodo(args) a una llamada Java por reflexión.
  • Solo es visible lo que el motor expone. Lo hace zombie.Lua.LuaManager$Exposer: su método exposeAll() llama a setExposed(Clase) unas 1.000 veces, una por cada clase que Lua puede usar. Lo que no está en esa lista no existe para Lua, aunque esté en el .jar (por ejemplo GameClient y GameServer, las clases de red, no están).
  • Las funciones globales (getPlayer, getText, instanceof...) son métodos estáticos de zombie.Lua.LuaManager$GlobalObject marcados con la anotación @LuaMethod(name = "...", global = true). Hay 759.

2. Lo que puedes hacer con un objeto Java desde Lua

Quieres…Se escribeEjemplo real del juego
Llamar un métodoobj:metodo(args) (con dos puntos)getPlayer():getInventory()
Crear un objetoClase.new(args)ArrayList.new() (53 usos), IsoThumpable.new(...) (38), ColorInfo.new() (100)
Llamar un método estáticoClase.metodo(args)ScriptManager.instance:getItem("Base.Apple") (campo estático instance + método)
Usar un enumEnum.VALORItemTag.MAGNIFIER, CharacterTrait.ILLITERATE, Keyboard.KEY_F11
Saber la claseinstanceof(obj, "IsoZombie")221 usos. En Java es GlobalObject.instof: busca la clase expuesta con ese nombre y usa Class.isInstance, así que también responde true con una clase padre (instanceof(zombi, "IsoGameCharacter"))
Guardar datos propios en el objetoobj:getModData() → tabla Luamódulo 14

Y lo que no puedes:

  • Leer o escribir campos (obj.x): Kahlua solo expone métodos. Si un dato no tiene getter, no se lee desde Lua. La única puerta es la reflexión que usa el visor de objetos (getNumClassFields, getClassField, getClassFieldVal), y está cerrada fuera de debug: getClassFieldVal llama primero a LuaManager.validateReflectionAccess, que lanza IllegalStateException("Not in debug") si el juego no está en modo debug. Sirve para inspeccionar, nunca para un mod publicado.
  • Agregar campos a un objeto Java (getPlayer().miCosa = 1 da error): para eso está getModData().
  • Heredar de una clase Java desde Lua. Lo que el juego deja extender (UI, acciones) está escrito en Lua justamente por eso.

3. Traducción de tipos

JavaEn LuaOjo con
int, float, double, longnumber (double)Al llamar un método que pide int, el convertidor (zombie.Lua.KahluaNumberConverter) usa Double.intValue(): 3.7 llega como 3 y -3.7 como -3 (trunca, no redondea)
booleanboolean
Stringstring
nullnilUn método que devuelve null te da nil: revisa antes de encadenar (getPlayer():getVehicle():getSpeed() revienta si no estás en un auto)
ArrayList, Listobjeto JavaÍndices desde 0: list:get(0); tamaño list:size() (1.206 usos)
KahluaTabletableEs la tabla Lua vista desde Java: así llegan las tablas que pasas a métodos que las aceptan

El bucle que vas a escribir mil veces

lua
local items = getPlayer():getInventory():getItems()   -- ArrayList de Java
for i = 0, items:size() - 1 do                         -- desde 0 hasta size-1
    local it = items:get(i)
    print(it:getFullType())
end

El juego tiene 565 :get(i) y 328 :get(i-1): los segundos recorren for i = 1, list:size() y restan 1. Ambos están bien; mezclar los dos es el bug.

Sobrecarga de métodos

Java permite varios métodos con el mismo nombre y distintos parámetros (AddItem(String), AddItem(InventoryItem)). Kahlua elige según la cantidad y el tipo de los argumentos que pasas. Si llamas con un tipo que no calza con ninguno, el error es confuso ("no such method" aunque el método exista): revisa los tipos.

4. Cómo averiguar la API real (el método)

De más rápido a más profundo:

  1. ¿Lo usa el Lua del juego? rg -n "getVehicle\(\)" _juego/lua — si el juego lo usa, existe y ves cómo se llama. Es la fuente más rápida y la más confiable para la forma de uso.
  2. ¿Qué tiene este objeto? En la consola, con el inspector del ejercicio 6 o el visor de objetos de debug (client/DebugUIs/ObjectViewer.lua), mira un objeto vivo.
  3. ¿Qué métodos tiene la clase? javap sobre la clase en el .jar (sección 5). Da la lista exacta de métodos públicos con sus tipos.
  4. ¿Qué hace el método? Decompilarlo (módulo 08).

En nuestros mods usamos este mismo método, con una regla que conviene adoptar: cuando algo se contradice, manda lo que dice el juego instalado.

5. javap en tres comandos

sh
mkdir -p /tmp/pzjar && cd /tmp/pzjar && unzip -oq ~/Zomboid/Workshop/_juego/projectzomboid.jar
javap -cp /tmp/pzjar -public zombie.characters.IsoPlayer | less          # métodos públicos y firmas
javap -cp /tmp/pzjar -p -c zombie.scripting.objects.Item \
  | awk '/public void DoParam\(java.lang.String, java.lang.String\)/,/^$/' \
  | grep -oE '// String [A-Za-z]+' | sort -u                               # las claves que acepta el parser de ítems

El segundo truco es oro: el parser de cada tipo de script es una clase Java, y sus cadenas literales son exactamente las claves que reconoce. En Build 42 (versión de octubre de 2026), cada línea Clave = valor de un ítem pasa por Item.DoParam(String, String), que tiene 407 cadenas distintas y compara con equalsIgnoreCase 440 veces: por eso las claves de los ítems no distinguen mayúsculas. (En una versión anterior contamos 404 en Item.Load: el código cambia entre parches, y por eso se verifica en el .jar que tienes.)


Ejercicios

Ejercicio 7.1 · Encadenar sin reventar

Escribe CursoLab.velocidad() que devuelva la velocidad del vehículo del jugador o 0 si va a pie, sin que nunca lance error. Pruébala a pie, de copiloto y manejando.

Ejercicio 7.2 · Recorrer colecciones de Java

Escribe CursoLab.inventarioPorCategoria() que devuelva una tabla Lua { Food = 3, Tool = 5, ... } con el inventario del jugador, incluyendo lo que va dentro de mochilas (pista: un ítem contenedor tiene su propio getInventory(); busca cómo recorre el juego inventarios anidados en luautils.countItemsRecursive).

Listo cuando: el conteo coincide con lo que ves en la interfaz, con y sin mochila.

Ejercicio 7.3 · Leer la API con javap

Con el .jar descomprimido:

  1. Lista los métodos públicos de IsoPlayer que empiezan con get y devuelven boolean. ¿Cuántos son?
  2. Busca un método de IsoGameCharacter que diga si el personaje está en el interior. Pruébalo en la consola.
  3. Saca la lista de claves que acepta Item.DoParam y compárala con las claves que usaste en el módulo 02. ¿Hay alguna que usaste y no esté? (Si la hay, el juego la ignora en silencio.)

Ejercicio 7.4 · ¿Qué está expuesto?

Encuentra en el .jar la clase que registra las clases visibles para Lua (pista: está en el paquete zombie.Lua; busca setExposed o exposeAll).

  1. Saca la lista completa de clases expuestas a notas/expuestas.txt (pista: en exposeAll, cada ldc ... // class seguido de setExposed). ¿Está IsoZombie? ¿Y VehicleManager? Comprueba con la lista que GameClient no está.
  2. Elige una clase no expuesta y comprueba en la consola que Lua no la ve.

Listo cuando: puedes responder con evidencia "¿puedo usar la clase X desde Lua?" para cualquier X.

Ejercicio 7.5 · Las funciones globales

Las funciones como getPlayer() están en una clase de zombie.Lua marcadas con una anotación. Encuéntrala, saca la lista completa de funciones globales y guárdala en notas/globales.txt. Marca 10 que no conocías y prueba 3.

Desafío 7

Escribe pzapi.py: recibe un nombre de clase (IsoPlayer) y genera un archivo de tipos para el Lua Language Server (anotaciones ---@class y ---@field/---@return) a partir de javap. Ponlo en .vscode y comprueba que VS Code autocompleta getPlayer(): con métodos reales.


Anterior: 06 · Lua en PZ · Siguiente: 08 · Leer el código de Java