At a glance

Where
CDNAME.TXT at the disc root; ASCII, one #define name N per line. Retail loads it into a 16-byte-record table at RAM 0x80088758
Numbering
N is a raw TOC index; the content lives at extraction entry N − 2
Parser
crates/prot/src/cdname.rs: parse_str (tolerant), parse_retail_str / retail_name_table (byte-exact), block_for_extraction_index (applies the −2 rule)
Confidence
Confirmed - loader traced; the shift is pinned by four retail loader constants and a disc-gated test
Used by
Asset loader, prot-extract filenames, every scene window in the engine, boot

What the file says

The file looks like the top of a C header:

#define init_data 0
#define gameover_data 1
#define town01 3
#define town0b 12
#define town0c 21
...
#define vab_01 1072

Each line names a block of consecutive archive entries - usually one scene (a town, a dungeon room, a menu) plus the handful of entries reserved for its assets. Two rules do all the work:

  • Names inherit forward. #define name N starts a block; every entry after N belongs to it until the next define. Entry 11 is town01 because town0b only starts at 12.
  • The numbers are the game's index space. The game counts raw TOC words, header included; extraction counts payload entries, which start two words later. So #define name N names extraction entry N − 2.

Most blocks reserve 6-8 slots per scene. Unused slots hold the one-sector pochi filler, never a real asset; the edstati3 block is almost entirely filler.

Numbering space (the −2 correction)

The boot loader copies PROT.DAT verbatim - 8-byte header included - into RAM at 0x801C70F0 (PROT TOC), so raw index = extraction index + 2. The extractor's NNNN_<name>.BIN filenames apply define numbers as extraction indices directly, and are kept that way for stability - so a filename label is systematically two slots early.

CDNAME.TXT raw TOC (RAM) extraction file hdr 0 hdr 1 raw 3 raw 4 raw 5 ... raw 12 #define town01 3 town0b 12 0001 0002 0003 ... 0010 town01 .MAP town0b .MAP
The two header words the game counts are the whole shift: define 3 is raw index 3 is extraction file 0001.
#defineRaw TOC indexExtraction entryContent
town01 330001town01 slot 0 - the scene's .MAP
(inherits)40002slot 1 - the v12 trigger sidecar
(inherits)5 .. 110003 .. 0009slots 2..8 (prescript, 7-asset table, assets)
town0b 12120010town0b slot 0 - filename still says 0010_town01.BIN

A label is a hint, not a verdict. When a shift-corrected block name still conflicts with the bytes, trust the bytes: the leading magic and the loader-call constant in the executable.

What the dev names really cover

With the shift applied the developers' names are accurate far more often than not. The entries most often misread from their filename:

ExtractionFilename saysRetail block (content)
raw 0..1(unindexed)init_data + gameover_data - the boot-UI gap with the menu-glyph atlas (system-UI bundle)
0863..0866edstati3/battle_databattle_data - the four player battle files
0867battle_datamonster_data - the monster stat / mesh / animation archive
0870..0873sound_data/befect_databefect_data - effect bundles (etim, etmd+vdf, billboards, efect.dat)
0874befect_dataplayer_data - the field character-mesh pack
0891level_upmonster_se - monster.snd, the 206-bank monster sound archive
0893..0894monster_se/card_databat_back_dat - summon.dat / readef.DAT (summon-readef)
0895..0969bat_back_dat/xxx_datxxx_dat - slot 0 is the boot init.pak; then the overlay code blobs
0970..0971xxx_datmove_program_no - movie program table (STR FMV table), not Tactical Arts
0972..0977move_program_no/other_gameother_game - casino / minigame overlays
1070..1192music_01/vab_01vab_01 - every entry a VAB bank

The music_01 block's bank map is piecewise: BGM id 2000+i is extraction 988+i for i ≤ 67 and 990+i for i ≥ 68 (music tracks).

Caveats that do not contradict the shift
  • level_up entry 0890 is a streaming carrier with its VABs wrapped inside, so a bare-magic check misses it at every shift.
  • Entries 0888 (sound_data2) and 1062 (music_01) are unidentified non-VAB blobs.
  • Only 2 of the 6 other_game entries carry an OTHER<n> banner.
  • Entry 0893 (summon.dat) opens with a shape that mimics a sound-address bank; the byte pins show texture streaming slots.
  • other7 (extraction 1226+) is a dev-leftover earlier revision of koin3's field MAN - a name-exact prefix subset of its records, sharing its gate flags. No code loads it.
  • One v12-shaped header sits at 1227, outside the scene region.
