Walk-on trigger sidecar (.PCH) Confirmed
Walk onto the right tile and something happens: a cutscene starts, a door leads out of town, the
story moves. "When the player steps on tile (x, z), fire event N" is what this small file encodes, one 4-byte
line per tile. Every scene can carry one - the developers called it DATA\FIELD\<scene>.PCH - and
it is exactly one 2048-byte sector in every one of the 97 cases. Internally it is scene_v12_table.
At a glance
- Where
- One PROT.DAT entry per scene: raw TOC index
n + 1for CDNAME#define <scene> n(= extraction entryn - 1). 97 entries. - Size
- Always
0x800bytes (one sector);paramrecords 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; CLIasset 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.
.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.
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0x000 | 2 | N + 4 | Directory end-of-table offset |
+0x002 | 2 | 0x0012 | Kind-0 offset (teleports; empty in retail) |
+0x004 | 2 | 0 | Kind-0 count |
+0x006 | 2 | 0x0014 | Kind-1 offset (= the records) |
+0x008 | 2 | param | Kind-1 count (0..=192 in retail) |
+0x00A | 2 | N | Kind-2 offset (elevation overrides; empty) |
+0x00C | 2 | 0 | Kind-2 count |
+0x00E | 2 | N + 2 | Kind-3 offset (region boxes; empty) |
+0x010 | 4 | 0 | Kind-3 count + pad |
+0x014 | param × 4 | records | Kind-1 trigger records |
+N .. +N+6 | 6 | empty bodies | The empty kind-2/3 sub-table bodies, zero on disc |
to 0x800 | padding | Zero; the entry ends at 0x800 |
Header algebra
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
| Byte | Field | Meaning |
|---|---|---|
+0 | tile_x | Trigger tile column (128-unit field tiles) |
+1 | tile_z | Trigger tile row |
+2 | p2_record | MAN partition-2 record spawned on step-on |
+3 | gate | 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
.MAP into the scene buffer, then .PCH at +0x12000 (or zero-fills 0x800 bytes), then efect.dat at +0x12800.0x400 into each trigger tile's event-cell word (+0x8000 grid)..MAP trigger block first, then falls back to the .PCH window; a hit spawns the partition-2 record.engine-vmEngine 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 common0xFFFF 0x0000lead ismodel_sel = -1(a pivot node), and0x0008terminates (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;
countranges 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
| Function | Address | What it proves | Dump |
|---|---|---|---|
| Field-asset loader | FUN_8001F7C0 | Stages .MAP, .PCH at +0x12000 (zero-fill 0x800 on miss), efect.dat at +0x12800 | funcs/8001f7c0.txt |
| Scene init | FUN_8003AEB0, body 0x8003AFA8..0x8003B018 | Kind-1 walk; footprint bit 0x400 at +0x8000 + (z << 8) + (x << 1) | funcs/8003aeb0.txt |
| Per-step tile lookup | FUN_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 gates | FUN_8003BDE0 vs DAT_80085758 | Partition-2 record spawn with story-flag gating | cutscene |
| Path suffix pool | 0x8007B3BC / C4 / CC | Literal .MAP / .PCH / .LZS strings in SCUS_942.54 | - |
| Live capture | town01 heap 0x8014B530 | = scene buffer 0x80139530 + 0x12000 | scenario manifest |
| Corpus test | scene_v12_corpus.rs | 97/97 one-sector entries, 97/97 valid next-entry prescript, position law n + 1 | disc-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_8007B8C2set): streams raw TOC entryDAT_8007B768 + 3- the destination block's.LZSslot - into_DAT_8007B85Cby index. - Case 4: builds
DATA_FIELD\<scene>.LZSby 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+4needs a targeted capture of the+0x12000window. - Empty kinds 0 / 2 / 3. No retail
.PCHpopulates teleports, elevation overrides or region boxes; those live only in the.MAPblock. - Prescript scope. The next-entry prescript and the
.LZSbundle'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.