At a glance

Where
Eight PROT.DAT entries (extraction names): 0002_gameover_data, 0003 / 0004 / 0005_town01, 0020_town0b, 0021 / 0022 / 0023_town0c
Magic
0x01059B84 - raw in only four of the eight carriers, referenced by no code
Size / stride
Schema: 97 × u32 (388 bytes), slots 0x60..0x16651; block ~91 KB
Parser
legaia_asset::field_pack::FieldPack + CANONICAL_SCHEMA; CLI asset field-pack / field-pack-scan
Confidence
Confirmed as a detector and for the constant-block finding (corpus byte scan, disc-gated field_pack_real.rs); per-slot meaning is Inferred from size clustering only
Used by
scene bundles, asset loader, world viewer

Layout

preamble (per scene) magic schema 388 B ~91 KB constant block TIMs / TMDs (some) 0 - 234 KB 4 of 8 identical everywhere 0 - 250 KB
Solid outline = per-scene content; dashed = a shared template the build tool stamped into each carrier. 0005_town01 and 0023_town0c are the template alone: block at offset 0, no preamble, no assets.
RegionSizeContentVaries?
preamble0 - 234 KBu16 count + ascending u16 offsets + records: the scene_event_scripts prescript shapeyes
magic40x01059B84, in 4 of 8 carriersstamp only
schema38897 × u32 slot offsets; slots[0] = 0x60, slots[96] = 0x16651; MD5 edcfdf15… everywhereno
block~91 KBThe slots' target; hash c85d6a44… in every full-length carrierno
assets0 - 250 KBPacked TIMs / TMDs, in some filesyes

The eight carriers

PROTmagic?preambleblockTIMs / TMDs
0002_gameover_datayes234 KB5.7 KB (truncated)2 TIMs + TMDs
0003_town01yes233 KBfull5 TIMs + 2 TMDs
0004_town01yes227 KBfull5 TIMs + 1 TMD
0005_town01yes0fullnone
0020_town0bno222 KB5.8 KB (truncated)-
0021_town0cno222 KBfull-
0022_town0cno213 KBfull-
0023_town0cno0full-
  • The block is a global constant. A region byte-identical across unrelated scenes is a shared template, not the scene's own field data.
  • The magic is decorative. The same block ships stamped in town01 and unstamped in town0c, and no code references the word. 0005 / 0023 are the same template-only entry, one stamped and one not.
  • The preamble is the scene. In 0003_town01 it opens with 63 records: record 0 a fixed 768-byte dispatch table, records 1.. word-aligned actor / event stager records (0xFFFF 0x0000 lead, 0x0008 terminator) - not field-VM bytecode. Field files that carry no block at all open with the same prescript.
History: "124 entries, slots filled per scene"

An earlier reading had 124 PROT entries sharing the schema with the preamble filling the slots per scene. It does not survive a byte scan of the corpus: the slots are a fixed template, there is nothing per-scene to project into them, and the per-scene structure is the already-parsed prescript. See do not re-walk.

What the slots look like

Because the schema is identical everywhere, slots with the same slot[i+1] - slot[i] are the same kind of record. Three uniform-stride runs dominate - the shape a field scene uses for NPC slots, event triggers and hit regions - and five 8 KB slots match the TIM count in the town01 files. asset field-pack <PATH> --groups prints the clusters; SlotKind in the parser is derived from them.

Slot sizeCountSlotsLikely kind
0x208851, 2, 3, 30, 41TIM-page-sized blobs
0x218215..25NPC-record array
0x11017-Dialog-trigger / event-region array
0x9016-Collision-box-sized array
0x21012-Smaller records
0x41064, 32, 44, 45, 61, 66Medium records
0x1010242, 43Medium blobs
0x110Single-byte type / flag marker
other1726, 29, 35, 54, 56, 57, 65, 67, 70, 72, 80, 82, 83, 89, 91, 94 (+ slot 96 open-ended)Singletons (0x100..0x810)

How it reaches RAM

No loader checks the magic. A scene change copies the new name into the scene-name table and raises a pending-transition bit; the field overlay's transition orchestrator then calls the field-asset loader, which streams the file and dispatches its descriptors by type.

1
Scene change
New name into 0x80084548, previous into 0x80084558, bit 0x40 in _DAT_1F800394.
SCUS
2
Orchestrator
Calls the loader with the scene buffer *(0x1F8003EC), the name table and the scene index.
overlay 0897
3
Field-asset loader
Loads DATA\FIELD\<scene> by path; loads efect.dat at buffer + 0x12800 and records that address in _DAT_8007B8D0.
SCUS
4
Descriptor walk
Iterates the static descriptor table at _DAT_8007B85C and dispatches each entry through the asset-type dispatcher.
SCUS

