Una pequeña introducción al ROM hacking

Hoy compartiré una forma sencilla de modificar el comportamiento de juegos para Game Boy Advance. Y otra para casos más complejos usando una API de un emulador. Para este tutorial usaré “Pokemon Mistery Dungeon: Red Rescue Team” (la versión americana) disponible para descargar aquí. Como emulador usaré mgba.

Índice

Conceptos básicos

El término ROM literalmente significa ‘Read-Only Memory’, un tipo de memoria no modificable (como la BIOS). En lo referente a juegos de consolas viene siendo más bien el cartucho, que en estos componentes viejos venía conteniendo los datos del juego solo para lectura. Por supuesto una ROM virtual para ser ejecuada en emulador es perfectamente modificable.

Game Boy Advance es una consola más vieja que yo, con dos características clave: retrocompatibilidad y un procesador armv7. La segunda es muy útil porque ARM es una arquitectura bien conocida que está presente en casi todos los dispositivos móviles y en buena parte de dispositivos embebidos así que es más fácil para alguien con antecedentes en estas plataformas. Una buena introducción a ARM puede encontrarse en Azeria Labs. Aunque es cierto que para los reemplazos más sencillos los emuladores como mgba tienen un escáner de memoria para hacerte la vida más fácil, aprender la arquitectura y el conjunto de instrucciones es necesario para luego realizar un análisis más detallado.

Partes de la memoria

La disposición general de la memoria en el Game Boy Advance es la siguiente:

  • System ROM: Contiene la BIOS. Ocupa 16kb en el rango de direcciones 0x0 - 0x00003FFF.
  • External Work RAM: RAM para los datos y código del juego. En mgba esta etiquetada como “Working RAM”. Ocupa 256kb y su espacio de direcciones es 0x02000000 - 0x0203FFFF. La memoria se refleja (su información se encuentra redundante) cada 0x40000 bytes hasta 0x02FFFFFF.
  • Internal Work RAM: Es la memoria más rápida. Ocupa 32kb en el rango de direcciones 0x03000000 - 0x03FFFFFF. Puede contener la pila, código y datos. Se refleja cada 0x8000 bytes hasta 0x03FFFFFF.
  • IO RAM: Usada para controlar gráficos, sonidos, DMA (un circuito para copiar datos sin usar la CPU), interrupciones del mando, etc. Ocupa 1kb en el espacio de direcciones 0x04000000 - 0x040003FF. En mgba esta etiquetada como “Memory-Mapped I/O”.
  • Palette RAM: Especifica valores de 16 bits para los modos de paletas. 0x05000000 es para fondos y 0x05000200 es para sprites. Ocupa 1kb y se encuentra en el rango de direcciones 0x05000000 - 0x050003FF. La memoria se refleja cada 0x400 bytes hasta 0x5FFFFFF. Las paletas de colores se usan para ahorrar memoria.
  • Video RAM: Es usada para almacenar el búfer de fotogramas o “frame buffer” en modo mapa de bits, y los datos de las tejas o “tiles” en texto basado en tiles y modos de rotación/escalado. Ocupa 96kb, en el rango 0x06000000 - 0x06017FFF.
  • OAM: Es la Object Attribute Memory y es usada para controlar los sprites en GBA. Ocupa 1kb en el rango 0x07000000 - 0x070003FF. La memoria se refleja cada 0x400 bytes hasta 0x07FFFFFF.
  • Game Pack ROM: Contiene la ROM del cartucho. Si un cartucho está presente al iniciar la consola, el registro a instrucción apuntará a 0x08000000 y comenzará la ejecución desde allí. Comienza en esa misma dirección y ocupa hasta 32mb. Contiene 2 copias que se usan para dar distintos tiempos de acceso a chips de la ROM.
  • Game Pack ROM Image 1: Comienza en 0x0A000000. Copia de la Game Pack ROM.
  • Game Pack ROM Image 2: Comienza en 0x0C000000. Copia de la Game Pack ROM.
  • Cart RAM: Suele ocupar entre 32 y 64kb, usada principalmente para guardar partidas. Puede ser SRAM(Static RAM, requiere batería de respaldo) o Flash ROM. Comienza en 0x0E000000 pero según la docummentación también podría aparecer a partir de 0x0F000000.
  • EEPROM: Otro tipo de Cart RAM, similar a la Flash ROM. Los tipos de Cart RAM son unificados por el emulador en una “Flash Memory” de 128kb, que se encarga de mapear los archivos de salvas.

