Dialog chain

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

How the game routes a world event to a specific line of dialog β€” from trigger tables through message files to the renderer.

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

  1. Game event β€” an NPC interaction, area entry, item use, or scripted trigger fires an event ID.
  2. 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.
  3. 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.
  4. 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:

FolderFile-ID rangeCount
data/message/ (deep tree)511 -- 18381,328
data/script/2151 -- 2533383
data/onemsg/2122 -- 213312
data/talk/ (single word.dat)25491
data/talkstart/2550 -- 258940
data/topic/2591 -- 25944

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 IDFile IDResolves to
0xf37a2550talkstart/tkst00.ofs
0xf37e2555talkstart/tkst05.ofs
0xf37f2560talkstart/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:

OffsetTypeField
+0x00u32record_count
+0x04u32header_size (always 16)
+0x08u32unknown
+0x0cu32total_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 byteMeaningFile-ID fieldPurpose
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 indexResolves toContent
0x1388 (5000)magic/magic.lzsMagic-list popup text
0x138a (5002)message/msg01/00/msg01001.ofsDialog file
0x138b (5003)message/msg20/04/msg20043.ofsDialog file
0x13ab (5035)message/msg98/05/msg98050.ofsDialog 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:

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

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:

Related