Upgrade to Pro — share decks privately, control downloads, hide ads and more …

O dia que fiz engenharia reversa num jogo de Gameboy Advance

O dia que fiz engenharia reversa num jogo de Gameboy Advance

O Gameboy Advance foi um dos videogames mais populares de seu tempo, e com isso, muitas comunidades surgiram para estudar e documentar sua arquitetura, desenvolver ROM hacking, assim como ferramentas próprias para o GBA.

Então, que tal explorarmos engenharia reversa na prática com o seguinte desafio: desenvolver um editor de fases para um jogo de GBA, o "Klonoa: Empire of Dreams"? Esse é um desafio bem interessante, pois precisaremos entender a arquitetura de um hardware em ARM, aplicar engenharia reversa para descobrir a lógica do jogo, escrever patches para a ROM, e enfim usar todas as nossas descobertas para construir um completo editor de fases.

Veremos o passo a passo da engenharia reversa e o desenvolvimento da ferramenta nessa talk.

Repositório do projeto: https://github.com/macabeus/klo-gba.js
Manual sobre o desenvolvimento do projeto:
https://medium.com/@bruno.macabeus/pt-br-engenharia-reversa-num-jogo-de-gameboy-advance-introdu%C3%A7%C3%A3o-21ebffe2f794

Bruno Macabeus

October 22, 2022
Tweet

More Decks by Bruno Macabeus

Other Decks in Programming

