At a glance

Where
One PROT.DAT entry per scene: raw TOC index n + 1 for CDNAME #define <scene> n (= extraction entry n - 1). 97 entries.
Size
Always 0x800 bytes (one sector); param records of 4 bytes at +0x14
In RAM
Staged at *(0x1F8003EC) + 0x12000, right after the scene's .MAP; zero-filled when the scene has no .PCH
Marker
No magic - a header algebra (below)
Parser
legaia_asset::scene_v12_table; CLI asset scene-v12 / asset scene-v12-scan
Confidence
Confirmed - header, record shape, one-sector size and the next-entry prescript hold across all 97 entries (disc-gated scene_v12_corpus); loader and consumers traced statically
Used by
field locomotion (trigger block), cutscene routing, door shuffle

What it is

A trigger does not contain its event - it points at one. The scene's MAN (its script-and-data bundle) holds the named event records in partition 2; each trigger names a tile and the partition-2 record to spawn when the player steps there. The .PCH is a patch layer over the .MAP file's trigger block: same directory shape, same record form, a second lookup window the per-step tile check falls back to.

Three scenes have no .PCH (opurud, opkorout, edson - cutscene scenes with no trigger tiles); the loader zero-fills their slot.

on disc .MAP, 36 sectors .PCH, 1 sector event prescript .LZS asset bundle ... BGM raw n+0 n+1 n+2 n+3 n+6 in RAM scene buffer .PCH efect.dat *(0x1F8003EC) +0x12000 +0x12800
The loader stages the .PCH at +0x12000 and efect.dat exactly 0x800 later; the event prescript is the next PROT entry, not part of this file.

Layout

The header is a directory of four sub-tables ("kinds"): for kind k, the body offset is the u16 at +4k+2 and the record count the u16 at +4k+4. Every retail .PCH populates kind 1 only; the empty kinds' offsets pack consecutively past the records.

OffsetSizeFieldMeaning
+0x0002N + 4Directory end-of-table offset
+0x00220x0012Kind-0 offset (teleports; empty in retail)
+0x00420Kind-0 count
+0x00620x0014Kind-1 offset (= the records)
+0x0082paramKind-1 count (0..=192 in retail)
+0x00A2NKind-2 offset (elevation overrides; empty)
+0x00C20Kind-2 count
+0x00E2N + 2Kind-3 offset (region boxes; empty)
+0x01040Kind-3 count + pad
+0x014param × 4recordsKind-1 trigger records
+N .. +N+66empty bodiesThe empty kind-2/3 sub-table bodies, zero on disc
to 0x800paddingZero; the entry ends at 0x800

With N = 4 × param + 22 (the first byte past the records, plus 2), the header always satisfies u16[0] = N + 4, u16[5] = N, u16[7] = N + 2. The detector checks this tie plus the fixed words; across the 1233-entry PROT corpus it matches exactly the 97 .PCH entries with no false positives.

Trigger records

ByteFieldMeaning
+0tile_xTrigger tile column (128-unit field tiles)
+1tile_zTrigger tile row
+2p2_recordMAN partition-2 record spawned on step-on
+3gateAlways 0x01 = "spawn on walk-on"

Records sharing a p2_record are multi-tile strips of one trigger - a gate three tiles wide gets three records. The spawned record's own story-flag gates still apply at spawn time. Example: garmel's table has 12 records, two of them 3-tile strips.

How the game uses it

1
Scene load
The field-asset loader reads .MAP into the scene buffer, then .PCH at +0x12000 (or zero-fills 0x800 bytes), then efect.dat at +0x12800.
loader
2
Scene init
The map initialiser walks the kind-1 records and ORs footprint bit 0x400 into each trigger tile's event-cell word (+0x8000 grid).
scene init
3
Per step
The tile lookup scans the .MAP trigger block first, then falls back to the .PCH window; a hit spawns the partition-2 record.
overlay 0897
4
Event runs
The spawned record is a field-VM script: cutscene, door, story beat (script VM).
engine-vm

Engine side: field_regions ports the lookup; the field locomotion page has the runtime contract.

The next entry: the event-script prescript

Raw TOC n + 2 - directly after the .PCH - is the scene's prescript: [u16 count][u16 offsets[count]] then per-record bytecode. Same shape as the standalone scene_event_scripts format. Parser scene_v12_table::parse_prescript_entry.

  • The records are move-VM stager records ([i16 model_sel][u16 flags][bytecode]), not field-VM scripts; the common 0xFFFF 0x0000 lead is model_sel = -1 (a pivot node), and 0x0008 terminates (move-VM Halt).
  • The field VM installs one by id through the effect-tree stager (move VM). The real per-scene field scripts live in the MAN.
  • All 97 scene blocks have one; count ranges 2..71. Town scenes' record 0 is the fixed 768-byte master ambient stager (scene bundles).
History: "prescript at +0x800 of the same entry, dead in retail"

An earlier reading placed a second copy of the prescript at +0x800 inside the .PCH and called it dead because efect.dat overwrote it. It came from an over-reading PROT entry size that appended the neighbour to every buffer. The entry ends at 0x800; the prescript is the next PROT entry, staged by its own load. See do not re-walk.