Cheats en GBA

Buscar y cambiar valores en memoria

Para cargar una ROM clickear en el menú File/Load ROM…

Dejaré la salva para la demostración por aquí . Para cargarla con mgba clickear en File/Load state file…
Pulsando ‘z’ y ‘x’ podemos interactuar con el banquero, depositar y sacar dinero del banco. Para buscar valores en memoria vamos a Tools/Game state views/Search memory… La interfaz es bastante intuitiva, introducimos la cantidad actual en la casilla a la derecha de “Value” y presionamos “New Search”.

Si depositamos por ejemplo, 100 PoKe, y usamos “Search Within” reducimos la cantidad de direcciones de memoria que pueden contener las monedas que tenemos a mano.

Lo hacemos varias veces hasta que acabamos con solo una:

Ahora si buscamos ese valor en memoria con “Open in Memory Viewer” e introducimos la dirección podemos ver su valor:

Observamos 0x6400 en hexadecimal. Este valor, dada la arquitectura que usa está en little-endian: los bytes menos significativos van en las direcciones de memoria menores, así 0x64 en 0x2038c08 y 0x00 en 0x2038c09 en orden seria 0x00064, que equivale a 100 decimal. Para modificarlo agrupamos a 2 bytes y cambiamos su valor unsigned. Antes de eso, pausamos el juego (Ctrl + P):

Aquí lo modifiqué a 65535, el máximo valor posible para un entero sin signo de dos bytes. Para ver los cambios reanudamos el juego, cerramos y abrimos el menú de nuevo (el juego no dibuja el dinero en pantalla al momento del cambio).

Aclarar quiero que el valor del dinero usa en realidad 4 bytes, la herramienta lo redujo a 2 en la búsqueda porque los bytes más significativos no estaban activos. Y además por algún motivo es un valor con signo, por lo que el valor más alto en realidad alcanzable vendría siendo en little-endian 0xFFFFFF7F (para conservar el bit más significativo como 0), lo que equivale a 2147483647 PoKe.

Este valor sin embargo no es conservado, la lógica del juego inmediatamente lo reduce a 99999 luego de una operación.
Vale resaltar que habrá casos donde no podamos encontrar la dirección correcta por medio del buscador de memoria, como enumeraciones que usen valores desconocidos. Igual en estos casos podemos observar la memoria cerca de las direcciones que contienen valores que conocemos del juego.

Tipos de cheats

Los dispositivos de trampa o “cheating devices”, también conocidos como dispositivos de alteración del juego, se utilizaban frecuentemente para hacer trampa en los videojuegos. Existen diversos dispositivos comerciales que se anunciaron con este fin. Una forma de usar un dispositivo de trampa es insertando un cartucho de juego dentro de otro cartucho (llamado cartucho de trampa) y luego insertando este último en la consola para activar códigos una vez encendida. Para GBA se produjeron varios, siendo los más relevantes Action Replay, Gameshark Advance y Codebreaker Advance / Gameshark SP.
Los códigos usados por cada dispositivo son diferentes, y deben encriptarse antes de usarse (Action Replay y Gameshark Advance con ARCrypt y Codebreaker Advance con CBA Crypt). Algunos juegos requieren también un “Master Code” para activar ciertos trucos. La lista de los códigos desencriptados o RAW y demás información se puede encontrar aquí.
En el caso de mgba, el emulador que estamos usando, esta demostrado que soporta los siguientes tipos:

  • GameShark Advance (encriptados)
  • Codebreaker (desencriptados)
  • Action Replay (códigos MAX)

Personalmente puedo decir que algunos códigos de Codebreaker como el “slide code” no funcionarán.

Un cheat de Codebreaker

