PROT.DAT / DMY.DAT TOC Confirmed
PROT.DAT is the disc's one big asset archive. Nearly everything you see and hear in the game -
every texture, model, sound bank, dialogue container, animation set, scene script, and the chunks of engine code that
load on demand - is one of its 1233 numbered entries. Only the movies, the streamed voice audio and the main
executable live outside it. This page covers its table of contents (TOC): the index at the front of the file that says
where each entry starts and how big it is.
At a glance
- Where
PROT.DATat the disc root; header + TOC = the first 3 sectors.DMY.DATshares the layout (dev fixtures only)- Magic
- None - two header words, then a flat
u32LBA table - Size / stride
- 1236 TOC rows of 4 bytes; entry size = gap to the next row, in 2048-byte sectors
- In RAM
- Raw copy of the first 3 sectors at
0x801C70F0 - Parser
crates/prot(Archive,runtime_toc,tiling)- Confidence
- Confirmed - retail's own resolver does the same subtraction, and the entries tile the archive exactly
- Used by
- asset loader, boot sequence, extraction pipeline, asset viewer
Three things to hold onto
An entry is one numbered region of the archive - the unit the game requests when it loads a scene, a battle or a menu. The TOC measures in sectors (2048 bytes) and LBAs, where an LBA here counts sectors from the start of PROT.DAT itself, not from the start of the disc.
- One word per entry. The TOC stores only each entry's start LBA. Its size is the gap to the next entry's start - exactly how the game computes it.
- The entries tile the archive. Starts ascend, each entry ends where the next begins, and the sizes sum to the whole file. That is what makes the reading self-checking.
- Two index spaces, off by 2. The game counts TOC rows from the file's first word; this project's extraction counts from the first entry. Every index recovered from game code is 2 higher than the matching
extracted/PROT/NNNN_*.BIN, and CDNAME.TXT is authored in the game's space.
Layout
PROT.DAT. Word +0x08 is both the TOC-size word and toc[0]; the first extracted entry's start LBA is two words later.| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x00 | 4 | unused | Zero in both retail archives |
+0x04 | 4 | rows - 1 | TOC row count minus one: PROT.DAT 1235, DMY.DAT 15 |
+0x08 | 4 | toc[0] | Header + TOC size in sectors (3), which is also the first content LBA |
+0x0C | 4 × rows | toc[1..] | Start LBAs; toc[1235] = 59206 is the end terminator |
The 1236 rows are toc[0] and toc[1] (two boot-UI regions), 1233 entry starts, and one terminator. For extraction entry p:
start_lba = toc[p + 2] // LBA relative to PROT.DAT
size_sectors = toc[p + 3] - toc[p + 2] // the gap to entry p+1
byte_offset = start_lba * 0x800
size_bytes = size_sectors * 0x800
The LBAs are PROT.DAT-relative, so they are identical on the USA and PAL discs even though PROT.DAT sits at a different disc LBA per region. That is what makes whole-sector entry growth tractable: shift the later start-LBA words, no disc-wide cascade (disc § full-ISO relayout). The detector tries the header at offset 0x000 first, then 0x800; both retail archives use 0x000.
In-RAM TOC and the two index spaces
At boot the game copies the first three sectors of PROT.DAT into RAM, header words included, and resolves every later asset request against that copy. There is no transformation - but the extraction builds its toc[] after the two header words, so the two sides count from different origins.
| File word | Raw TOC index (game) | Extraction index | Holds |
|---|---|---|---|
| 0, 1 | - | - | the two header words |
| 2 | 0 | (unindexed) | boot-UI region a, LBA 3 |
| 3 | 1 | (unindexed) | boot-UI region b |
| 4 | 2 | 0000 | first extracted entry, LBA 121 |
p + 4 | p + 2 | p | start LBA of extraction entry p |
Raw TOC index = extraction index + 2. Any PROT index recovered from a resolver argument must subtract 2 to land in extraction space (the battle side-band files: raw 0x37F / 0x380 = extraction 893 / 894). CDNAME.TXT's #define numbers are raw-space, so extraction filename labels are shifted +2 from the content the defines name.
Raw entries 0 and 1 (LBA 3..120) are two TIM-packs holding the boot-resident system-UI bundle - glyph atlas, sprite sheets, cursor parts (tim-pack).
How the game asks for an entry
- By index - the streaming loader and the dev-build sound branch call the resolver with a raw TOC index.
- By path - dev paths like
data\battle\efect.datorh:\PROT\FIELD\<scene>\…resolve through the CDNAME name map to an index, then to the resolver. Most retail code lands here. - Overlays - the field, battle and menu engines are code entries loaded on mode switch. Two paired loaders manage two independently swappable RAM slots, each calling the resolver with
param + 0x381= extraction entryparam + 0x37F(2 = 0897 field, 3 = 0898 battle, 4 = 0899 menu). Two overlays can be resident at once.
A dual-mode loader chooses between the TOC index (retail, the boot-time value of _DAT_8007B8C2) and an h:\ dev-station path through a break 0x103 host trap. Only the retail branch runs on a real disc. The CD-read API stack behind these is on boot.
How we know
| Function | Address | What it proves | Dump |
|---|---|---|---|
| Entry size | FUN_8003E68C | Twelve instructions returning TABLE[i+3] - TABLE[i+2]; result goes straight to the sector read | funcs/8003e68c.txt |
| TOC resolver | FUN_8003E8A8 | Indexes 0x801C70F0 + (idx+2)*4; stores start_lba at gp+0x8f0, returns the span in v0 | funcs/8003e8a8.txt |
| Boot TOC copy | FUN_8003E4E8 | Reads the first 3 sectors raw into 0x801C70F0; byte-verified against a save state | funcs/8003e4e8.txt |
| By-index loader | FUN_8003EB98 → FUN_8003E800 | Passes the resolver's span as the sector count | funcs/8003eb98.txt |
| Path resolver | FUN_8003E6BC | CDNAME name map → index → LBA resolver | funcs/8003e6bc.txt |
| Overlay slot loaders | FUN_8003EBE4 (*DAT_8001038C, gp+0x924), FUN_8003EC70 (*DAT_80010390, gp+0x934) | Two parallel slots; param + 0x381 argument | per-function dumps |
| Dual-mode loader | FUN_8003E360; FUN_800608F0 / FUN_80060944 | Retail TOC branch vs dev h:\ trap | funcs/8003e360.txt |
| Tiling | archive_tiling_real.rs | Sizes partition LBA 121..59206 exactly | disc-gated test |
Further evidence that the gap is the size, independent of the definition: known-length files agree with it (readef.DAT = 78 × 0x10800, summon.dat = 103 × 0x10800, every .MAP = 0x12000, bse.dat = 2 sectors); every scene-asset table lands at offset 0 of its entry; mid-cast save states hold a stager byte-resident up to the sector gap and stale bytes after it; and PROT 899 is 74 sectors, all its own.
Deep dive
History: the over-read entry size and the coordinates it mislabelled
The expression toc[p+5] - toc[p+3] + 4 is not an entry's size: it expands to size(p+1) + size(p+2) + 4, the two entries that follow p. It overshoots for 931 of 1233 entries and falls short for the rest. Parsing over that window ran into neighbours' sectors. Entry::declared_span_sectors keeps the value for diagnostics only (prot-extract list, column decl_span, flag OVR).
Every constant of the form "asset X at PROT N offset K" measured inside that window names real bytes but, when K ran past entry N's end, the wrong owner. Re-keying changes no byte:
| Was | Is | Why |
|---|---|---|
Kingdom bundle at PROT 0085 / 0244 / 0391 | 0086 / 0245 / 0392 | The 7-asset table is at offset 0 of the next entry, not "at 0x1800 of the prescript entry" |
Battle-form atlases at PROT 1204 0x25804 + k×0x8224, seven, last truncated | PROT 1205 4 + k×0x8224, eight | 0x25800 is 1204's exact length; the window stopped between atlases 6 and 7 |
Title TIM at PROT 0888 0x1AA28 with "duplicates" in 0889 / 0890 | PROT 0890 0x14228, one copy | All three resolve to the same absolute offset |
Other artefacts of the over-read: the "prescript-prefixed asset table" (a neighbour's offset-0 table), inflated overlay self-pointer ratios (PROT 0901 has three, not nine), "shifted copy" claims between 0900 and 0901, and an init.pak detector that demanded 0x30000 bytes of a 75-sector entry. Two checks make a coordinate trustworthy: offset + length fits inside the entry, and the payload's own framing terminates inside it.
Older still: a Python proof-of-concept used start_lba = toc[p+5] - toc[p+2], collapsing ~80% of entries onto the same low-LBA ranges; and "the boot loader transforms the TOC" is falsified - only the index space differs. Catalogue: do-not-re-walk.