Overview

An "entry" (or "slot") is one numbered region of the archive - the unit the game requests when it loads assets. The TOC measures everything in sectors and LBAs (a sector is the disc's 2048-byte block; an LBA is a sector number counting from the start of the file). Two wrinkles make this TOC more interesting than a plain offset table, and both are covered below:

  • Trailing-overlay sectors: for ~24% of entries the game reads past the size the TOC declares - the extra sectors carry real content (e.g. PROT 899's trailing 60 sectors are the title-screen overlay code, a chunk of MIPS code the game loads into RAM on demand).
  • Two index spaces, off by 2: the game's in-RAM copy of the TOC counts entries from a different origin than this project's extraction, so every index recovered from game code must be shifted by 2 - the root cause of a whole family of historical mislabelings.

DMY.DAT is a sibling archive at the disc root that turns out to be developer fixtures (memory-bus test pattern + paired random blobs), not game data; see DMY.DAT. Implementation: crates/prot/src/archive.rs.

Header (8 bytes at offset 0x000 OR 0x800)

u32 file_count_minus_1
u32 header_sectors      // size of TOC in 0x800-byte sectors

The detector tries offset 0x000 first, then 0x800, accepting whichever yields plausible values. PROT.DAT uses 0x000.

TOC (immediately after header)

The TOC is a flat array of u32 words (toc[] below - word 0 is the first u32 after the 8-byte header). p is the 0-based entry index (the extraction index); each entry contributes one start-LBA word at toc[p+2], so the formulas below reach into neighbouring entries' words (toc[p+3] is both entry p's footprint end and entry p+1's start). For entry index p:

start_lba             = toc[p + 2]                       // absolute LBA into PROT.DAT
indexed_size_sectors  = toc[p + 5] - toc[p + 3] + 4      // TOC-declared payload size
footprint_sectors     = toc[p + 3] - toc[p + 2]          // on-disc span to next entry
size_sectors          = max(indexed_size_sectors, footprint_sectors)
byte_offset           = start_lba * 0x800
size_bytes            = size_sectors * 0x800

toc[p+5] is the absolute LBA of entry p+3 (an end-marker that aliases the next-entry's start), so toc[p+5] - toc[p+3] + 4 recovers the indexed size in sectors.

Trailing-overlay sectors

For ~24% of entries the on-disc contiguous range to the next entry's start LBA is larger than the indexed payload - the trailing sectors carry overlay content the SCUS boot loader (SCUS_942.54 is the game's main executable) reads via a multi-sector ReadN past the TOC-claimed end. PROT entry 899 is the canonical example: indexed payload is 14 sectors (28 KiB, the options menu), but the on-disc footprint is 74 sectors - the trailing 60 sectors are the title-screen overlay code (see boot.md). The extractor surfaces these trailing sectors so the on-disc footprint matches what the boot loader actually reads.

Archive::read_entry reads the full footprint; Archive::read_entry_indexed reads only the indexed sub-region. Scene-side parsers were designed for the indexed view and use read_entry_indexed via ProtIndex::entry_bytes; asset-viewer / disc-browser consumers use the full footprint so trailing-overlay content is visible.

In-RAM TOC + the two index spaces

SCUS_942.54 keeps a copy of the TOC at RAM address 0x801C70F0. Used at FUN_8003E8A8 (the LBA resolver; FUN_80xxxxxx names are Ghidra-traced function addresses in the executable):

start_lba    = TABLE[(idx + 2) * 4 + 0x801C70F0]
end_lba      = TABLE[(idx + 3) * 4 + 0x801C70F0]
size_sectors = end_lba - start_lba

The in-RAM copy is raw PROT.DAT from byte 0 - FUN_8003E4E8 reads the first three sectors of PROT.DAT into 0x801C70F0 at boot, header words included (byte-verified against a live save state's RAM). There is no transformation; but the index space differs by 2 from the extraction's: the extraction (crates/prot, and the NNNN in extracted/PROT/NNNN_*.BIN) builds its toc[] array after the two file-header words, so extraction entry p's start_lba sits at file word p + 4, while the resolver's TABLE[(idx + 2)] is file word idx + 2.

The canonical rule: raw TOC index = extraction index + 2. Any PROT index recovered from a FUN_8003E8A8 argument must subtract 2 to land in extraction space (byte-verified for the battle side-band files: raw TOC indices 0x37F/0x380 resolve to extraction entries 893/894, see summon-readef). Raw-TOC entries 0 and 1 cover the pre-init_data boot-UI region (LBA 3..120) that extraction indexing leaves unindexed: two TIM-packs holding the boot-resident system-UI bundle (menu-glyph atlas, sprite sheets, cursor parts; uploaded once at boot by FUN_800198E0 with flat-strip CLUT semantics, parser legaia_asset::system_ui_bundle) - see tim-pack § boot-resident system-UI instance. CDNAME.TXT's #define numbers are authored in this raw-TOC space - the extractor's filename labels are shifted +2 relative to the content the defines name; see cdname § numbering space.

Resolving entries by name vs by index

Two entry points:

  • FUN_8003E8A8 - index-based (consumed directly by the streaming loader and the dev-build sound branch).
  • FUN_8003E6BC - path-based; resolves dev paths like data\battle\efect.dat or h:\PROT\FIELD\<scene>\… into an index via the CDNAME-driven name map, then delegates to the LBA resolver. Most retail-build code paths land here.

Names come from CDNAME.TXT, which lives at the top level of the disc.

Superseded readings
  • toc[p+5] - toc[p+2] computes the SIZE in sectors, not the start LBA. Read as a start LBA it collapses ~80% of PROT entries onto the same few low-LBA byte ranges - anything derived from that formula's outputs is artefacted; trust only start_lba = toc[p+2] extractions.
  • "The boot loader transforms the TOC into a different in-RAM layout" - falsified; the in-RAM copy at 0x801C70F0 is raw PROT.DAT from byte 0, and only the index space differs (by 2).

See also