The 97 slots are consumed at static offsets - there is no slot-iteration loop in any captured code. The consumer treats the buffer as a fixed in-RAM template and reads by hard-coded index; the specific per-NPC / per-event handlers need a scene-init execution trace to pin.

The field-file RAM base is _DAT_8007B8D0 - 0x12800 in any save (capture_observations::field_pack_load::recover_base): 0x80139530 in a town01 capture, 0x800A25F0 in a town0c one.

How we know

Function / evidenceAddressWhat it provesDump
Magic reference scanSCUS_942.54 + every captured overlayZero hits for the LUI+ADDIU/ORI pair or bytes 84 9B 05 01: the runtime never compares the magicghidra/scripts/find_field_pack_magic.py, find_field_pack_consumers.py
Scene-change packetFUN_8001FD44Name-table writes and pending bit; called directly by dialog handlers such as FUN_801D1344funcs/8001fd44.txt
Transition orchestratorFUN_801D6704 (call at 0x801D6AE8, walker call at 0x801D6B0C)Loader arguments; descriptor walk after loadfuncs/overlay_0897_801d6704.txt
Field-asset loaderFUN_8001F7C0FUN_8003E6BCPath build, efect.dat at +0x12800, _DAT_8007B8D0 storefuncs/8001f7c0.txt
Descriptor walker + dispatcherFUN_80020224FUN_8001F05CTable at _DAT_8007B85C = 0x8015CBD0, static across savesfuncs/80020224.txt
Corpus scanfield_pack_real.rs4 stamped / 8 schema carriers; block hash identical across town01 and town0cdisc-gated test
Save diffsreal_saves.rsActive scene label + RAM base; town01 vs town0c heap deltascrates/mednafen/tests

Deep dive

Loader order of operations (mid-transition capture)

A save captured between town01 and town0c shows, at once: the bundle pool at 0x80084540 already naming the destination; _DAT_8007B8D0 still holding the previous scene's value; the destination region partially populated at its canonical base; the previous region zeroed; the descriptor table bit-identical. So the loader (1) writes the new name, (2) zeroes the previous region, (3) populates the destination, (4) flips _DAT_8007B8D0 last. field_pack_intra_transition::detect_mid_transition(main_ram) detects the in-flight state by that disagreement.

Save-state diff observations
RegionBytes changedInterpretation
0x800C505C..0x80139527~402 KBShared scene-asset pool; ends just before town01's field-file base
0x801853F5..0x801B93D0~205 KBHeap-resident sibling region
0x8015CBD0..0x80184C89~152 KBDescriptor table contents
0x80098900..0x800BE5FC~132 KBOther heap-resident scene buffers
0x80084140..0x80084398526 BScene-bundle metadata
0x801F3488..0x801F69D87.6 KBScratch just before the 9 KB slot-B overlay window

The 9 KB MIPS overlay at 0x801F69D8..0x801F8F02 (the slot-B stager window, overlay pointer table) is identical between the two town captures - both are town-resident.

History: "runtime layout differs from on-disc schema"

Reading a town capture at base + 0x60, where slot 0 was assumed to sit, yields GP0 GPU primitive packets, and an earlier section concluded the loader transforms preamble bytes into the slots. The premise fails twice: the block is a disc-side constant, and base is the field-file buffer while the loader places the field-asset region at buffer + 0x12000, so base + 0x60 is the file's own primitive region. Not worth re-walking without a mid-load capture.

CLI and Rust API
asset field-pack <PATH>                # show schema + slot sizes
asset field-pack <PATH> --all-slots    # all 97 slot offsets/sizes
asset field-pack <PATH> --groups       # cluster slots by size
asset field-pack-scan <DIR>            # find every field-pack in a PROT dir
use legaia_asset::field_pack::{CANONICAL_SCHEMA, canonical_slot, iter_canonical_slots};
assert_eq!(CANONICAL_SCHEMA[0], 0x60);
let (off, size) = canonical_slot(5).unwrap();          // size is None for slot 96
for (i, kind, off, size) in iter_canonical_slots() { /* SlotKind per cluster */ }
// On a parsed file: fp.slot_kind(i), fp.iter_slots(buf)
// (bytes are non-empty only when the block sits at offset 0, e.g. 0005_town01).

Source of record: docs/formats/field-pack.md.

See also