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
- 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; 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; 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.
.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.
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.
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
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
- Scene loadOn retail the field-asset loader issues one 40-sector read from the
.MAPentry -0x14000bytes, four sectors more than the.MAP- so the.PCHand the prescript arrive with it. The three-file version, with a zero-fill when the.PCHis missing, is the developer-station branch. - Scene initThe map initialiser walks the kind-1 records and ORs footprint bit
0x400into each trigger tile's event-cell word (+0x8000grid). - Every stepThe tile lookup (field overlay 0897) scans the
.MAPtrigger block first, then falls back to the.PCHwindow; a hit spawns the partition-2 record. - 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 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).
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
| Function | Address | What it proves | Dump |
|---|---|---|---|
| Field-asset loader | FUN_8001F7C0 | Retail arm at 0x8001F9A4: resolve the .MAP entry, read 0x28 sectors, return 0x14000. Dev arm: three host opens, zero-fill on a missing .PCH | 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 |
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_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_800252ECreads its[count][offsets]table from_DAT_8007B8D0(0x800252F4), which the field asset loader points at theefect.datwindow*(0x1F8003EC) + 0x12800at0x8001F864- 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.