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
At *(0x1F8003EC) + 0x12000, right after the scene's .MAP - on retail because one 40-sector read covers both
Marker
No magic - a header algebra (below)
Parser
legaia_scene_formats::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; 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 developer build zero-fills their slot; retail simply reads whatever the following sectors hold, because it does not open the file at all.

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 .PCH sits at +0x12000 and the efect.dat pointer 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.

+0x00N+4 (dir end)u16
+0x02kind 0 teleports: 0x12, 04 B
+0x06kind 1 triggers: 0x14, param4 B
+0x0Akind 2 elevation: N, 04 B
+0x0Ekind 3 regions: N+2, 0 + pad6 B
+0x14kind-1 trigger recordsparam × 4
+Nempty kind-2/3 bodies6
zero to 0x800

Kind-1 count param is 0..=192 in retail; the empty kinds' offsets pack consecutively past the records, and their zero-length bodies sit at +N.

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

+0tile_xu8
+1tile_zu8
+2p2_recordu8
+3gate = 0x01u8

tile_x / tile_z address a 128-unit field tile; p2_record is the MAN partition-2 record spawned on step-on; gate is always 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 loadOn retail the field-asset loader issues one 40-sector read from the .MAP entry - 0x14000 bytes, four sectors more than the .MAP - so the .PCH and the prescript arrive with it. The three-file version, with a zero-fill when the .PCH is missing, is the developer-station branch.
  2. Scene initThe map initialiser walks the kind-1 records and ORs footprint bit 0x400 into each trigger tile's event-cell word (+0x8000 grid).
  3. Every stepThe tile lookup (field overlay 0897) scans the .MAP trigger block first, then falls back to the .PCH window; a hit spawns the partition-2 record.
  4. Event runsThe spawned record is a field-VM script: cutscene, door, story beat (script VM).

In the port: parser legaia_scene_formats::scene_v12_table; the lookup is field_regions; 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 reserved][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).

Who writes the window at runtime

Almost nothing. Sweeping the executable and all 83 mapped overlay images, the +0x12000 offset is built six times: twice in the loader's developer arm, once in the scene-init pass that only reads the directory, and three times inside the two-window tile lookup (the field overlay's, plus private copies in the fishing and dance overlays). The lookup's helper is called from nowhere else, so the record pointer it returns is the only other handle anyone has on the window.

Seven routines call that lookup and exactly one writes through what it returns: a field-VM arm - main opcode 0x4C, operand nibbles 8 then 3 - that walks a tile rectangle and, for every tile with an elevation-override record, sets that record's coarse step from its own operand and clears the fine quads. It is how a script re-floors a ramp or a bridge in the middle of a scene.

Two consequences. The header's trailing zero words are never filled at runtime - the writer patches a record the directory already counts and cannot append one, and nothing stores a count or an offset into the directory at all. And the empty kinds stay empty by construction: the one writer needs a record to match, every retail sidecar ships zero elevation records, and teleports and region boxes have no writer in any image.

How we know

Header, record shape, one-sector size and the position law n + 1 hold across all 97 entries; the loader and every consumer are traced from the disassembly.

The routines and the capture
FunctionAddressWhat it provesDump
Field-asset loaderFUN_8001F7C0Retail arm at 0x8001F9A4: resolve the .MAP entry, read 0x28 sectors, return 0x14000. Dev arm: three host opens, zero-fill on a missing .PCHfuncs/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_801D5630 → FUN_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

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_scene_formats::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.

Closed questions and falsified leads
  • The empty sub-table bodies - nothing writes them. The window's whole write surface is the loader plus the one field-VM arm above, which patches a record's body, never the directory.
  • Empty kinds 0 / 2 / 3 - kind 2 has that one writer, kinds 0 and 3 have none in any image, and none can gain a record, so they stay empty by construction.
  • Prescript scope - FUN_800252EC reads its [count][offsets] table from _DAT_8007B8D0 (0x800252F4), which the field asset loader points at the efect.dat window *(0x1F8003EC) + 0x12800 at 0x8001F864 - one sector past the .PCH. A record installed by id comes from the next-entry prescript, never from the .PCH.

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