Usaremos un cheat de Codebreaker RAW para obtener dinero infinito. Usaremos el código para escribir 16 bits en la RAM: 8aaaaaaa yyyy, donde”aaaaaaa” es la dirección y “yyyy” es el valor. Entonces quedaría 82038c08 ffff para escribir 65535 de manera constante en esa dirección. Vamos a Tools/Cheats…, introducimos el valor y le damos a “Add Lines”.

Podemos exportar nuestros cheats a un archivo .cheats y cargarlo luego usando “Save” o directamente escribirlo a mano:

1
2
# Oro en mano infinito
82038c08 ffff

Scripts con Lua

Análisis inicial y el depurador

Hay veces que quieres hacer algo más complejo que simplemente editar valores en memoria. Por ejemplo, a mí se me ocurrió hacer que el jugador pueda caminar sobre cualquier tile. Este hack solo es aplicable en las zonas de combate del juego. Dejo una salva de esta por aquí.

Nota: Esto me tomó bastante tiempo debido a que no soy muy entendido en el desarrollo de esta clase de juegos e intenté varias vías distintas, tales como localizar las coordenadas del jugador o la presión de las teclas de movimiento. Obtuve información que eventualmente me ayudó en el resultado final.

Comencé viendo que en la ROM cada Pokemon tiene una estructura con cierta información, y entre esta información están los tipos de tiles que puede caminar:


Toda la información está disponible en datacrystal

Bien, pues mgba cuenta con un depurador, lo abrimos con “Tools/Open debugger console”. Escribimos “help” y vemos los comandos disponibles (si alguien ha usado el depurador gdb antes esta sintaxis le resultará bastante cómoda). Hay una serie de comandos llamado “watch…” que nos permite detener la ejecución del programa cuando el programa accede a una dirección de memoria. La idea es usar “watch/r” para detener la ejecución en la instrucción justo después de que se en la dirección que contiene las tiles en las que puede caminar nuestro pokemon.

Mi inicial en este juego es Squirtle, en la Pokedex es el número 7. Ese número es el índice en la estructura del pokemon correspondiente en la ROM.

Sabemos que la ROM se mapea comenzando por la dirección 0x08000000, y que la estructura está a un desplazamiento u “offset” de 0x357b98 + sizeof(struct) * idx bytes, y que el campo que buscamos está a 0x15 bytes del inicio de la estructura. Cada estructura ocupa 0x48 bytes, entonces nuestro objetivo es:

1
2
>>> hex(0x08000000 + 0x357b98 + (0x48*7) + 0x15)
'0x8357da5'


Vemos que es 0x03, un pokemon de agua puede caminar en “Ground” y “Water”. Si queremos verificar que en realidad es la correcta podemos ver que a 4 bytes de offset en la estructura hay un puntero a una cadena con la descripción y nombre del pokemon.

Finalmente creamos el watchpoint con watch/r 0x8357da5 (antes de eso escribir stack trace-only para poder ver que funciones fueron llamadas antes de ese punto de ejecución). Luego le damos continue y al mover el personaje debería activarse el watchpoint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> stack trace-only
r0: 00000031 r1: 08000000 r2: 000014BD r3: 09EC95C8
r4: 02039F60 r5: 0203A7C4 r6: 0201FDFF r7: 0201FDFE
r8: 0000004C r9: 00000000 r10: 002A0000 r11: 002A0000
r12: 000001CE r13: 03007B40 r14: 00000000 r15: 030001F0
cpsr: 2000001F [--C----]
Cycle: 79960233985
030001EC: E0D300D1 ldrsb r0, [r3], #1
> c
Hit watchpoint 2 at 0x08357DA5: (value = 0x00000005)
r0: 00000005 r1: 08357D90 r2: 08357B98 r3: 00000000
r4: 02017310 r5: 00000000 r6: 00000000 r7: 00000000
r8: 02004190 r9: 00000000 r10: 02017310 r11: 00000000
r12: 000001CE r13: 03007BB0 r14: 08070333 r15: 0808DB28
cpsr: 0000003F [------T]
Cycle: 79981910980
0808DB26: 4770 bx lr
> bt
#0 0x0807032A (00000007 00000002 000018FC 00000000 02017310 00000000 00000000 00000000 02004190 00000000 02017310 00000000 000001CE 03007BB4 0805E0CD 08070D7A cpsr: 0000003F)
at 0x08070D78 [0x08070D6E+10]
#1 0x08070D6E (02017310 00000000 000018FC 00000000 00000000 00000000 00000000 00000000 02004190 00000000 02017310 00000000 000001CE 03007BC4 0805EC8B 0805E0CA cpsr: 4000003F)
at 0x0805E0C8

