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

Retro Game Decompilation Using AI

Retro Game Decompilation Using AI

Let’s explore matching decompilation in games: the process of rewriting high-level code that, when compiled, produces a binary identical to the original one.
And more: we’ll see how LLMs can help on this amazing challenge!

Rewriting accurately a classic game’s code helps preserve its legacy and paves the way to deeply understand the game and make modifications that create new experiences.

Did you like the topic and want to dive deeper? Check out my posts and my VS Code extension:

📝 Posts explaining step by step the project’s development: https://gambiconf.substack.com/p/development-journey-on-game-decompilation
🧩 VS Code Extension: https://github.com/macabeus/kappa

Meetup page: https://www.meetup.com/leiria-tech-talks/events/310954373

Avatar for Bruno Macabeus

Bruno Macabeus

September 16, 2025
Tweet

More Decks by Bruno Macabeus

Other Decks in Programming

Transcript

  1. preamble For a long time, I loved to have fun

    working on reverse engineering games Reverse engineering games was always a playtime for me.
 Ragnarok Online was my fi rst sandbox.
  2. preamble For a long time, I loved to have fun

    working on reverse engineering games Reverse engineering games was always a playtime for me.
 Ragnarok Online was my fi rst sandbox.
  3. preamble What is matching decompilation? How is the work fl

    ow to decompile a game like Super Mario 64? Can AI be useful on matching decompilation?
  4. preamble What is matching decompilation? How is the work fl

    ow to decompile a game like Super Mario 64? Can AI be useful on matching decompilation?
  5. preamble What is matching decompilation? How is the work fl

    ow to decompile a game like Super Mario 64? Can AI be useful on matching decompilation?
  6. Disassembly A disassembler is a tool that does the reverse

    process of an assembler. That is, it converts a binary back into assembly.
  7. Disassembly 0x23 0xF3 0x80 0xFD 0x78 0x46 0x3C 0x30 0x00

    0x47 A disassembler is a tool that does the reverse process of an assembler. That is, it converts a binary back into assembly.
  8. Disassembly 0x23 0xF3 0x80 0xFD 0x78 0x46 0x3C 0x30 0x00

    0x47 Disassembly ARMv4T
 (Thumb instructions) A disassembler is a tool that does the reverse process of an assembler. That is, it converts a binary back into assembly.
  9. Disassembly 0x23 0xF3 0x80 0xFD 0x78 0x46 0x3C 0x30 0x00

    0x47 bl 08367610h mov r0, r15 add r0, 3Ch bx r0 Disassembly ARMv4T
 (Thumb instructions) A disassembler is a tool that does the reverse process of an assembler. That is, it converts a binary back into assembly.
  10. Disassembly Rom hacking ; adds the amount the player sold

    to their money AddAmountSoldToMoney:: ld de, wPlayerMoney + 2 ld hl, hMoney + 2 ; total price of items ld c, 3 ; length of money in bytes predef AddBCDPredef ; add total price to money ld a, MONEY_BOX ld [wTextBoxID], a call DisplayTextBoxID ; redraw money text box ld a, SFX_PURCHASE call PlaySoundWaitForCurrent jp WaitForSoundToFinish ; function to remove an item (in varying quantities) from the player's bag or PC box ; INPUT: ; HL = address of inventory (either wNumBagItems or wNumBoxItems) ; [wWhichPokemon] = index (within the inventory) of the item to remove ; [wItemQuantity] = quantity to remove RemoveItemFromInventory:: homecall RemoveItemFromInventory_ ret
  11. decompilation Binary Assembly Disassembler Higher-level code (e.g., C) Decompiler A

    decompiler is a program that performs the reverse process of a compiler.
 That is, it converts assembly back into a higher-level language
  12. matching decomp. And what if… the decompilation is the goal

    of a project? Build the level editor The decompilation is a mean for the project Decompile
  13. matching decomp. Decompile Readable code that generates the same binary

    The decompilation itself is the goal for the project The decompilation is a mean for the project Build the level editor Decompile
  14. matching decomp. It’s matching decompilation! Decompile Readable code that generates

    the same binary The decompilation itself is the goal for the project
  15. matching decomp. Matching decompilation is when the goal is
 to

    rewrite the code from a binary - using a
 high-level language and having a readable code - that, when compiled, it generates a binary that matches the original one Decompile Readable code that generates the same binary The decompilation itself is the goal for the project
  16. matching decomp. OoT MQ debug N64 ROM .z65 fi OoT

    Decomp github.com/zeldaret/oot Game assets (texturas, models…) Build process (how to assemble the ROM from the assets and code) Game code (physics, logic…) Match OoT MQ debug N64 rom .z65 fi
  17. matching decomp. .z65 fi OoT Decomp github.com/zeldaret/oot Game assets (textures,

    models…) Build process (how to assemble the ROM from the assets and code) Game code (physics, logic…) OoT MQ debug N64 rom .z65 fi OoT MQ debug N64 ROM Match
  18. matching decomp. .z65 fi OoT Decomp github.com/zeldaret/oot Game assets (textures,

    models…) Build process (how to assemble the ROM from the assets and code) Game code (physics, logic…) OoT MQ debug N64 rom .z65 fi OoT MQ debug N64 ROM Match
  19. matching decomp. .z65 fi OoT Decomp github.com/zeldaret/oot Game assets (textures,

    models…) Build process (how to assemble the ROM from the assets and code) Game code (physics, logic…) OoT MQ debug N64 rom .z65 fi OoT MQ debug N64 ROM Match
  20. matching decomp. .z65 fi OoT Decomp github.com/zeldaret/oot Game assets (textures,

    models…) Build process (how to assemble the ROM from the assets and code) Game code (physics, logic…) OoT MQ debug N64 rom .z65 fi OoT MQ debug N64 ROM Match
  21. matching decomp. .z65 fi OoT Decomp github.com/zeldaret/oot Game assets (textures,

    models…) Build process (how to assemble the ROM from the assets and code) Game code (physics, logic…) OoT MQ debug N64 rom .z65 fi le OoT MQ debug N64 ROM Match
  22. matching decomp. .z65 fi OoT Decomp github.com/zeldaret/oot Game assets (textures,

    models…) Build process (how to assemble the ROM from the assets and code) Game code (physics, logic…) OoT MQ debug N64 rom .z65 fi le OoT MQ debug N64 ROM Match
  23. matching decomp. walkthrough glabel func_8087828C /* 0012C 8087828C AFA50004 */

    sw $a1, 0x0004($sp) /* 00130 80878290 84820168 */ lh $v0, 0x0168($a0) ## 00000168 /* 00134 80878294 24010001 */ addiu $at, $zero, 0x0001 ## $at = 00000001 /* 00138 80878298 3C0E8016 */ lui $t6, 0x8016 ## $t6 = 80160000 /* 0013C 8087829C 1441000B */ bne $v0, $at, .L808782CC /* 00140 808782A0 24030002 */ addiu $v1, $zero, 0x0002 ## $v1 = 00000002 /* 00144 808782A4 95CEF566 */ lhu $t6, -0x0A9A($t6) ## 8015F566 /* 00148 808782A8 3C188088 */ lui $t8, %hi(func_80878300) ## $t8 = 80880000 /* 0014C 808782AC 27188300 */ addiu $t8, $t8, %lo(func_80878300) ## $t8 = 80878300 /* 00150 808782B0 31CF0040 */ andi $t7, $t6, 0x0040 ## $t7 = 00000000 /* 00154 808782B4 15E00005 */ bne $t7, $zero, .L808782CC /* 00158 808782B8 00000000 */ nop /* 0015C 808782BC 24030002 */ addiu $v1, $zero, 0x0002 ## $v1 = 00000002 /* 00160 808782C0 A4830178 */ sh $v1, 0x0178($a0) ## 00000178 /* 00164 808782C4 03E00008 */ jr $ra /* 00168 808782C8 AC980164 */ sw $t8, 0x0164($a0) ## 00000164 .L808782CC: /* 0016C 808782CC 14620005 */ bne $v1, $v0, .L808782E4 /* 00170 808782D0 3C198088 */ lui $t9, %hi(func_80878300) ## $t9 = 80880000 /* 00174 808782D4 27398300 */ addiu $t9, $t9, %lo(func_80878300) ## $t9 = 80878300 /* 00178 808782D8 A4830178 */ sh $v1, 0x0178($a0) ## 00000178 /* 0017C 808782DC 03E00008 */ jr $ra /* 00180 808782E0 AC990164 */ sw $t9, 0x0164($a0) ## 00000164 .L808782E4: /* 00184 808782E4 04410004 */ bgez $v0, .L808782F8 /* 00188 808782E8 3C088088 */ lui $t0, %hi(func_808783D4) ## $t0 = 80880000 /* 0018C 808782EC 250883D4 */ addiu $t0, $t0, %lo(func_808783D4) ## $t0 = 808783D4 /* 00190 808782F0 A4830178 */ sh $v1, 0x0178($a0) ## 00000178 /* 00194 808782F4 AC880164 */ sw $t0, 0x0164($a0) ## 00000164 .L808782F8: /* 00198 808782F8 03E00008 */ jr $ra /* 0019C 808782FC 00000000 */ nop
  24. matching decomp. walkthrough s16 func_8087828C(void *arg0, s32 arg1) { if

    (arg0->unk168 == 1) { if ((*(void *)0x8015F566 & 0x40) == 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } } if ((u16)2 == arg0->unk168) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } if (arg0->unk168 < 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_808783D4; } return arg0->unk168; }
  25. matching decomp. walkthrough s16 func_8087828C(void *arg0, s32 arg1) { if

    (arg0->unk168 == 1) { if ((*(void *)0x8015F566 & 0x40) == 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } } if ((u16)2 == arg0->unk168) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } if (arg0->unk168 < 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_808783D4; } return arg0->unk168; } static void func_8087828C( BgGateShutter* this, GlobalContext* globalCtx ) { if (this->unk_168 == 1) { if (!(gSaveContext.inf_table[7] & 0x40)) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } } if (this->unk_168 == 2) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } if (this->unk_168 < 0) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_808783D4; } }
  26. matching decomp. walkthrough s16 func_8087828C(void *arg0, s32 arg1) { if

    (arg0->unk168 == 1) { if ((*(void *)0x8015F566 & 0x40) == 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } } if ((u16)2 == arg0->unk168) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } if (arg0->unk168 < 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_808783D4; } return arg0->unk168; } s16 return arg0->unk168; static void func_8087828C( BgGateShutter* this, GlobalContext* globalCtx ) { if (this->unk_168 == 1) { if (!(gSaveContext.inf_table[7] & 0x40)) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } } if (this->unk_168 == 2) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } if (this->unk_168 < 0) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_808783D4; } } static void BgGateShutter* this, GlobalContext* globalCtx
  27. matching decomp. walkthrough s16 func_8087828C(void *arg0, s32 arg1) { if

    (arg0->unk168 == 1) { if ((*(void *)0x8015F566 & 0x40) == 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } } if ((u16)2 == arg0->unk168) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } if (arg0->unk168 < 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_808783D4; } return arg0->unk168; } if ((*(void *)0x8015F566 & 0x40) == 0) { if (!(gSaveContext.inf_table[7] & 0x40)) { static void func_8087828C( BgGateShutter* this, GlobalContext* globalCtx ) { if (this->unk_168 == 1) { if (!(gSaveContext.inf_table[7] & 0x40)) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } } if (this->unk_168 == 2) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } if (this->unk_168 < 0) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_808783D4; } }
  28. matching decomp. walkthrough s16 func_8087828C(void *arg0, s32 arg1) { if

    (arg0->unk168 == 1) { if ((*(void *)0x8015F566 & 0x40) == 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } } if ((u16)2 == arg0->unk168) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } if (arg0->unk168 < 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_808783D4; } return arg0->unk168; } (u16)2; (u16)2 (u16)2; (u16)2 static void func_8087828C( BgGateShutter* this, GlobalContext* globalCtx ) { if (this->unk_168 == 1) { if (!(gSaveContext.inf_table[7] & 0x40)) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } } if (this->unk_168 == 2) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } if (this->unk_168 < 0) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_808783D4; } } 2; 2 2 2
  29. matching decomp. walkthrough s16 func_8087828C(void *arg0, s32 arg1) { if

    (arg0->unk168 == 1) { if ((*(void *)0x8015F566 & 0x40) == 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } } if ((u16)2 == arg0->unk168) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } if (arg0->unk168 < 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_808783D4; } return arg0->unk168; } static void func_8087828C( BgGateShutter* this, GlobalContext* globalCtx ) { if (this->unk_168 == 1) { if (!(gSaveContext.inf_table[7] & 0x40)) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } } if (this->unk_168 == 2) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } if (this->unk_168 < 0) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_808783D4; } } (u16)2 == arg0->unk168) (this->unk_168 == 2)
  30. matching decomp. walkthrough s16 func_8087828C(void *arg0, s32 arg1) { if

    (arg0->unk168 == 1) { if ((*(void *)0x8015F566 & 0x40) == 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } } if ((u16)2 == arg0->unk168) { arg0->unk178 = (u16)2; arg0->unk164 = &func_80878300; return arg0->unk168; } if (arg0->unk168 < 0) { arg0->unk178 = (u16)2; arg0->unk164 = &func_808783D4; } return arg0->unk168; } static void func_8087828C( BgGateShutter* this, GlobalContext* globalCtx ) { if (this->unk_168 == 1) { if (!(gSaveContext.inf_table[7] & 0x40)) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } } if (this->unk_168 == 2) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } if (this->unk_168 < 0) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_808783D4; } }
  31. matching decomp. walkthrough static void func_8087828C( BgGateShutter* this, GlobalContext* globalCtx

    ) { if (this->unk_168 == 1) { if (!(gSaveContext.inf_table[7] & 0x40)) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } } if (this->unk_168 == 2) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_80878300; return; } if (this->unk_168 < 0) { this->unk_178 = 2; this->actionFunc = (ActorFunc)func_808783D4; } } void func_8087828C( BgGateShutter* this, PlayState* play ) { if ( this->openingState == 1 && !GET_INFTABLE(INFTABLE_76) ) { this->unk_178 = 2; this->actionFunc = func_80878300; } else if (this->openingState == 2) { this->unk_178 = 2; this->actionFunc = func_80878300; } else if (this->openingState < 0) { this->unk_178 = 2; this->actionFunc = func_808783D4; } }
  32. matching decomp. beyond When we achieve 100% match for a

    game, it paves the way for new possibilities
  33. AI + matching decomp. I wanted to continue working on

    Klonoa: Empire of Dreams. After building the level editor, the next challenge would be decompiling the game
  34. AI + matching decomp. But it’s a Gameboy Advance game.


    It isn’t a very popular platform on the decomp community
  35. AI + matching decomp. So, as a way to learn

    decompilation, I decided to help on decompile Sonic Advance 3
  36. AI + matching decomp. And although I got to decompile

    the fi rst function… …this process was very time consuming
  37. AI + matching decomp. But then, I started to wonder

    “this process looks like a lot as fi nding patterns”… Akatento Aotento Marun
  38. AI + matching decomp. Akatento Aotento Marun But then, I

    started to wonder “this process looks like a lot as fi nding patterns”… and AI excels at pattern matching!
  39. AI + matching decomp. Then, I focused on prompt re

    fi nement: • Adding examples from the other enemies • Including more context in the prompt
  40. AI + matching decomp. Then, I focused on prompt re

    fi nement: • Adding examples from the other enemies • Including more context in the prompt But I didn’t had a good result as the fi rst one…
  41. AI + matching decomp. Akatento Aotento Decompiled enemies: My target:

    Marun Gaogao Minimole … Finished after many prompts on DeepSeek R1 and a good PR review
  42. AI + matching decomp. Akatento Aotento Decompiled enemies: My target:

    Gaogao Minimole … Marun Kyacchaa Next target, but now using Claude Sonnet 4
  43. AI + matching decomp. Function Match Rate Di ffi culty

    to Finish CreateEntity_Kyacchaa 99% Easy sub_806599C 85% Medium sub_8065A8C 95% Easy sub_8065B0 85% Easy sub_8065B90 91% Hard sub_8065C48 61% Hard sub_8065E48 90% Easy sub_8065EB0 91% Easy Task_Kyacchaa 99-100% Easy sub_8065F5C 99-100% Easy sub_8065F30 99-100% Easy sub_8065CE0 99-100% Easy sub_8065F10 99-100% Easy TaskDestructor_Kyacchaa 99-100% Easy
  44. AI + matching decomp. Function Match Rate Di ffi culty

    to Finish CreateEntity_Kyacchaa 99% Easy sub_806599C 85% Medium sub_8065A8C 95% Easy sub_8065B0 85% Easy sub_8065B90 91% Hard sub_8065C48 61% Hard sub_8065E48 90% Easy sub_8065EB0 91% Easy Task_Kyacchaa 99-100% Easy sub_8065F5C 99-100% Easy sub_8065F30 99-100% Easy sub_8065CE0 99-100% Easy sub_8065F10 99-100% Easy TaskDestructor_Kyacchaa 99-100% Easy
  45. AI + matching decomp. It wires decompilation tools on
 VS

    Code It provides AI-powered features for decompilation on the top of VS Code As the result, it lower the bar to help on decompilation projects