Objetivo: usar el bytecode del juego como fuente de datos, no solo como último recurso. El módulo 08 te enseñó a leer un método. Este te enseña a preguntarle cosas al .jar entero: valores por defecto, quién llama a quién, qué eventos existen de verdad, qué cambió entre versiones. Todo con ejemplos reales de Build 42 (versión de octubre de 2026).
1. ¿Por qué bytecode si existe el decompilador?
El decompilador te da Java legible, y para entender una función es lo mejor. El bytecode gana en todo lo demás:
| Necesitas… | Decompilador | Bytecode (javap) |
|---|---|---|
| Entender la lógica de un método | mejor | lento |
Un valor exacto (10.0f, 0x2, un texto) | a veces lo reescribe o lo calcula | literal, tal cual |
| Buscar en las 5.095 clases del juego a la vez | lento (horas) | minutos |
Anotaciones (@LuaMethod, @PacketSetting) | a veces las omite | javap -v las muestra todas |
| Saber qué sobrecarga se llama exactamente | puede confundirse | la firma exacta en cada llamada |
| Cuando el decompilador falla o inventa | — | el bytecode no miente: es lo que corre |
La regla práctica: bytecode para buscar y para datos exactos; decompilador para leer la lógica que encontraste.
2. Anatomía de un .class
javap -v muestra todo lo que tiene una clase:
Constant pool: ← todas las constantes: textos, números, nombres de clases y métodos
#921 = String #922 // LevelForDismantleXPCutoff
...
{
public static zombie.characters.IsoPlayer getPlayer(); ← cada método
descriptor: ()Lzombie/characters/IsoPlayer; ← su firma en formato JVM
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic ... ← las instrucciones
LineNumberTable: ... ← línea del .java original
RuntimeVisibleAnnotations: ← anotaciones
se.krka.kahlua.integration.annotations.LuaMethod(
name="getPlayer"
global=true
)
}Tres cosas que vas a usar todo el tiempo:
- El constant pool tiene todos los textos literales de la clase. Por eso buscar un texto en los
.classfunciona (módulo 08, sección 3). - Las anotaciones son metadatos que el motor lee al arrancar. En PZ dicen qué funciones ve Lua (
@LuaMethod) y qué permisos pide cada paquete de red (@PacketSetting). LineNumberTablerelaciona cada instrucción con su línea en el.javaoriginal: un stack trace de Java te da esas líneas.
Construcciones que vas a encontrar
| Ves en el bytecode | Era en Java |
|---|---|
<init> | un constructor |
static {}; | el inicializador estático: aquí viven las constantes y los valores iniciales de los campos estáticos |
Clase$Interna, Clase$1 | clase interna / anónima (LuaManager$GlobalObject, ItemPickerJava$KeyNamer) |
lambda$metodo$0 | una lambda dentro de metodo |
invokedynamic ... makeConcatWithConstants | "texto" + variable |
String.hashCode + lookupswitch + String.equals | un switch sobre un texto |
checkcast / instanceof | un cast / un instanceof |
Ejemplo del switch sobre texto, en ItemPickerJava$KeyNamer.getName:
93: invokevirtual String.hashCode()
96: lookupswitch { // salta según el hash...
-1898802882: 148
-1896111701: 132
2049469: 164
default: 177 }
132: aload 4
134: ldc "Prison" // ...y confirma con equals (dos textos pueden tener el mismo hash)
136: invokevirtual String.equalsLos casos del switch están en los ldc que siguen a cada salto: así sacas la lista completa de valores que acepta.
3. Qué le puedes preguntar al .jar: casos de uso
Cada caso tiene la pregunta, cómo se responde y lo que salió en nuestra versión.
3.1 Valores por defecto y constantes
Pregunta: ¿qué pasa si no pongo distanceMin en un sonido? ¿Cuál es el tope de densidad de zombis en el loot?
Cómo: el constructor (<init>) o el static {} de la clase.
$ javap -p -c zombie.audio.GameSoundClip # constructor
5: fconst_1; 6: putfield volume → 1.0
15: ldc 10.0f; 17: putfield distanceMin → 10.0 ← el caso del Sable de Luz (módulo 16)
21: ldc 10.0f; 23: putfield distanceMax → 10.0
$ javap -p -c zombie.inventory.ItemPickerJava # static {}
10: ldc 8.0f; 13: putstatic zombieDensityCap → 8.0Las opciones de sandbox del juego salen igual. En el constructor de zombie.SandboxOptions hay 183 llamadas newXxxOption (33 booleanas, 50 decimales, 72 de lista, 26 enteras, 2 de texto), cada una con nombre, mínimo, máximo y valor por defecto:
2452: ldc "LevelForDismantleXPCutoff"
2455: iconst_0 ← mínimo 0
2456: bipush 10 ← máximo 10
2458: iconst_0 ← por defecto 0
2459: invokevirtual newIntegerOption(String, int, int, int)Ese 0 por defecto es exactamente por qué "desarmar muebles no da XP" (caso 4 del módulo 16).
3.2 La firma exacta y qué conversión se hace
Pregunta: ¿qué recibe realmente ZombRand(a, b) desde Lua?
Cómo: el método en LuaManager$GlobalObject. Lo viste en el módulo 08: ZombRand(double, double) hace d2i a cada argumento (trunca) y llama RandLua.Next(int, int), que da un número en [a, b). Sin el bytecode no sabrías que ZombRand(1.9, 6.9) es ZombRand(1, 6).
3.3 Condiciones exactas
Pregunta: ¿en qué casos hace algo una función?
Tres respuestas que solo dio el bytecode (módulos 07, 09 y 12):
sendServerCommand:getstatic GameServer.server; ifeq → return. En un jugador no hace nada.IsoGameCharacter.autoDrink: siTriggerHook("AutoDrink")devuelvetrue,return. Registrar unHookapaga la lógica de Java.LuaManager.validateReflectionAccess:getstatic Core.debug; ifne→ si no es debug,"Not in debug".
3.4 Quién llama a quién
Pregunta: ¿desde dónde se dispara un gancho? ¿Quién usa este campo?
$ python3 pzxref.py callers LuaHookManager.TriggerHook
zombie.ai.states.SwipeStatePlayer enter(IsoGameCharacter)
zombie.characters.IsoGameCharacter Hit(HandWeapon, IsoGameCharacter, float, boolean, float, boolean)
zombie.characters.IsoGameCharacter calculateStats()
zombie.characters.IsoGameCharacter autoDrink()
zombie.characters.IsoGameCharacter triggerContextualAction(String) (y 4 sobrecargas más)
zombie.characters.IsoLivingCharacter AttemptAttack(float)
$ python3 pzxref.py fields zombieDensityCap
get ItemPickerJava.getZombieDensityFactor(...)
put ItemPickerJava.static {}El segundo dice que nadie más cambia el tope: no hay opción de sandbox ni función de Lua que lo toque.
3.5 Eventos: dónde se disparan y cuáles no existen de verdad
Esto es probablemente lo más útil para un modder. LuaEventManager.AddEvents registra los eventos (por eso Events.X.Add no da error), pero registrar no es disparar. Cruzando los registros con todas las llamadas a triggerEvent del .jar y del Lua del juego:
$ python3 pzxref.py events OnPlayerGetDamage
BodyDamage.Update() ← enfermedad, hambre...
BodyPart.DamageUpdate() ← sangrado, heridas
Nutrition.setWeight(double)
IsoGameCharacter.handleLandingImpact(...) ← caídas
IsoGameCharacter.FireCheck() ← fuego
IsoGameCharacter.Hit(...) ← golpes
IsoGameCharacter.ReduceHealthWhenBurning()
IsoPlayer.onHitByVehicleApplyDamage(...) ← atropellos
BaseVehicle.damagePlayers(float) ← choquesAhora sabes todas las fuentes de daño que avisan, y puedes mirar en cada una qué argumentos pasa (el tipo de daño).
Y lo contrario: eventos que están registrados pero que ni Java ni el Lua del juego disparan nunca con su nombre (en nuestra versión). Algunos de los que un modder buscaría:
| Evento | Qué pasa |
|---|---|
OnRainStart | nunca se dispara. Su pareja OnRainStop sí (RainManager.stopRaining) |
OnBeingHitByZombie | nunca se dispara: usa OnPlayerGetDamage o OnWeaponHitCharacter |
OnMakeItem | nunca se dispara, aunque un archivo del juego lo escucha |
OnCharacterCreateStats, OnPreGameStart, OnPlayerSetSafehouse, OnAIStateEnter/Execute/Exit | nunca se disparan |
Y dos ganchos de Hook que tampoco: UseItem y WeaponSwingHitPoint (el evento OnWeaponSwingHitPoint sí existe y se dispara en CombatManager.attackCollisionCheck).
Si tu mod escucha uno de estos, nunca va a correr, y no hay ningún error que te avise. El ejercicio 8b.3 te pide comprobar uno en el juego.
3.6 Las claves que acepta un script
Pregunta: ¿qué claves acepta un item? ¿Una clave mal escrita da error?
Cómo: los textos del método que parsea (Item.DoParam(String, String)): 407 textos distintos, comparados con equalsIgnoreCase 440 veces (módulo 07). Una clave que no está en esa lista se ignora en silencio: el bytecode es la única lista completa. Lo mismo vale para mod.info (ChooseGameInfo), los sonidos (GameSoundScript) y cada tipo de script.
3.7 Permisos de red
Pregunta: ¿qué puede mandarle un jugador cualquiera al servidor?
Cómo: cada paquete de zombie.network.packets tiene una anotación @PacketSetting(requiredCapability = ...). Con javap -v sobre las 242 clases de paquetes:
| Permiso pedido | Paquetes |
|---|---|
LoginOnServer (cualquiera que entró) | 204 |
None | 5 |
permisos de admin o debug (ConnectWithDebug, TeleportToPlayer, AnimalCheats, RolesWrite...) | el resto |
Ejemplos: GlobalModDataPacket y AddItemInInventoryPacket piden solo LoginOnServer; SyncPlayerStatsPacket pide CanModifyBodyStats. Es la lista de lo que el servidor acepta de cualquiera, y por qué tu código de servidor tiene que validar (módulo 12).
3.8 Qué cambió entre versiones
Pregunta: el juego se actualizó, ¿qué se rompió?
Cómo: guarda la salida pública de cada versión y compara:
javap -public -cp jar_42.21 $(cat clases_que_uso.txt) > api_42.21.txt
javap -public -cp jar_42.22 $(cat clases_que_uso.txt) > api_42.22.txt
diff api_42.21.txt api_42.22.txt # métodos que desaparecieron o cambiaron de firmaY con dos índices de pzxref (uno por versión) puedes comparar eventos, claves de scripts y valores por defecto. Es la versión automática del módulo 17, sección 3.
4. La herramienta: pzxref.py
Recorre las clases del juego con javap -p -c, guarda en una base SQLite cada texto, cada llamada, cada lectura o escritura de campo y cada evento disparado, y te deja consultarlos:
python3 herramientas/pzxref.py build ~/pz-java/jar # una vez por versión (~1 minuto, ~90 MB)
python3 herramientas/pzxref.py str Rotten.png # quién usa un texto
python3 herramientas/pzxref.py callers ItemPickerJava.fillContainer
python3 herramientas/pzxref.py calls zombie/inventory/ItemPickerJava.getActualSpawnChance
python3 herramientas/pzxref.py fields zombieDensityCap
python3 herramientas/pzxref.py events # todos los eventos y ganchos con su cantidad de lugares
python3 herramientas/pzxref.py events OnFillContainerCon nuestra versión: 5.095 clases, 55.101 textos, 326.047 llamadas y 211 eventos distintos disparados desde Java. Una consulta tarda menos de un segundo.
Cómo reconoce los eventos: en el bytecode, el nombre del evento es el primer argumento de triggerEvent, así que es el último texto conocido (uno de los que registran LuaEventManager y LuaHookManager) antes de la llamada.
Lo que no ve:
- Llamadas por reflexión o nombres armados en tiempo de ejecución (un evento cuyo nombre se construye con
+no aparece). - Lo que pasa en Lua: para eso,
rgsobre_juego/lua. La lista de "eventos que nunca se disparan" de la sección 3.5 cruza las dos cosas. - Las bibliotecas fuera de
zombie/(puedes indexarlas cambiandoprefixenbuild).
5. Qué gana tu mod con esto
Leer el .jar no es un fin. Sirve porque cambia decisiones concretas de tu mod. Esto es lo que te da, con ejemplos que ya salieron en el curso.
5.1 Bugs que no lanzan error
Los peores bugs de un mod son los silenciosos: no hay nada en el log, simplemente no pasa nada. El bytecode los encuentra antes de que un jugador los reporte.
| Situación | Qué te dice el .jar | Qué ganas |
|---|---|---|
Escuchas OnRainStart, OnMakeItem u OnBeingHitByZombie | Nadie los dispara (sección 3.5) | No pierdes una tarde probando: usas otro evento desde el principio |
Registras Hook.UseItem | Java nunca lo llama | Lo mismo, con ganchos |
Agregas loot a la cocina con OnFillContainer | También se dispara desde BaseVehicle.randomizeContainer | Filtras por roomName y tu ítem no aparece en maleteros |
Tu acción con barra no tiene complete | Java marca useCustomRemoteTimedActionSync y la acción corre solo en el cliente | Sabes por qué "en multijugador no guarda" antes de probarlo |
Mandas un objeto en sendClientCommand | TableNetworkUtils solo manda texto, números, booleanos, tablas y unos pocos tipos del juego; lo demás se descarta con un aviso en el log | Mandas ids en vez de objetos |
Rutina: antes de escribir Events.X.Add, corre pzxref events X y rg -n "triggerEvent\(\"X\"" _juego/lua. Si los dos están vacíos, ese evento no existe para ti.
5.2 Números elegidos, no adivinados
| Decisión | De dónde sale el número | Ejemplo |
|---|---|---|
| Peso de loot | La fórmula de doRollItemInternal (módulo 08) | En Tocomples Food, uno de nuestros mods, un peso 50 es un 50 % por tirada: casi seguro en cada refrigerador |
| Rango de una opción de sandbox | Los 183 newXxxOption del constructor de SandboxOptions (ejercicio 8b.1) | Tus rangos se parecen a los del juego y el jugador no se sorprende |
| Condición de un ítem fabricado | RecipeCodeOnCreate.minorCondition | Sabes que nunca baja del 50 % y que con habilidad 10 sale perfecto |
| Duración de una acción | adjustMaxTime | Tu acción se alarga sola con dolor de manos, borrachera o frío: no lo dupliques en tu getDuration |
| Valores al azar | ZombRand(a, b) trunca y devuelve de a a b-1 | Tu ZombRand(1, 6) nunca da 6 |
5.3 Mods que sobreviven a las actualizaciones
Cada parche de PZ puede cambiar firmas de métodos. Con la lista de clases que usa tu mod y un diff del API (sección 3.8, desafío 8b) sabes qué se rompe antes de que los jugadores lo reporten. Ejemplo real: IsoCell.getVehicles() ahora devuelve un Set, y el código que lo recorría como lista falla; getObjectListForLua() es el reemplazo.
Rutina tras cada actualización del juego: copiar el .jar nuevo, pzxref build, diff del API de tus clases, revisar pzxref events contra la versión anterior. Diez minutos en vez de esperar comentarios en Steam.
5.4 Multijugador sin agujeros
El .jar te dice qué controla el servidor y qué no:
- 204 de 242 paquetes solo piden haber iniciado sesión.
ModData.transmitdesde un cliente llega al servidor y el servidor lo reenvía a todos. Cualquier jugador puede mandarte datos falsos: tu servidor valida todo lo que recibe (módulo 12). OnReceiveGlobalModDatano guarda nada solo. Si no lo guardas tú, se pierde.- El
completede una acción corre solo donde!GameClient.client: ahí va lo que cambia el mundo.
5.5 Rendimiento
Saber desde dónde y cuántas veces se dispara un evento te dice cuánto cuesta escucharlo. OnPlayerGetDamage sale de 9 lugares (enfermedad, sangrado, caídas, fuego, golpes, vehículos): si solo te importan los golpes, filtra por tipo en la primera línea. Y el aviso SlowLuaEvents del modo debug salta con callbacks de más de 250 ms: ya sabes dónde mirar.
5.6 Sobrescribir con seguridad
Cuando reemplazas algo del juego (módulo 09), el .jar te dice qué estás apagando. TriggerHook devuelve true apenas hay un callback registrado, y Java se salta su propia lógica: si registras un gancho, eres responsable de todo lo que hacía Java ahí. Leer ese método antes te evita romper una mecánica sin darte cuenta.
5.7 Respuestas con certeza
Cuando alguien comenta en Steam "tu ítem nunca aparece" o "¿esto funciona en multijugador?", puedes responder con el dato y no con una suposición. Y tus descripciones pueden decir cosas exactas ("aparece en refrigeradores con un 4 % por tirada") en vez de "es raro".
Lo que no te da
- No es para parchear el juego. Los mods del Workshop son Lua, scripts y recursos. El
.jares para saber, no para cambiar. - No reemplaza probar. Te da la hipótesis correcta; el juego la confirma (paso 5 y 7 del método del módulo 15).
- Vale para tu versión. Cada dato es de la versión que indexaste. Vuelve a comprobarlo después de una actualización.
Ejercicios
Ejercicio 8b.1 · Las opciones de sandbox del juego
Escribe un script que lea el bytecode del constructor de zombie.SandboxOptions y genere una tabla con cada opción: nombre, tipo, mínimo, máximo y valor por defecto. Pista: para cada invokevirtual newXxxOption, los argumentos son las constantes que se empujaron justo antes (ldc, iconst_N, bipush, sipush, ldc2_w para los double).
Listo cuando: tu tabla tiene 183 filas y LevelForDismantleXPCutoff sale con 0, 10 y 0.
Ejercicio 8b.2 · Un switch de texto
En ItemPickerJava$KeyNamer.getName, saca los tres textos del switch y explica qué hace el método con cada uno. Después decompila la clase y compara.
Ejercicio 8b.3 · Un evento que no existe
- Registra en CursoLab un
Events.OnRainStart.Add(...)y unEvents.OnRainStop.Add(...)que impriman en la consola. - Usa el menú de debug para hacer llover y parar la lluvia.
- ¿Cuál se imprime? Compáralo con lo que dice
pzxref.py events OnRainStart. - Diseña cómo detectar el inicio de la lluvia sin ese evento (pista: busca con
pzxref strqué clase maneja la lluvia y qué expone a Lua).
Ejercicio 8b.4 · Todas las fuentes de daño
Con pzxref.py events OnPlayerGetDamage, abre cada uno de los 9 lugares y anota qué argumentos pasa cada uno al evento (el tipo de daño suele ser un texto: búscalo con javap -c justo antes de la llamada). Con eso, escribe un OnPlayerGetDamage en CursoLab que anote en la bitácora (6.2) el tipo y la cantidad de cada daño.
Ejercicio 8b.5 · Los permisos de red
Saca la tabla completa de paquetes y permisos (sección 3.7). Elige tres paquetes con LoginOnServer que te parezcan delicados y busca en su método de proceso del lado servidor qué validaciones hace el juego.
Ejercicio 8b.6 · Auditoría de un mod tuyo
Elige un mod tuyo publicado y revisa, con pzxref y rg:
- Cada
Events.X.AddyHook.X.Add: ¿ese evento se dispara de verdad? ¿desde cuántos lugares? - Cada peso de loot: ¿qué probabilidad por tirada da con la fórmula?
- Cada
sendClientCommand/sendServerCommand: ¿los valores que mandas sobreviven aTableNetworkUtils?
Listo cuando: tienes una lista de hallazgos con la evidencia de cada uno (clase.método o archivo y línea) y arreglaste al menos uno.
Desafío 8b
Extiende pzxref.py con un comando diff <base1.db> <base2.db> que liste, entre dos versiones del juego: eventos nuevos o que dejaron de dispararse, textos nuevos en Item.DoParam (claves de script nuevas) y métodos de LuaManager$GlobalObject que aparecieron o desaparecieron. Guarda la base de tu versión actual para usarla con el próximo parche.
Anterior: 08 · Leer el código de Java · Siguiente: 09 · Sobrescribir comportamiento