Nota: Ahora sí es importante saber lo básico de ARM

Análisis estático con Ghidra

Aunque es posible lograr lo que queremos solo con el depurador es verdad que es bastante tedioso. Para seguir más cómodamente el flujo de instrucciones previas usé Ghidra con el plugin Ghidra-GBA. No voy a entrar en detalles de como se usa la herramienta en este tutorial ya que se alargaría demasiado, para aprender lo básico recomiendo este curso. Dicho esto, cargamos el juego en Ghidra y esperamos a que analice todo (puede tomar un rato). Cuando termine vamos a la dirección que nos devolvió el watchpoint y revisamos el decompilado:

1
2
3
4
5
// Es buena práctica cambiar los nombres de las funciones para reconocerlas más fácilmente.
undefined FUN_0808db14_FIND_POKE_WALKABLE(sh ort param_1)
{
return *(undefined *)(param_1 * 0x48 + DAT_0202f3 e0 + 0x15);
}

Esta función hace lo mismo que hicimos nosotros para acceder al campo de tiles caminables. Vamos a la parte de la función que invocó a esta, según el volcado de la pila con bt en el depurador viene siendo 0x0807032A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

undefined8 FUN_08070328(short param_1)

{
uint uVar1;
undefined4 in_lr;

uVar1 = FUN_0808db14_FIND_POKE_WALKABLE((int) param_1);
uVar1 = uVar1 & 0xff;
if (3 < uVar1) {
uVar1 = (uint)(byte)(&DAT_0202f314)[uVar1];
}
return CONCAT44(in_lr,uVar1);
}

Nada relevante todavía, seguimos hacia 0x08070D6E:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

undefined8 FUN_08070d6c_CAN_WALK(int param_1,ui nt param_2)

{
char cVar1;
uint walkable_tile_type;
ushort *puVar2;
int iVar3;
undefined4 uVar4;
undefined4 in_lr;

walkable_tile_type = FUN_08070328((int)*(short *)(*( int *)(param_1 + 0x70) + 2));
walkable_tile_type = walkable_tile_type & 0xff;
puVar2 = (ushort *)
FUN_0804954c((int)*(short *)(param_1 + 4) + (i nt)(short)(&DAT_080f4448)[param_2 * 2],
(int)*(short *)(param_1 + 6) + (int)(short) (&DAT_080f444a)[param_2 * 2]);
if (((*puVar2 & 0x10) == 0) && (*(int *)(puVar2 + 8) = = 0)) {
cVar1 = FUN_080441e8();
if (cVar1 == '\0') {
if ((*(char *)(*(int *)(param_1 + 0x70) + 0xe4) == '\ x03') ||
(cVar1 = FUN_08046cb0(param_1,9), cVar1 != '\0' )) {
walkable_tile_type = 3;
}
else {
cVar1 = FUN_080718d8(param_1,0xc);
if ((cVar1 != '\0') ||
((cVar1 = FUN_080718d8(param_1,0xd), cVar1 ! = '\0' &&
(walkable_tile_type = 3, (param_2 & 1) != 0)))) {
walkable_tile_type = 2;
}
}
}
iVar3 = FUN_0804954c((int)*(short *)(param_1 + 4), (int)*(short *)(param_1 + 6));
if (((&DAT_08106fad)[param_2 & 7] & *(byte *)(iVar 3 + 10 + walkable_tile_type)) != 0) {
uVar4 = 1;
goto LAB_08070e36;
}
}
uVar4 = 0;
LAB_08070e36:
return CONCAT44(in_lr,uVar4);
}

