Overview
arm9.bin is the persistent main executable. It loads at boot and stays resident for the entire game session β unlike overlays, which swap in and out of shared RAM slots as the player moves between game modes. Every overlay calls into arm9 for shared services: asset loading, sprite rendering, input polling, sound, dialog parsing, save data access.
The binary is BLZ-compressed on the ROM cartridge: 564,500 bytes
compressed, 723,044 bytes decompressed. The BLZ footer sits just
before the trailing NDS module signature bytes
(21 06 C0 DE / 21 06 AE D5), not at the
very end of the file. This detail matters β generic BLZ decompression
tools that look for the footer at EOF will fail.
Memory layout
The decompressed arm9 loads at address 0x02000000.
Ghidra auto-analysis identified approximately 5,005 functions.
| Address range | Size | Content |
|---|---|---|
0x02000000–0x020001FF | 512 B | NDS vector table + startup stubs |
0x02000200–0x0209FFFF | ~640 KB | Code (THUMB/ARM functions) |
0x020A0000–0x020AA73F | ~42 KB | Read-only data tables, constant pools, string literals |
Notable fixed addresses within the data region:
| Address | Structure |
|---|---|
0x020AA740 | Master content-cache table (712 references) |
0x020A9000 | Global input state struct (key state, touch position) |
0x020B6CA8 | Primary sprite-cache struct |
Key subsystems
The following major subsystems have been identified through Ghidra reverse engineering.
Content cache (asset loader)
The central asset-loading system. A master table at
0x020AA740 holds 712 references. The generic loader
function at FUN_02009894 takes:
(loader_context, path_string, sub_index, max_size, flags,
type_code, ...).
The path string region spans approximately 28 KB of arm9 data space
and contains 131 unique path strings like
"item/itemicon.ofs",
"2d/audition.lzs",
"npc/NpcData.lzs". The engine resolves these strings to
FAT file IDs at runtime. 16 named asset-loader wrapper functions have
been identified in arm9, each specialized for a specific content type
(item icons, UCC materials, recipes, etc.).
See the content cache page for the full loader call-site catalog and entry layout.
Sprite rendering pipeline
The OAM (Object Attribute Memory) sprite pipeline runs through arm9. It manages a shadow-OAM buffer, flushes it to hardware via DMA during the vertical blank, and allocates/frees sprite slots.
| Address | Function |
|---|---|
0x0201F12C | Sprite constructor (primary cache at 0x020B6CA8) |
0x0201C3A0 | Sprite constructor (alternate cache) |
0x020769C0 | OAM flush to main screen |
0x02076A00 | OAM flush to sub screen |
Input system
Hardware key state and touch-screen input are managed by a global
struct at 0x020A9000. Overlays read from this struct via
accessor functions rather than polling hardware directly.
| Address | Function |
|---|---|
0x02006488 | Get touch X |
0x02006494 | Get touch Y |
0x0200650C | Get touch pressed state |
0x02006DE4 | Test key edge-pressed (bitmask) |
0x02006DFC | Test key edge-released |
0x02006D7C | Test key held |
Standard key bitmasks:
| Key | Bit |
|---|---|
| A | 0x01 |
| B | 0x02 |
| SELECT | 0x04 |
| START | 0x08 |
| RIGHT | 0x10 |
| LEFT | 0x20 |
| UP | 0x40 |
| DOWN | 0x80 |
Sound manager
A singleton manages all audio playback.
| Address | Function |
|---|---|
0x0202BC88 | Get sound manager instance |
0x0202BF90 | Load sound bank (category, bank_id) |
0x0202BFE8 | Poll bank load complete |
0x0202C028 | Play/sustain sound |
0x0202C074 | Get playback parameter |
0x02085AE8 | Play SFX by ID (thin wrapper) |
Scene manager
Manages overlay loading and scene transitions. Each overlay's scene vtable (typically 6 entries: init, update, draw_bg, draw_obj, destroy, free) is called by the scene manager's per-frame dispatch loop.
| Address | Function |
|---|---|
0x02018698 | Register scene-exit slot (slot_id, owner, flag) |
0x02003CC0 | Poll "renderer fully released" |
Dialog system
The dialog parser and dispatch live primarily in arm9, with rendering delegated to ov157.
| Address | Function |
|---|---|
0x02066C58 | Universal dialog parser entry β calls ov157's dialogue_renderer_init_canonical() |
0x02049CB8 | Dialog record iterator init |
0x02049D1C | Bind iterator to data buffer |
0x02049D5C | Get record count |
0x02049D6C | Get record by index (returns base + idx x 28 bytes) |
Wallet / save data
| Address | Function |
|---|---|
0x0201B95C | Get wallet struct pointer |
0x020451E8 | Get wallet cash (clamped to 999,999) |
0x020451D8 | Set wallet cash |
0x020451A0 | Get bank balance (clamped to 9,999,999) |
0x02045190 | Set bank balance |
NPC data access
| Address | Function |
|---|---|
0x0201B21C | Get NPC record by index (stride 196 bytes, save-buffer-B) |
0x0201B560 | Query NPC friendship/role stride |
0x02199D70 | Test "is NPC present?" |
0x0219E12C | Query "does NPC know spell at tier N?" |
0x0201A364 | Get spell weight for card selection |
RNG
| Address | Function |
|---|---|
0x01FF8E00 | Uniform random(lo, hi) β returns integer in [lo, hi) |
Math intrinsics
| Address | Function |
|---|---|
0x02002194 | Signed 32-bit divmod (quotient in r0, remainder in r1) |
0x020023A0 | Unsigned 32-bit divmod |
Content-ID lookup tables
arm9 contains five 9-byte (key, file_id) lookup tables that the
engine uses to resolve content IDs to ROM file IDs. The largest table
at offset 0x7EE5F has 180 records covering 14 different
top-level folders.
Editing arm9
The ARM9 binary can be edited in-place if the edit stays within the same byte count. The BLZ compression can be removed (the NDS loader handles uncompressed arm9 if the module-params are zeroed), which makes the binary larger on cartridge but allows direct hex editing of the decompressed code.
String literals embedded in arm9 (like NPC action verbs in the menu pool) can be edited as long as the replacement fits the original byte budget.