How we know

FunctionAddressWhat it provesDump
Field-asset loaderFUN_8001F7C0Stages .MAP, .PCH at +0x12000 (zero-fill 0x800 on miss), efect.dat at +0x12800funcs/8001f7c0.txt
Scene initFUN_8003AEB0, body 0x8003AFA8..0x8003B018Kind-1 walk; footprint bit 0x400 at +0x8000 + (z << 8) + (x << 1)funcs/8003aeb0.txt
Per-step tile lookupFUN_801D5630FUN_801D5AE0 (overlay 0897)Four-kind directory reader; .MAP block first, .PCH window fallback; hit → FUN_8003BDE0(x, z, rec[2], rec[3])funcs/overlay_0897_801d5630.txt
Record spawn + flag gatesFUN_8003BDE0 vs DAT_80085758Partition-2 record spawn with story-flag gatingcutscene
Path suffix pool0x8007B3BC / C4 / CCLiteral .MAP / .PCH / .LZS strings in SCUS_942.54-
Live capturetown01 heap 0x8014B530= scene buffer 0x80139530 + 0x12000scenario manifest
Corpus testscene_v12_corpus.rs97/97 one-sector entries, 97/97 valid next-entry prescript, position law n + 1disc-gated

Deep dive

The scene-transition streamer stages the .LZS, not the .PCH

FUN_80021934 (entry three instructions before the 0x80021940 prologue) is a 5-state streaming actor (state actor+0x1A, jump table 0x80010760) that pre-streams the next scene's asset bundle during the transition fade. It is spawned by the scene-change packet FUN_8001FD44 through the pool spawner FUN_80020DE0, from a 24-byte system-actor descriptor at 0x80070734; all five call sites are in overlay 0897 (field-VM op 0x3F plus four controller sites).

  • Case 0 seeds a 70-frame countdown; cases 1 / 3 poll stream progress.
  • Case 2 (retail, _DAT_8007B8C2 set): streams raw TOC entry DAT_8007B768 + 3 - the destination block's .LZS slot - into _DAT_8007B85C by index.
  • Case 4: builds DATA_FIELD\<scene>.LZS by name, then hands off to mode 2 (main init), whose chain stages .MAP / .PCH / efect as above.

The transition actor touches only n + 3; _DAT_8007B85C never holds the .PCH.

dolk2 and rikuroa: the MAN-less scenes

dolk2 and rikuroa are the two scenes whose own base+3 bundle is the MAN-less count=4 form (types [1, 2, 6, 0x14]). Their scripts come from a standalone data_field_streaming entry in the same block whose type-3 chunk is a plain MAN (dolk2 extraction 70, rikuroa extraction 157); the live script heap at the Caruban beat byte-matches that chunk. Engine: scene_bundle::streaming_man_payloads.

History: extraction entries 0076 and 0164 appeared to embed a 7-asset bundle at 0x1000. The bytes from 0x1000 are byte-identical to extraction 0078 and 0166 - the slice over-read into the next entries. Those windows are suimon's and geremi's .PCH files under the position law.

Naming caveat: extraction filenames are shifted

Extraction filename labels apply CDNAME defines as extraction indices and land one block late. 0093_map01.BIN is garmel's table; Drake map01's is 0084_suimon.BIN (the only one carrying p2 = 0x26, the fly-in record); town01's is 0002_gameover_data.BIN, holding the opening trigger (0x1D, 0x5B, 0x03). See CDNAME numbering.

Reading the parsed structure
use legaia_asset::scene_v12_table;

let buf = std::fs::read("extracted/PROT/0093_map01.BIN")?;
let t = scene_v12_table::detect(&buf).expect("v12 header valid");

println!("N={}, param={}", t.n, t.param);
for (i, rec) in t.records.iter().enumerate() {
    println!("rec[{i}]: b0={:02x} b1={:02x} b2={:02x}",
             rec.b0, rec.b1, rec.b2);
}

Detection: len >= 16; u16[1] == 0x0012, u16[2] == 0, u16[3] == 0x0014, u16[6] == 0; u16[0] == u16[5] + 4; u16[7] == u16[5] + 2; param <= 1024; N == 4 * param + 22.

Open questions
  • The empty sub-table bodies. Whether any runtime writer fills the zero words at +N..+N+4 needs a targeted capture of the +0x12000 window.
  • Empty kinds 0 / 2 / 3. No retail .PCH populates teleports, elevation overrides or region boxes; those live only in the .MAP block.
  • Prescript scope. The next-entry prescript and the .LZS bundle's scripted-table prefix both carry stager records; which each supplies at runtime is unpinned.

Falsified leads, kept so they are not re-walked: the _DAT_8007B8D0 relocation is the efect.dat stager; the type-dispatch loader never sees the .PCH (it is a top-level PROT entry, not a typed chunk); FUN_8002541C is a generic streaming driver.

Source of record: docs/formats/scene-v12-table.md.

See also