Sé que pinta feo pero no necesitamos entender todo esto, recordemos que la función que verifica si la tile a donde nos queremos mover debe devolver un valor booleano que debe ser comparado en algún lugar, y ese lugar es este:

1
2
3
4
5
6
7
8
9
10
11
    iVar3 = FUN_0804954c((int)*(short *)(param_1 + 4), (int)*(short *)(param_1 + 6));
if (((&DAT_08106fad)[param_2 & 7] & *(byte *)(iVar 3 + 10 + walkable_tile_type)) != 0) {
// Se puede caminar
uVar4 = 1;
goto LAB_08070e36;
}
}
// No se puede caminar
uVar4 = 0;
LAB_08070e36:
return CONCAT44(in_lr,uVar4);

En el desensamblado luce así:

1
2
08070e26  00 28                          cmp                   walkable_tile_type,#0x0
08070e28 04 d0 beq LAB_08070e34

Si vemos esto en el depurador, confirmamos que el if termina siendo cierto si r0 no es 0 en ese momento:

1
2
3
4
> dis 0x8070e26
08070E26: 2800 cmp r0, #0
> dis 0x8070e28
08070E28: D004 beq 0x08070E34

Deshabilitamos el watchpoint y habilitamos un breakpoint en esta dirección:

1
2
3
4
5
> listw
E 2: 0x8357DA5
> disable 2
> b 0x8070e26
Added breakpoint #3

Ahora nos movemos hacia una pared e intentamos avanzar hacia ella. El breakpoint saltará dos veces para el jugador y una vez extra para cada entidad que esté en pantalla e intente moverse y no sea el jugador (esto descubierto a base de prueba y error). Luego usamos w/r r0 1 en ambos para hacer pasar la condición por verdadera y así movernos a las rocas:

Perfecto! Pero el personaje ha sido teletransportado. Esto último ocurre de manera legítima en algunos momentos del juego, como cuando deciden salir de una zona o dos unidades aliadas intercambian posición y uno queda sobre una tile donde no tiene permitido estar.

Cuando es teletransportado se ve el mensaje “ warped!”, en Ghidra podemos buscar las referencias a esta cadena a ver en que función se utiliza.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
undefined4 FUN_0807d148_WARPED_FUN(undefined 4 param_1,int param_2,int param_3,undefined4 *para m_4)