Engine consequence: scene windows are the retail block

Scene::load converts the raw-TOC block range to the extraction frame so a scene's entries are its retail block: first the .MAP, then the v12 sidecar. An unshifted window is two entries late - it drops those and bleeds in the next block's first two, mis-framing any scene whose load-bearing entry sits at a block edge (an unshifted rikuroa window loads Jeremi's MAN). The two head defines (init_data 0, gameover_data 1) sit inside the TOC header rows and keep their unshifted legacy windows.

History: "mislabeled block" findings that were the shift

Several earlier findings that the developers had mislabeled blocks - vab_01 "without VAB headers", 0895_bat_back_dat being init.pak, the "battle_data 0865 vs monster archive 0867" tangle - were the filename shift, not dev mislabeling. Collected in do-not-re-walk.

What retail holds in RAM: long names are mangled

The boot loader copies each name into a 16-byte record and then stores the index over bytes 12-13 of the same record. There is no length check and no NUL is ever written. Consequences:

Name lengthWhat the record holds
≤ 11The name, terminated by a table zero. Clean.
12-13The name with the index's two bytes overlaid at 12..13 and no terminator of its own.
14-15As above, plus bytes 14-15 of the name, which survive past the index store.
≥ 16Spills into the next record. No shipped name is this long; a modded CDNAME.TXT can be.

Clean C-string capacity is 11 bytes, not 12. Five shipped names exceed it and none round-trips:

#define nameIndexIn-RAM C string
gameover_data1gameover_dat + 01
monster_data869monster_data + 65 03
bat_back_dat895bat_back_dat + 7F 03
move_program_no972move_program + CC 03 + o
monster_test980monster_test + D4 03
  • Repo tooling uses the tolerant parse_str, which keeps the full spelling. Anything compared against retail RAM must go through retail_name_table / parse_retail_str, which return names as bytes - the index bytes are routinely not valid UTF-8, and a lossy conversion prints fd for every one of them.
  • The first line not starting with # ends the map; a line with an empty name still emits a record.
  • Index 990 is declared twice (music_test, then music_01). Retail emits two records; the tolerant map keeps the later name. Compare the two readers by declaration order, not by index.
  • Retail really does populate the table: main() sets the source flag at cold boot so the loader reads cdname.txt off the disc rather than over the developer-station link.
History: the "writer-less flag" framing

An earlier reading held that the source flag _DAT_8007B8C2 had no writer and the table might never be populated. The sweep behind it searched only the absolute lui+offset form and missed the gp-relative store. Catalogued in Ghidra tooling under decompiler artifacts that have produced false claims.

How we know

Function / testAddressWhat it provesDump
CDNAME loaderFUN_8001D8FC16-byte records at 0x80088758; unbounded copy at 0x8001D980, index stores at 0x8001DA78/0x8001DA908001d8fc.txt
Source flag write0x80015F08 in main()sh v0,0x5aa(gp) sets _DAT_8007B8C2 non-zero, selecting the disc readSCUS disassembly
PROT resolverFUN_8003E8A8Indexes the raw in-RAM TOC, header included8003e8a8.txt
Player-file loaderFUN_800558FCConstants 0x361..0x364 = defines battle_data 865..868live trace
Monster SE loaderFUN_8003E104li v0,0x37d = define monster_se 8938003e104.txt
Side-band loaderFUN_801F17F80x37F/0x380 = bat_back_dat 895; RAM↔disc byte-verified at extraction 893/894overlay_battle_801f17f8.txt
Overlay pagerFUN_8003EBE4 / FUN_8003EC70param + 0x381 = xxx_dat 897; param 2 = extraction 08978003ec70.txt
Structural checkv12 tablesAll 96 scene-region sidecars sit at block slot 1 only at shift −2 (scene blocks vary 7..11 slots)scripts/asset-investigation/cdname_shift_analysis.py
Disc-gated testcdname_retail_parse_discEach shipped name against an independent byte-level record modelcrates/prot

Source of record: docs/formats/cdname.md.

See also