ARM9 binary

DS game only γ¨γ‚“γŒγ‚Šγƒœγ‚¦γ‚·γ¨γŠγ—γ‚ƒγ‚Œγͺまほう぀かい The field guide documents the DS game's internals.

The main game executable β€” 723 KB of persistent engine code that drives overlays, loads assets, and manages every game system.

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 rangeSizeContent
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:

AddressStructure
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.

AddressFunction
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.

AddressFunction
0x02006488Get touch X
0x02006494Get touch Y
0x0200650CGet touch pressed state
0x02006DE4Test key edge-pressed (bitmask)
0x02006DFCTest key edge-released
0x02006D7CTest key held

Standard key bitmasks:

KeyBit
A0x01
B0x02
SELECT0x04
START0x08
RIGHT0x10
LEFT0x20
UP0x40
DOWN0x80

Sound manager

A singleton manages all audio playback.

AddressFunction
0x0202BC88Get sound manager instance
0x0202BF90Load sound bank (category, bank_id)
0x0202BFE8Poll bank load complete
0x0202C028Play/sustain sound
0x0202C074Get playback parameter
0x02085AE8Play 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.

AddressFunction
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.

AddressFunction
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

AddressFunction
0x0201B95CGet wallet struct pointer
0x020451E8Get wallet cash (clamped to 999,999)
0x020451D8Set wallet cash
0x020451A0Get bank balance (clamped to 9,999,999)
0x02045190Set bank balance

NPC data access

AddressFunction
0x0201B21CGet NPC record by index (stride 196 bytes, save-buffer-B)
0x0201B560Query NPC friendship/role stride
0x02199D70Test "is NPC present?"
0x0219E12CQuery "does NPC know spell at tier N?"
0x0201A364Get spell weight for card selection

RNG

AddressFunction
0x01FF8E00 Uniform random(lo, hi) β€” returns integer in [lo, hi)

Math intrinsics

AddressFunction
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.