CDNAME.TXT - entry name map Confirmed
The developers left a plain-text file at the root of the disc: a list of C-style #define lines that give human-readable names - town01, battle_data, music_01 - to the numbered entries of PROT.DAT, the disc's single big asset archive. It is the closest thing the disc has to a directory listing, and the game itself reads it at boot. It also hides a numbering trap: its numbers count entries the way the game does, which is two higher than the way this project's extraction does.
At a glance
- Where
CDNAME.TXTat the disc root; ASCII, one#define name Nper line. Retail loads it into a 16-byte-record table at RAM0x80088758- Numbering
Nis a raw TOC index; the content lives at extraction entryN − 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-extractfilenames, 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 Nstarts a block; every entry afterNbelongs to it until the next define. Entry 11 istown01becausetown0bonly 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 Nnames extraction entryN − 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.
#define | Raw TOC index | Extraction entry | Content |
|---|---|---|---|
town01 3 | 3 | 0001 | town01 slot 0 - the scene's .MAP |
| (inherits) | 4 | 0002 | slot 1 - the v12 trigger sidecar |
| (inherits) | 5 .. 11 | 0003 .. 0009 | slots 2..8 (prescript, 7-asset table, assets) |
town0b 12 | 12 | 0010 | town0b 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:
| Extraction | Filename says | Retail block (content) |
|---|---|---|
| raw 0..1 | (unindexed) | init_data + gameover_data - the boot-UI gap with the menu-glyph atlas (system-UI bundle) |
| 0863..0866 | edstati3/battle_data | battle_data - the four player battle files |
| 0867 | battle_data | monster_data - the monster stat / mesh / animation archive |
| 0870..0873 | sound_data/befect_data | befect_data - effect bundles (etim, etmd+vdf, billboards, efect.dat) |
| 0874 | befect_data | player_data - the field character-mesh pack |
| 0891 | level_up | monster_se - monster.snd, the 206-bank monster sound archive |
| 0893..0894 | monster_se/card_data | bat_back_dat - summon.dat / readef.DAT (summon-readef) |
| 0895..0969 | bat_back_dat/xxx_dat | xxx_dat - slot 0 is the boot init.pak; then the overlay code blobs |
| 0970..0971 | xxx_dat | move_program_no - movie program table (STR FMV table), not Tactical Arts |
| 0972..0977 | move_program_no/other_game | other_game - casino / minigame overlays |
| 1070..1192 | music_01/vab_01 | vab_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_upentry 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_gameentries carry anOTHER<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 ofkoin3'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 length | What the record holds |
|---|---|
| ≤ 11 | The name, terminated by a table zero. Clean. |
| 12-13 | The name with the index's two bytes overlaid at 12..13 and no terminator of its own. |
| 14-15 | As above, plus bytes 14-15 of the name, which survive past the index store. |
| ≥ 16 | Spills 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 name | Index | In-RAM C string |
|---|---|---|
gameover_data | 1 | gameover_dat + 01 |
monster_data | 869 | monster_data + 65 03 |
bat_back_dat | 895 | bat_back_dat + 7F 03 |
move_program_no | 972 | move_program + CC 03 + o |
monster_test | 980 | monster_test + D4 03 |
- Repo tooling uses the tolerant
parse_str, which keeps the full spelling. Anything compared against retail RAM must go throughretail_name_table/parse_retail_str, which return names as bytes - the index bytes are routinely not valid UTF-8, and a lossy conversion printsfdfor 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, thenmusic_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 readscdname.txtoff 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 / test | Address | What it proves | Dump |
|---|---|---|---|
| CDNAME loader | FUN_8001D8FC | 16-byte records at 0x80088758; unbounded copy at 0x8001D980, index stores at 0x8001DA78/0x8001DA90 | 8001d8fc.txt |
| Source flag write | 0x80015F08 in main() | sh v0,0x5aa(gp) sets _DAT_8007B8C2 non-zero, selecting the disc read | SCUS disassembly |
| PROT resolver | FUN_8003E8A8 | Indexes the raw in-RAM TOC, header included | 8003e8a8.txt |
| Player-file loader | FUN_800558FC | Constants 0x361..0x364 = defines battle_data 865..868 | live trace |
| Monster SE loader | FUN_8003E104 | li v0,0x37d = define monster_se 893 | 8003e104.txt |
| Side-band loader | FUN_801F17F8 | 0x37F/0x380 = bat_back_dat 895; RAM↔disc byte-verified at extraction 893/894 | overlay_battle_801f17f8.txt |
| Overlay pager | FUN_8003EBE4 / FUN_8003EC70 | param + 0x381 = xxx_dat 897; param 2 = extraction 0897 | 8003ec70.txt |
| Structural check | v12 tables | All 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 test | cdname_retail_parse_disc | Each shipped name against an independent byte-level record model | crates/prot |
Source of record: docs/formats/cdname.md.