{
char cVar1;
int iVar2;
undefined4 *puVar3;
int iVar4;
byte bVar5;
undefined4 in_lr;
undefined4 uStack_2c;
undefined4 *puStack_28;
int iStack_24;

iVar2 = *(int *)(param_2 + 0x70);
iStack_24 = 0;
puStack_28 = param_4;
FUN_08045b94(&DAT_0202df98,param_2,0);
cVar1 = FUN_08071824(param_2,0xe);
if (cVar1 != '\0') {
puVar3 = &DAT_080fcae8;
LAB_0807d194:
FUN_080522f4_Print_Log(param_1,param_2,*puVar 3);
/* WARNING: Read-only address (ram,0x02 03b418) is written */
return in_lr;
}
cVar1 = FUN_080441e8();
if (cVar1 != '\0') {
puVar3 = &DAT_080fc97c;
goto LAB_0807d194;
}
if ((param_3 == 1) && (*(int *)(DAT_0203b418 + 0xe2 1c) == *(int *)(param_2 + 4))) {
FUN_080522f4_Print_Log(param_1,param_2,"It~27s already on the stairs.");
FUN_08076d10(param_1,param_2);
return in_lr;
}
FUN_080522f4_Print_Log(param_1,param_2,"$m0 w arped!");
FUN_0807a96c(param_2,param_2);
FUN_080421ac(param_1,param_2);
cVar1 = FUN_08045888(param_2);
if (cVar1 != '\0') {
bVar5 = *(byte *)(iVar2 + 0x46);
iVar4 = *(int *)(param_2 + 0x1c) + 0x800;
*(int *)(param_2 + 0x1c) = iVar4;
while (iVar4 < 0xa000) {
if ((DAT_0202edcc & 3) == 0) {
bVar5 = bVar5 + 1 & 7;
*(byte *)(iVar2 + 0x46) = bVar5;
FUN_0806ce68(param_2,bVar5);
}
FUN_0803e46c(0x22);
iVar4 = *(int *)(param_2 + 0x1c) + 0x800;
*(int *)(param_2 + 0x1c) = iVar4;
}
}
if (param_3 == 1) {
cVar1 = FUN_0808384c(&uStack_2c,DAT_0203b418 + 0xe21c);
if (cVar1 == '\0') {
uStack_2c = *(undefined4 *)(param_2 + 4);
iStack_24 = 1;
}
goto LAB_0807d2ce;
}
if (param_3 != 0) {
if (param_3 == 2) {
cVar1 = FUN_0808384c(&uStack_2c,puStack_28);
if (cVar1 == '\0') {
uStack_2c = *(undefined4 *)(param_2 + 4);
iStack_24 = 1;
}
goto LAB_0807d2ce;
}
if (param_3 == 3) {
uStack_2c = *puStack_28;
goto LAB_0807d2ce;
}
}
cVar1 = FUN_08083660(&uStack_2c);
if (cVar1 == '\0') {
uStack_2c = *(undefined4 *)(param_2 + 4);
iStack_24 = 1;
}
LAB_0807d2ce:
FUN_080694c0(param_2,(int)(short)uStack_2c,(int)uS tack_2c._2_2_,1);
FUN_0804535c(param_2,0);
FUN_0807bb78(param_2);
FUN_0803f580(1);
cVar1 = FUN_08045888(param_2);
if (cVar1 != '\0') {
bVar5 = *(byte *)(iVar2 + 0x46);
*(undefined4 *)(param_2 + 0x1c) = 0x9c00;
do {
if ((DAT_0202edcc & 3) == 0) {
bVar5 = bVar5 + 1 & 7;
*(byte *)(iVar2 + 0x46) = bVar5;
FUN_0806ce68(param_2,bVar5);
}
FUN_0803e46c(0x22);
iVar4 = *(int *)(param_2 + 0x1c) + -0x400;
*(int *)(param_2 + 0x1c) = iVar4;
} while (0 < iVar4);
}
*(undefined4 *)(param_2 + 0x1c) = 0;
FUN_0803e46c(0x22);
if (iStack_24 != 0) {
FUN_080522f4_Print_Log(param_1,param_2,"But it dropped back at the same spot!");
}
if (param_3 == 1) {
FUN_08076d10(param_1,param_2);
}
if (*(char *)(iVar2 + 7) != '\0') {
FUN_0804ac20(param_2 + 4);
*(undefined *)(DAT_0203b418 + 1) = 0;
*(undefined4 *)(DAT_0203b418 + 0x5c0) = 0xffffffff;
FUN_0807ec28(0);
}
FUN_0806a5b8(param_2);
FUN_08075900(param_2,*(undefined *)(DAT_0203b 418 + 0x3a08));
return in_lr;
}

Si ponemos un breakpoint en esta función y vemos el backtrace con bt llegamos a que es usada en 0x080755FA:

1
2
3
4
5
FUN_0806a5b8(iVar9);
cVar4 = FUN_080706a4(iVar9,iVar9 + 4);
if (cVar4 != '\0') {
FUN_0807d148_WARPED_FUN(iVar9,iVar9, 0,0);
}

Nuevamente vemos que hay una verificación, esta vez de que r0 sea distinto de 0 para que se ejecute la función que teletransporta:

1
2
3
4
5
6
7
8
080755ee  00 28                       cmp                   r0,#0x0
080755f0 05 d0 beq LAB_080755fe
080755f2 20 1c add r0,r4,#0x0
080755f4 21 1c add r1,r4,#0x0
080755f6 00 22 mov r2,#0x0
080755f8 00 23 mov r3,#0x0
080755fa 07 f0 a5 fd bl FUN_0807d148_WARPED_FUN undefined FUN_0807d148_WARPED_FUN()

Concatenamos ambas escrituras, w/r r0 1 para caminar sobre los muros y w/r r0 0 para evadir la teletransporación.