Transcript

  1. Desafios de engenharia reversa: Esticar a ponte Entendendo o DMA

    Entendendo a física do jogo Encontrar o tilemap na ROM Pintar o tilemap Projeto Tools
  2. Desafios de engenharia reversa: Esticar a ponte Entendendo o DMA

    Entendendo a física do jogo Encontrar o tilemap na ROM Pintar o tilemap Projeto Tools Live demo!
  3. Desafios de engenharia reversa: Esticar a ponte Entendendo o DMA

    Entendendo a física do jogo Encontrar o tilemap na ROM Pintar o tilemap Projeto Tools Desafios na implementação: WebAssembly Salvando o novo tilemap na ROM Live demo!
  4. Desafios de engenharia reversa: Esticar a ponte Entendendo o DMA

    Entendendo a física do jogo Encontrar o tilemap na ROM Pintar o tilemap Projeto Tools Desafios na implementação: WebAssembly Salvando o novo tilemap na ROM Resultados Live demo!
  5. Esse é o tile ID. Nesse exemplo, os tiles com

    ID 9B são do canto esquerdo da ponte. 9A é o ID da parte contínua da ponte.
  6. A VRAM carrega o timemap contendo apenas o que o

    jogador está vendo. Assim, existe uma outra região da memória que contém todo o conteúdo, sendo ela a fonte dos dados
  7. É um recurso do sistema do computador que permite que

    determinados subsistemas de hardware acessem a memória principal do sistema (memória de acesso aleatório), independente da CPU https://bit.ly/gba-manual-tilemap
  8. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 84000048 O byte mais próximo da ponte (0600F1AD).
  9. Em outras palavras, a VRAM é atualizada aqui DMA3: 03000900

    0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0 0600F000 80000200 DMA3: 03004800 07000000 84000048
  10. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 84000048 Esse byte é o data source da VRAM. Está localizada na seção Fast WRAM.
  11. OAM

  12. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 8400003E Podemos ver exatamente o byte que escreve no objeto do Klonoa (07000000).
  13. DMA3: 03000900 0600E000 80000400 DMA3: 03001100 0600E800 80000400 DMA3: 03004DB0

    0600F000 80000200 DMA3: 03004800 07000000 8400003E Podemos ver exatamente o byte que escreve no objeto do Klonoa (07000000).
  14. …e executando um frame, nota-se que atualizou a posição Y

    do Klonoa! Mas também retorna no byte o valor correto…
  15. Assim, comecei a fazer a fazer debug step by step

    no No$GBA para ver como e onde os bytes 03002922:03002923 são atualizados. E encontrei duas instruções importantes:
  16. Assim, comecei a fazer a fazer debug step by step

    no No$GBA para ver como e onde os bytes 03002922:03002923 são atualizados. E encontrei duas instruções importantes: Instrução em 0800FF16 sempre incrementa o Y
  17. Assim, comecei a fazer a fazer debug step by step

    no No$GBA para ver como e onde os bytes 03002922:03002923 são atualizados. E encontrei duas instruções importantes: Instrução em 0800FF16 sempre incrementa o Y Instrução em 0801200E corrige o Y SE o Klonoa estiver no chão
  18. ?

  19. ? ?

  20. !

  21. vamos encontrar o tilemap na rom O DIA QUE FIZ

    ENGENHARIA REVERSA NUM JOGO DE GBA
  22. >

  23. BIOS é um firmware que inicializa os componentes físicos do

    sistema No GBA, a BIOS expõe muitas funções comum em jogos, incluindo de compressão e descompressão de dados
  24. BIOS é um firmware que inicializa os componentes físicos do

    sistema No GBA, a BIOS expõe muitas funções comum em jogos, incluindo de compressão e descompressão de dados Cada função tem um código numérico associado, que deve ser usado junto da instrução SWI
  25. Como a divisão funciona no GBA: swi 0x06 Input: R0

    
 numerador R1 
 denominador
  26. Como a divisão funciona no GBA: swi 0x06 Input: R0

    
 numerador R1 
 denominador R0 
 numerador / denominador R1 
 numerador % denominador R3 
 abs(numerador / denominador) Output:
  27. r0: endereço do tilemap na ROM r1: endereço do output

    é o endereço de input do LZ77 Huffman
  28. 6E 6E 6E B0 B1 BD 77 AB B3 AB

    B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E
  29. 6E 6E 6E B0 B1 BD 77 AB B3 AB

    B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E 6E 6E 6E B0 B1 BD 77 AB B3 AB B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E
  30. 6E 6E 6E B0 B1 BD 77 AB B3 AB

    B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E 6E 6E 6E B0 B1 BD 77 AB B3 AB B3 7C 6E 6E 6E 6E 6E 6E 6E 6E 7C 6E 6E 6E 6E 7C 6E 6E =
  31. 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x1A 0x1B 0x1C 0x1D

    0x1E 0x1F 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
  32. 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x1A 0x1B 0x1C 0x1D

    0x1E 0x1F 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
  33. x x x x a x x x x 


    x x x x b x x x x Estou no tile a Abaixo de mim está o tile b
  34. x x x x a x x x x 


    x x x x b x x x x Do tile a para b tem 9 bytes (= 9 tiles), então a largura do tilemap é exatamente 9 Estou no tile a
  35. Quando altero esse byte… …isso se reflete aqui, e esse

    é o tile a,… …que está em 02008439
  36. const Jimp = require('jimp') const { drop } = require('ramda')

    const fs = require('fs') const data = drop(3, fs.readFileSync(‘dump/level-1/tilemap'))
  37. const Jimp = require('jimp') const { drop } = require('ramda')

    const fs = require('fs') const data = drop(3, fs.readFileSync('dump/level-1/tilemap'))
  38. const getPixelColor = hexTile = > { if (hexTile ===

    0) { return 0x000000 } return 0xFFFFFF }
  39. new Jimp(300, 600, (err, image) = > { let x

    = 0 let y = 0 for (let i = 0; i < data.length; i += 1) { const value = data[i] x += 1 if (x === 420) { y += 1 x = 0 } let color = getPixelColor(value) image.setPixelColor(color, x, y) } image.write('image.png') })
  40. Tilemap comprimido > Antigo código em C para descomprimir Tilemap

    descomprimido > Não da para rodar código em C no browser!
  41. Emscripten WebAssembly > > Tilemap comprimido > Antigo código em

    C para descomprimir Tilemap descomprimido >
  42. Algumas mudanças para tornar o antigo código em C compatível

    com o Emscripten: Remover o main e prints
  43. Algumas mudanças para tornar o antigo código em C compatível

    com o Emscripten: Remover o main e prints Adicionar a flag EMSCRIPTEN_KEEPALIVE nas funções de encode e decode não serem removidas
  44. Substituir a chamada de filelength Algumas mudanças para tornar o

    antigo código em C compatível com o Emscripten: Remover o main e prints Adicionar a flag EMSCRIPTEN_KEEPALIVE nas funções de encode e decode não serem removidas
  45. const lzssModule = require('./wasm/lzss.js') const { FS } = lzssModule

    const lzssDecode = (buffer) => { FS.writeFile('filelzss', buffer) lzssModule._LZS_Decode() try { return lzssModule.FS.readFile('filelzss', { encoding: 'binary' }) } catch (e) { throw new LzssDecodeError(e) } }
  46. const lzssModule = require('./wasm/lzss.js') const { FS } = lzssModule

    const lzssDecode = (buffer) => { FS.writeFile('filelzss', buffer) lzssModule._LZS_Decode() try { return lzssModule.FS.readFile('filelzss', { encoding: 'binary' }) } catch (e) { throw new LzssDecodeError(e) } }
  47. const lzssModule = require('./wasm/lzss.js') const { FS } = lzssModule

    const lzssDecode = (buffer) => { FS.writeFile('filelzss', buffer) lzssModule._LZS_Decode() try { return lzssModule.FS.readFile('filelzss', { encoding: 'binary' }) } catch (e) { throw new LzssDecodeError(e) } }
  48. const lzssModule = require('./wasm/lzss.js') const { FS } = lzssModule

    const lzssDecode = (buffer) => { FS.writeFile('filelzss', buffer) lzssModule._LZS_Decode() try { return lzssModule.FS.readFile('filelzss', { encoding: 'binary' }) } catch (e) { throw new LzssDecodeError(e) } }
  49. const lzssModule = require('./wasm/lzss.js') const { FS } = lzssModule

    const lzssDecode = (buffer) => { FS.writeFile('filelzss', buffer) lzssModule._LZS_Decode() try { return lzssModule.FS.readFile('filelzss', { encoding: 'binary' }) } catch (e) { throw new LzssDecodeError(e) } }
  50. const lzssModule = require('./wasm/lzss.js') const { FS } = lzssModule

    const lzssDecode = (buffer) => { FS.writeFile('filelzss', buffer) lzssModule._LZS_Decode() try { return lzssModule.FS.readFile('filelzss', { encoding: 'binary' }) } catch (e) { throw new LzssDecodeError(e) } }
  51. function docker_run_emscripten { local filename="$1" echo "Compiling $filename..." docker run

    \ --rm -it \ -v $(pwd)/scissors/src/wasm:/src \ trzeci/emscripten:1.38.43 \ emcc -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 \ -s EXTRA_EXPORTED_RUNTIME_METHODS=[\”FS\"] \ -s EXPORT_NAME=\"$filename\" -o ./$filename.js $filename.c } docker_run_emscripten huffman docker_run_emscripten lzss
  52. function docker_run_emscripten { local filename="$1" echo "Compiling $filename..." docker run

    \ --rm -it \ -v $(pwd)/scissors/src/wasm:/src \ trzeci/emscripten:1.38.43 \ emcc -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 \ -s EXTRA_EXPORTED_RUNTIME_METHODS=[\”FS\"] \ -s EXPORT_NAME=\"$filename\" -o ./$filename.js $filename.c } docker_run_emscripten huffman docker_run_emscripten lzss
  53. O switch entre ARM e Thumb é feito com a

    instrução bx. Ela apenas ler de um registrador
  54. O switch entre ARM e Thumb é feito com a

    instrução bx. Ela apenas ler de um registrador bx atualiza o PC com o valor do registrador usado como parâmetro
  55. Very big hole space at the end of cartridge First

    Level Second Level Third Level Instruction to load a level
  56. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole
  57. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole 0836720 FC 27 1B 08 0836724 00 77 36 08 0836728 5C 3E 1B 08 083672C B0 86 36 08 0836730 AC 50 1B 08 0836734 80 93 36 08
  58. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole 0836720 FC 27 1B 08 0836724 00 77 36 08 0836728 5C 3E 1B 08 083672C B0 86 36 08 0836730 AC 50 1B 08 0836734 80 93 36 08
  59. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole 0836720 FC 27 1B 08 0836724 00 77 36 08 0836728 5C 3E 1B 08 083672C B0 86 36 08 0836730 AC 50 1B 08 0836734 80 93 36 08
  60. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole 0836720 FC 27 1B 08 0836724 00 77 36 08 0836728 5C 3E 1B 08 083672C B0 86 36 08 0836730 AC 50 1B 08 0836734 80 93 36 08
  61. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole
  62. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole Original Loader 08043B0A mov r4, r0 08043B0C add r0, r5, 4 08043B0E mov r1, r4 08043B10 bl 0805143Ch
  63. First Level Second Level Third Level Instruction to load a

    level (patched) Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole Original Loader 08043B0A mov r4, r0 08043B0C add r0, r5, 4 08043B0E mov r1, r4 08043B10 bl 0805143Ch Patched Loader 08043B0A mov r4, r0 08043B0C bl 08367610h 080v3B10 bl 0805143Ch
  64. First Level Second Level Third Level Instruction to load a

    level (patched) Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole Patched Loader 08043B0A mov r4, r0 08043B0C bl 08367610h 080v3B10 bl 0805143Ch R0 é o ponteiro de qual tilemap será carregado. Devemos atualizá-lo com o endereço do level customizado
  65. First Level Second Level Third Level Instruction to load a

    level (patched) Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole Patched Loader 08043B0A mov r4, r0 08043B0C bl 08367610h 080v3B10 bl 0805143Ch
  66. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole 08367610 mov r0, r15 ; r15 = PC
 08367612 add r0, 3Ch
 08367614 bx r0
  67. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole 08367650 add r0, r4, 4h 08367654 ldr r4, [r15, #-3Ch] 08367658 cmp r0, r4 0836765C ldreq r0, [r15, #-40h] 08367660 ldr r4, [r15, #-40h] 08367664 cmp r0, r4 08367668 ldreq r0, [r15, #-44h] 0836767C ldr r4, [r15, #-44h] 08367680 cmp r0, r4 08367684 ldreq r0, [r15, #-48h] 08367688 mov r4, r1 0836768C bx r14
  68. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole 08367650 add r0, r4, 4h 08367654 ldr r4, [r15, #-3Ch] 08367658 cmp r0, r4 0836765C ldreq r0, [r15, #-40h] 08367660 ldr r4, [r15, #-40h] 08367664 cmp r0, r4 08367668 ldreq r0, [r15, #-44h] 0836767C ldr r4, [r15, #-44h] 08367680 cmp r0, r4 08367684 ldreq r0, [r15, #-48h] 08367688 mov r4, r1 0836768C bx r14
  69. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole 08367650 add r0, r4, 4h 08367654 ldr r4, [r15, #-3Ch] 08367658 cmp r0, r4 0836765C ldreq r0, [r15, #-40h] 08367660 ldr r4, [r15, #-40h] 08367664 cmp r0, r4 08367668 ldreq r0, [r15, #-44h] 0836767C ldr r4, [r15, #-44h] 08367680 cmp r0, r4 08367684 ldreq r0, [r15, #-48h] 08367688 mov r4, r1 0836768C bx r14
  70. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole 08367650 add r0, r4, 4h 08367654 ldr r4, [r15, #-3Ch] 08367658 cmp r0, r4 0836765C ldreq r0, [r15, #-40h] 08367660 ldr r4, [r15, #-40h] 08367664 cmp r0, r4 08367668 ldreq r0, [r15, #-44h] 0836767C ldr r4, [r15, #-44h] 08367680 cmp r0, r4 08367684 ldreq r0, [r15, #-48h] 08367688 mov r4, r1 0836768C bx r14
  71. First Level Second Level Third Level Instruction to load a

    level Constant addresses map table Hole Customised level loader Customised First Level Customised Second Level Customised Third Level Hole Hole Hole 08367650 add r0, r4, 4h 08367654 ldr r4, [r15, #-3Ch] 08367658 cmp r0, r4 0836765C ldreq r0, [r15, #-40h] 08367660 ldr r4, [r15, #-40h] 08367664 cmp r0, r4 08367668 ldreq r0, [r15, #-44h] 0836767C ldr r4, [r15, #-44h] 08367680 cmp r0, r4 08367684 ldreq r0, [r15, #-48h] 08367688 mov r4, r1 0836768C bx r14
  72. const setPatchCustomVisionLoader = (romBuffer) => { const visionsWithCustomTilemap = allVisions.map(visionInfo

    => visionHasCustomTilemap(romBuffer, visionInfo)) const addresses = allVisions.map(visionInfo => ({ custom: mapAddressToRomOffset(visionInfo.rom.customTilemap), original: mapAddressToRomOffset(visionInfo.rom.tilemap), })) // set bl to go to our patch romBuffer.set([0x23, 0xF3, 0x80, 0xFD], 0x43B0C) // bl 08367610h // switch to arm mode and run our code romBuffer.set([0x78, 0x46], 0x367610) // mov r0,r15 romBuffer.set([0x3C, 0x30], 0x367612) // add r0, 3Ch romBuffer.set([0x00, 0x47], 0x367614) // bx r0 // write original and custom address of each vision addresses.forEach(({ custom, original }, index) => { const offset = 0x367620 + (index * 8) setConstant(romBuffer, offset, original) setConstant(romBuffer, offset + 4, custom) }) // change value at R0 to custom address if need romBuffer.set([0x04, 0x00, 0x85, 0xE2], 0x367650) // add r0, r5, 4h addresses.forEach((_, index) => { if (visionsWithCustomTilemap[index]) { const offset1 = 0x3C + ((index * 12) - (index * 8)) const offset2 = 0x40 + ((index * 12) - (index * 8)) romBuffer.set([offset1, 0x40, 0x1F, 0xE5], 0x367654 + (index * 12)) // ldr r4,[r15,#-offset1] romBuffer.set([0x04, 0x00, 0x50, 0xE1], 0x367658 + (index * 12)) // cmp r0, r4 romBuffer.set([offset2, 0x00, 0x1F, 0x05], 0x36765C + (index * 12)) // ldreq r0,[r15,#-offset2] } }) romBuffer.set([0x01, 0x40, 0xA0, 0xE1], 0x367660 + (addresses.length * 12)) // mov r4, r1 romBuffer.set([0x1E, 0xFF, 0x2F, 0xE1], 0x367664 + (addresses.length * 12)) // bx r14 } 1. Redirecionar para o patched level loader 2. Escrever a tabela 3. Escrever a lógica que compara o R0 com os valores da tabela e atualizá- lo se der match 4. Volta ao level loader original
  73. 1. Redirecionar para o patched level loader 2. Escrever a

    tabela 3. Escrever a lógica que compara o R0 com os valores da tabela e atualizá- lo se der match 4. Volta ao level loader original const setPatchCustomVisionLoader = (romBuffer) => { const visionsWithCustomTilemap = allVisions.map(visionInfo => visionHasCustomTilemap(romBuffer, visionInfo)) const addresses = allVisions.map(visionInfo => ({ custom: mapAddressToRomOffset(visionInfo.rom.customTilemap), original: mapAddressToRomOffset(visionInfo.rom.tilemap), })) // set bl to go to our patch romBuffer.set([0x23, 0xF3, 0x80, 0xFD], 0x43B0C) // bl 08367610h // switch to arm mode and run our code romBuffer.set([0x78, 0x46], 0x367610) // mov r0,r15 romBuffer.set([0x3C, 0x30], 0x367612) // add r0, 3Ch romBuffer.set([0x00, 0x47], 0x367614) // bx r0 // write original and custom address of each vision addresses.forEach(({ custom, original }, index) => { const offset = 0x367620 + (index * 8) setConstant(romBuffer, offset, original) setConstant(romBuffer, offset + 4, custom) }) // change value at R0 to custom address if need romBuffer.set([0x04, 0x00, 0x85, 0xE2], 0x367650) // add r0, r5, 4h addresses.forEach((_, index) => { if (visionsWithCustomTilemap[index]) { const offset1 = 0x3C + ((index * 12) - (index * 8)) const offset2 = 0x40 + ((index * 12) - (index * 8)) romBuffer.set([offset1, 0x40, 0x1F, 0xE5], 0x367654 + (index * 12)) // ldr r4,[r15,#-offset1] romBuffer.set([0x04, 0x00, 0x50, 0xE1], 0x367658 + (index * 12)) // cmp r0, r4 romBuffer.set([offset2, 0x00, 0x1F, 0x05], 0x36765C + (index * 12)) // ldreq r0,[r15,#-offset2] } }) romBuffer.set([0x01, 0x40, 0xA0, 0xE1], 0x367660 + (addresses.length * 12)) // mov r4, r1 romBuffer.set([0x1E, 0xFF, 0x2F, 0xE1], 0x367664 + (addresses.length * 12)) // bx r14 }
  74. const setPatchCustomVisionLoader = (romBuffer) => { const visionsWithCustomTilemap = allVisions.map(visionInfo

    => visionHasCustomTilemap(romBuffer, visionInfo)) const addresses = allVisions.map(visionInfo => ({ custom: mapAddressToRomOffset(visionInfo.rom.customTilemap), original: mapAddressToRomOffset(visionInfo.rom.tilemap), })) // set bl to go to our patch romBuffer.set([0x23, 0xF3, 0x80, 0xFD], 0x43B0C) // bl 08367610h // switch to arm mode and run our code romBuffer.set([0x78, 0x46], 0x367610) // mov r0,r15 romBuffer.set([0x3C, 0x30], 0x367612) // add r0, 3Ch romBuffer.set([0x00, 0x47], 0x367614) // bx r0 // write original and custom address of each vision addresses.forEach(({ custom, original }, index) => { const offset = 0x367620 + (index * 8) setConstant(romBuffer, offset, original) setConstant(romBuffer, offset + 4, custom) }) // change value at R0 to custom address if need romBuffer.set([0x04, 0x00, 0x85, 0xE2], 0x367650) // add r0, r5, 4h addresses.forEach((_, index) => { if (visionsWithCustomTilemap[index]) { const offset1 = 0x3C + ((index * 12) - (index * 8)) const offset2 = 0x40 + ((index * 12) - (index * 8)) romBuffer.set([offset1, 0x40, 0x1F, 0xE5], 0x367654 + (index * 12)) // ldr r4,[r15,#-offset1] romBuffer.set([0x04, 0x00, 0x50, 0xE1], 0x367658 + (index * 12)) // cmp r0, r4 romBuffer.set([offset2, 0x00, 0x1F, 0x05], 0x36765C + (index * 12)) // ldreq r0,[r15,#-offset2] } }) romBuffer.set([0x01, 0x40, 0xA0, 0xE1], 0x367660 + (addresses.length * 12)) // mov r4, r1 romBuffer.set([0x1E, 0xFF, 0x2F, 0xE1], 0x367664 + (addresses.length * 12)) // bx r14 } 1. Redirecionar para o patched level loader 2. Escrever a tabela 3. Escrever a lógica que compara o R0 com os valores da tabela e atualizá- lo se der match 4. Volta ao level loader original
  75. const setPatchCustomVisionLoader = (romBuffer) => { const visionsWithCustomTilemap = allVisions.map(visionInfo

    => visionHasCustomTilemap(romBuffer, visionInfo)) const addresses = allVisions.map(visionInfo => ({ custom: mapAddressToRomOffset(visionInfo.rom.customTilemap), original: mapAddressToRomOffset(visionInfo.rom.tilemap), })) // set bl to go to our patch romBuffer.set([0x23, 0xF3, 0x80, 0xFD], 0x43B0C) // bl 08367610h // switch to arm mode and run our code romBuffer.set([0x78, 0x46], 0x367610) // mov r0,r15 romBuffer.set([0x3C, 0x30], 0x367612) // add r0, 3Ch romBuffer.set([0x00, 0x47], 0x367614) // bx r0 // write original and custom address of each vision addresses.forEach(({ custom, original }, index) => { const offset = 0x367620 + (index * 8) setConstant(romBuffer, offset, original) setConstant(romBuffer, offset + 4, custom) }) // change value at R0 to custom address if need romBuffer.set([0x04, 0x00, 0x85, 0xE2], 0x367650) // add r0, r5, 4h addresses.forEach((_, index) => { if (visionsWithCustomTilemap[index]) { const offset1 = 0x3C + ((index * 12) - (index * 8)) const offset2 = 0x40 + ((index * 12) - (index * 8)) romBuffer.set([offset1, 0x40, 0x1F, 0xE5], 0x367654 + (index * 12)) // ldr r4,[r15,#-offset1] romBuffer.set([0x04, 0x00, 0x50, 0xE1], 0x367658 + (index * 12)) // cmp r0, r4 romBuffer.set([offset2, 0x00, 0x1F, 0x05], 0x36765C + (index * 12)) // ldreq r0,[r15,#-offset2] } }) romBuffer.set([0x01, 0x40, 0xA0, 0xE1], 0x367660 + (addresses.length * 12)) // mov r4, r1 romBuffer.set([0x1E, 0xFF, 0x2F, 0xE1], 0x367664 + (addresses.length * 12)) // bx r14 } 1. Redirecionar para o patched level loader 2. Escrever a tabela 3. Escrever a lógica que compara o R0 com os valores da tabela e atualizá- lo se der match 4. Volta ao level loader original
  76. const setPatchCustomVisionLoader = (romBuffer) => { const visionsWithCustomTilemap = allVisions.map(visionInfo

    => visionHasCustomTilemap(romBuffer, visionInfo)) const addresses = allVisions.map(visionInfo => ({ custom: mapAddressToRomOffset(visionInfo.rom.customTilemap), original: mapAddressToRomOffset(visionInfo.rom.tilemap), })) // set bl to go to our patch romBuffer.set([0x23, 0xF3, 0x80, 0xFD], 0x43B0C) // bl 08367610h // switch to arm mode and run our code romBuffer.set([0x78, 0x46], 0x367610) // mov r0,r15 romBuffer.set([0x3C, 0x30], 0x367612) // add r0, 3Ch romBuffer.set([0x00, 0x47], 0x367614) // bx r0 // write original and custom address of each vision addresses.forEach(({ custom, original }, index) => { const offset = 0x367620 + (index * 8) setConstant(romBuffer, offset, original) setConstant(romBuffer, offset + 4, custom) }) // change value at R0 to custom address if need romBuffer.set([0x04, 0x00, 0x85, 0xE2], 0x367650) // add r0, r5, 4h addresses.forEach((_, index) => { if (visionsWithCustomTilemap[index]) { const offset1 = 0x3C + ((index * 12) - (index * 8)) const offset2 = 0x40 + ((index * 12) - (index * 8)) romBuffer.set([offset1, 0x40, 0x1F, 0xE5], 0x367654 + (index * 12)) // ldr r4,[r15,#-offset1] romBuffer.set([0x04, 0x00, 0x50, 0xE1], 0x367658 + (index * 12)) // cmp r0, r4 romBuffer.set([offset2, 0x00, 0x1F, 0x05], 0x36765C + (index * 12)) // ldreq r0,[r15,#-offset2] } }) romBuffer.set([0x01, 0x40, 0xA0, 0xE1], 0x367660 + (addresses.length * 12)) // mov r4, r1 romBuffer.set([0x1E, 0xFF, 0x2F, 0xE1], 0x367664 + (addresses.length * 12)) // bx r14 } 1. Redirecionar para o patched level loader 2. Escrever a tabela 3. Escrever a lógica que compara o R0 com os valores da tabela e atualizá- lo se der match 4. Volta ao level loader original
  77. const setPatchCustomVisionLoader = (romBuffer) => { const visionsWithCustomTilemap = allVisions.map(visionInfo

    => visionHasCustomTilemap(romBuffer, visionInfo)) const addresses = allVisions.map(visionInfo => ({ custom: mapAddressToRomOffset(visionInfo.rom.customTilemap), original: mapAddressToRomOffset(visionInfo.rom.tilemap), })) // set bl to go to our patch romBuffer.set([0x23, 0xF3, 0x80, 0xFD], 0x43B0C) // bl 08367610h // switch to arm mode and run our code romBuffer.set([0x78, 0x46], 0x367610) // mov r0,r15 romBuffer.set([0x3C, 0x30], 0x367612) // add r0, 3Ch romBuffer.set([0x00, 0x47], 0x367614) // bx r0 // write original and custom address of each vision addresses.forEach(({ custom, original }, index) => { const offset = 0x367620 + (index * 8) setConstant(romBuffer, offset, original) setConstant(romBuffer, offset + 4, custom) }) // change value at R0 to custom address if need romBuffer.set([0x04, 0x00, 0x85, 0xE2], 0x367650) // add r0, r5, 4h addresses.forEach((_, index) => { if (visionsWithCustomTilemap[index]) { const offset1 = 0x3C + ((index * 12) - (index * 8)) const offset2 = 0x40 + ((index * 12) - (index * 8)) romBuffer.set([offset1, 0x40, 0x1F, 0xE5], 0x367654 + (index * 12)) // ldr r4,[r15,#-offset1] romBuffer.set([0x04, 0x00, 0x50, 0xE1], 0x367658 + (index * 12)) // cmp r0, r4 romBuffer.set([offset2, 0x00, 0x1F, 0x05], 0x36765C + (index * 12)) // ldreq r0,[r15,#-offset2] } }) romBuffer.set([0x01, 0x40, 0xA0, 0xE1], 0x367660 + (addresses.length * 12)) // mov r4, r1 romBuffer.set([0x1E, 0xFF, 0x2F, 0xE1], 0x367664 + (addresses.length * 12)) // bx r14 } 1. Redirecionar para o patched level loader 2. Escrever a tabela 3. Escrever a lógica que compara o R0 com os valores da tabela e atualizá- lo se der match 4. Volta ao level loader original