When a player talks to an NPC, enters an area, or uses an item, the engine has to figure out which specific line of dialog to show. That resolution isn't a single lookup β it's a four-step chain that starts with an abstract event ID and ends with rendered UTF-16-LE text on screen. Every piece of dialog in the game travels this chain, and understanding it is the key to finding where any given line lives in the ROM.
The chain at a glance
- Game event β an NPC interaction, area entry, item use, or scripted trigger fires an event ID.
- Content-ID lookup β a 9-byte record table in
arm9.bin(or an overlay) resolves the event's content ID to a numeric ROM file ID. - Talkstart resolution β the talkstart file at that file ID maps a (scenario, slot) pair to a specific message file ID using fixed 20-byte records.
- Message file playback β the message file (a RESO v1.21 variant=1 container) holds the actual dialog: CMD records and UTF-16-LE text. Overlay 157 renders the result on screen.
Steps 2 and 3 are pure indirection β they exist so the engine can
route thousands of distinct events to the right file using compact
integer tables instead of path strings. The engine never references
files by path at runtime; a grep of arm9.bin for path
strings like "talkstart", "msg", or "message" returns zero matches.
Everything is integer file IDs from the NDS FAT/FNT.
File-ID addressing
The NDS file allocation table (FAT) assigns every file in the ROM a sequential integer ID. The file name table (FNT) maps those IDs to directory paths β but only extraction tools use the FNT. The running game works exclusively with the integer IDs. The dialog-relevant ranges are:
| Folder | File-ID range | Count |
|---|---|---|
data/message/ (deep tree) | 511 -- 1838 | 1,328 |
data/script/ | 2151 -- 2533 | 383 |
data/onemsg/ | 2122 -- 2133 | 12 |
data/talk/ (single word.dat) | 2549 | 1 |
data/talkstart/ | 2550 -- 2589 | 40 |
data/topic/ | 2591 -- 2594 | 4 |
Content-ID lookup tables
The first resolution step converts a content ID (the abstract "I want content X" value) into a file ID. The engine does this by scanning tables of fixed 9-byte records. Each record packs two (key, file_id) pairs:
byte 0 = 0x00
bytes 1..2 = u16 LE key
bytes 3..4 = u16 LE file_id
bytes 5..6 = u16 LE key
bytes 7..8 = u16 LE file_id
Eight such tables exist across the ROM β five baked into
arm9.bin and three in overlays:
| Location | Records | Key range | File-ID range | Folders covered |
|---|---|---|---|---|
arm9 @ 0x7ee5f | 180 | 0xf02e -- 0xf3c6 | 613 -- 2642 | message, minigame, model, mydesign, npc, onemsg, pc_shop, recipe, script, shop, start, talkstart, topic, ucc (14 folders) |
arm9 @ 0x7c4dc | 8 | 0x0c3a -- 0x0c76 | 2569 -- 2642 | talkstart, topic, ucc |
arm9 @ 0x7c75d | 14 | 0x0e2e -- 0x0e79 | 1746 -- 1898 | message, minigame, model |
arm9 @ 0x7ca14 | 18 | 0x0f87 -- 0x0fb6 | 774 -- 989 | message |
arm9 @ 0x7dc42 | 12 | 0xf02c -- 0xf044 | 339 -- 737 | 2d, message |
ov027 @ 0x299b | 14 | 0x00f1 -- 0x015b | 248 -- 350 | 2d |
ov126 @ 0x3935 | 8 | 0x01b6 -- 0x0962 | 484 -- 2416 | map, message, model, pc_shop, script |
ov143 @ 0xd04 | 33 | 0x03ee -- 0x0470 | 1007 -- 1137 | message |
The biggest table (180 records at arm9 0x7ee5f) covers
14 different top-level folders β dialog, NPC data, scripts, models,
and more all route through the same lookup mechanism. A few example
entries from that table:
| Content ID | File ID | Resolves to |
|---|---|---|
0xf37a | 2550 | talkstart/tkst00.ofs |
0xf37e | 2555 | talkstart/tkst05.ofs |
0xf37f | 2560 | talkstart/tkst10.ofs |
Talkstart files
The 40 talkstart files (data/talkstart/tkstNN.ofs) are
the second hop in the chain. They are RESO v1.21 variant=1
containers, and each entry inside one holds a header followed by an
array of fixed 20-byte records. Each talkstart file represents a
scenario type β greeting, shopping, combat, and so on β and each
record within it is a specific slot for that scenario, identifying
which character or zone is involved and which message file to load.
Entry header
Each entry begins with a 16-byte header:
| Offset | Type | Field |
|---|---|---|
+0x00 | u32 | record_count |
+0x04 | u32 | header_size (always 16) |
+0x08 | u32 | unknown |
+0x0c | u32 | total_entry_size (= 16 + record_count x 20) |
Record type bytes
Each 20-byte record has a type byte at offset +0x12 that determines how the rest of the record is interpreted. Three type bytes are classified:
| Type byte | Meaning | File-ID field | Purpose |
|---|---|---|---|
0x02 | Dialog trigger | u16 at +0x04 | Loads and plays the message file at that file ID |
0x0b | Asset preload | u16 at +0x00 | Preloads a UI, sound, or 2D resource at scene start |
0xff | Data reference | u16 at +0x04 | Tracks an asset (tkst20--31) or indexes a per-scenario lookup (tkst40--51) |
Worked example: tkst20 entry 0
Entry 0 of tkst20.ofs is a verified chain. A selection
of its records shows the pattern:
| Record index | Resolves to | Content |
|---|---|---|
| 0x1388 (5000) | magic/magic.lzs | Magic-list popup text |
| 0x138a (5002) | message/msg01/00/msg01001.ofs | Dialog file |
| 0x138b (5003) | message/msg20/04/msg20043.ofs | Dialog file |
| 0x13ab (5035) | message/msg98/05/msg98050.ofs | Dialog file |
Triggers 5002 through 5035 walk through roughly 36 different message files spaced exactly 32 file IDs apart. This is "play the same scenario but in a different NPC's voice" β one trigger per active speaker for that scenario.
Message files
The message files (data/message/*.ofs) are the terminal
node of the chain β where actual dialog text lives. Each is a RESO
v1.21 variant=1 container with up to 100 sparse slots. Each slot
holds a dialog entry consisting of alternating CMD records (delimited
by 0x2400 open / 0x3B00 close) and
UTF-16-LE body text.
The body text uses U+0000 as phrase boundaries,
a dedicated break marker for page/line breaks within a phrase, and
{kanji|reading} notation for furigana. The full
byte-level wire format is documented on the
wire format page.
Total across the game: 1,328 message containers holding 50,196 dialog entries.
The message folder tree
The data/message/ directory organizes its roughly 1,700
files by category:
| Prefix | Subdirectories | Content |
|---|---|---|
msg00 -- msg12 | /00/ | System and event dialog |
msg20 -- msg31 | /00/ through /06/ | Per-NPC personality dialog (12 personality types, roughly 45 files each) |
msg32 -- msg33 | /00/ through /13/ | Grouped dialog (NPCs by category) |
msg40 -- msg52 | /00/ through /10/ | Large groups (11 subdirs each) β classroom lessons, seasonal events |
msg98 | /00/ through /09/ | System messages, error text |
msg99 | /00/ through /14/ | UI labels, crafting tables, encyclopedia entries, lookup data |
Runtime rendering
Once the chain resolves to a message file and a slot within it,
overlay 157 (the dialog renderer, always resident during
conversations) takes over. The caller passes a bubble_sel
byte that determines the rendering mode:
- 0 = WINDOW β a 260 x 138 pixel dialog box at the bottom of the screen, typically three lines of roughly 30 characters each.
- 1--4 = BUBBLE β a 128 x 76 pixel floating bubble above the speaker, two lines maximum, with a practical cap of 32 visible characters per page.
The renderer maintains a 660-byte instance struct per active dialog and runs a 9-state per-frame state machine to handle typewriter animation, color, and layout. The full rendering details are on the bubble vs window page.
Onemsg files
The 12 files in data/onemsg/ are single-shot floating
messages β the tooltip text that appears over characters' heads as
the player walks past them. These always render as bubbles (32-char
page cap) and use the same RESO v1.21 format as regular message
files, but are loaded through a separate code path that bypasses
talkstart resolution entirely. They enter the renderer with
bubble_sel = 4 hardcoded.
Open questions
Three parts of the chain are not yet fully traced:
- The tokenizer function. The exact ARM function that
parses the raw
0x2400...0x3B00byte stream into the 28-byte records the dispatch consumes has not been identified. The strongest candidates are functions reachable fromarm9 @ 0x0200ba54. - Slot selection. A message file can hold up to 100 slots. The logic that selects which slot to play for a given dialog trigger lives in the arm9 scene manager and has not been fully reverse-engineered.
- Additional talkstart type bytes. Only type bytes
0x02,0x0b, and0xffare classified.tkst32alone uses roughly 40 different type bytes whose meanings are unknown.
Related
- Wire format β the byte-level CMD + text encoding inside message files.
- Opcode catalog β all 88 CMD opcodes with hex values and arg layouts.
- Bubble vs window β how ov157 picks the rendering container and the 32-char bubble cap.
- Shared payloads β TOC deduplication within message containers.