Automatización

Nota: Las funcionalidades usadas de la API en este script específico son exclusivas de la versión en desarrollo de mgba, asegurarse que se descarga una de las versiones bajo el apartado "Development Downloads"

Para programar scripts en mgba se usa Lua como lenguaje de programación, está bien documentado online. La API experimental de mgba se encuentra aquí, no trae ejemplos, pero es lo que tenemos. Para empezar tenemos un objeto emu, que es una instancia de CoreAdapter, que es la clase con la funcionalidad que necesitamos. emu:setBreakpoint nos permite poner un breakpoint y asignarle un callback (esto nos permite en la práctica “hookear” la función); con emu:readRegister y emu:writeRegister podemos leer y escribir registros respectivamente. Una primera versión del script podría ser así:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
local function walk_over()
local r0 = emu:readRegister("r0")
if r0 == 0 then
console:log("Walking over the walls...")
emu:writeRegister("r0", 1)
end
end

local function disable_warp()
local r0 = emu:readRegister("r0")
if r0 ~= 0 then
console:log("Avoiding warp...")
emu:writeRegister("r0", 0)
end
end

-- breakpoint para caminar sobre cualquier tile
emu:setBreakpoint(walk_over,0x8070e26)
-- breakpoint para deshabilitar el intento de teletransporte
emu:setBreakpoint(disable_warp,0x80755ee)

Vamos a “Tools/Scripting…” e insertamos el script:

Nada mal pero aún hay dos problemas:

  • El jugador acaba con hambre muuuy rápido (supongo que es porque en el juego pasar por una escalera para descender un nivel quite un porcentaje mayor del porcentaje de llenura)
  • Intentar golpear en esta zona también te teletransporta

El primero se puede resolver manteniendo al máximo ese valor. Hay dos formas:

  • Usando callbacks:add("frame", function)*
  • Usando un cheat

Opté por lo segundo, ese valor se almacena en 0x20042cc:

El segundo requiere (una vez más) poner un breakpoint en la función que imprime el mensaje cuando el personaje se teletransporta e identificar la condicional en dicha función. Al final solo añadimos otro breakpoint con el mismo callback que teníamos para eso:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local function walk_over()
local r0 = emu:readRegister("r0")
if r0 == 0 then
console:log("Walking over the walls...")
emu:writeRegister("r0", 1)
end
end

local function disable_warp()
local r0 = emu:readRegister("r0")
if r0 ~= 0 then
console:log("Avoiding warp...")
emu:writeRegister("r0", 0)
end
end

emu:setBreakpoint(walk_over,0x8070e26)
emu:setBreakpoint(disable_warp,0x80755ee)
emu:setBreakpoint(disable_warp,0x80732b8)

Enlaces

Roms and isos, https://wiki.recalbox.com/en/tutorials/games/generalities/isos-and-roms#dat-files-1
GBA Memory, https://gbadev.net/gbadoc/memory.html
Sprite (videojuegos), https://es.wikipedia.org/wiki/Sprite_(videojuegos)
Cheating Device, http://www.starfywiki.org/wiki/Cheating_device
Hacking GBA, https://doc.kodewerx.org/hacking_gba.html#gsa
mGBA cheat code compatibility, https://forums.libretro.com/t/mgba-cheat-code-compatibility/34713
Codebreaker Advance slide codes not working, are discarded from cheat list, https://github.com/mgba-emu/mgba/issues/2283
Internal Data for Pokémon Mystery Dungeon: Red Rescue Team, https://datacrystal.tcrf.net/wiki/Pok%C3%A9mon_Mystery_Dungeon:_Red_Rescue_Team
Azeria Labs, https://azeria-labs.com
Ghidra Software Reverse Engineering Framework, https://github.com/NationalSecurityAgency/ghidra/
GBA ROM loader for ghidra, https://github.com/SiD3W4y/GhidraGBA/
Course materials for hackaday.io Ghidra training, https://github.com/wrongbaud/hackaday-u/
Programming in Lua , https://www.lua.org/pil/
Callback (computer programming), https://en.wikipedia.org/wiki/Callback_(computer_programming)