Field-pack format Confirmed
Rim Elm is the first town a player walks around, and the disc entries that carry it are unusual: they open with a scene-shaped script table, then a magic number, then a 97-slot offset table (the "schema") and the ~91 KB block it indexes. That block is byte-identical in every file that carries it - a shared template, not per-scene data - and no game code ever checks the magic. The real per-scene payload is the preamble in front.
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; CLIasset 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
0005_town01 and 0023_town0c are the template alone: block at offset 0, no preamble, no assets.| Region | Size | Content | Varies? |
|---|---|---|---|
| preamble | 0 - 234 KB | u16 count + ascending u16 offsets + records: the scene_event_scripts prescript shape | yes |
| magic | 4 | 0x01059B84, in 4 of 8 carriers | stamp only |
| schema | 388 | 97 × u32 slot offsets; slots[0] = 0x60, slots[96] = 0x16651; MD5 edcfdf15… everywhere | no |
| block | ~91 KB | The slots' target; hash c85d6a44… in every full-length carrier | no |
| assets | 0 - 250 KB | Packed TIMs / TMDs, in some files | yes |
The eight carriers
| PROT | magic? | preamble | block | TIMs / TMDs |
|---|---|---|---|---|
0002_gameover_data | yes | 234 KB | 5.7 KB (truncated) | 2 TIMs + TMDs |
0003_town01 | yes | 233 KB | full | 5 TIMs + 2 TMDs |
0004_town01 | yes | 227 KB | full | 5 TIMs + 1 TMD |
0005_town01 | yes | 0 | full | none |
0020_town0b | no | 222 KB | 5.8 KB (truncated) | - |
0021_town0c | no | 222 KB | full | - |
0022_town0c | no | 213 KB | full | - |
0023_town0c | no | 0 | full | - |
- 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/0023are the same template-only entry, one stamped and one not. - The preamble is the scene. In
0003_town01it opens with 63 records: record 0 a fixed 768-byte dispatch table, records 1.. word-aligned actor / event stager records (0xFFFF 0x0000lead,0x0008terminator) - 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 size | Count | Slots | Likely kind |
|---|---|---|---|
0x2088 | 5 | 1, 2, 3, 30, 41 | TIM-page-sized blobs |
0x218 | 21 | 5..25 | NPC-record array |
0x110 | 17 | - | Dialog-trigger / event-region array |
0x90 | 16 | - | Collision-box-sized array |
0x210 | 12 | - | Smaller records |
0x410 | 6 | 4, 32, 44, 45, 61, 66 | Medium records |
0x1010 | 2 | 42, 43 | Medium blobs |
0x1 | 1 | 0 | Single-byte type / flag marker |
| other | 17 | 26, 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.
0x80084548, previous into 0x80084558, bit 0x40 in _DAT_1F800394.*(0x1F8003EC), the name table and the scene index.DATA\FIELD\<scene> by path; loads efect.dat at buffer + 0x12800 and records that address in _DAT_8007B8D0._DAT_8007B85C and dispatches each entry through the asset-type dispatcher.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 / evidence | Address | What it proves | Dump |
|---|---|---|---|
| Magic reference scan | SCUS_942.54 + every captured overlay | Zero hits for the LUI+ADDIU/ORI pair or bytes 84 9B 05 01: the runtime never compares the magic | ghidra/scripts/find_field_pack_magic.py, find_field_pack_consumers.py |
| Scene-change packet | FUN_8001FD44 | Name-table writes and pending bit; called directly by dialog handlers such as FUN_801D1344 | funcs/8001fd44.txt |
| Transition orchestrator | FUN_801D6704 (call at 0x801D6AE8, walker call at 0x801D6B0C) | Loader arguments; descriptor walk after load | funcs/overlay_0897_801d6704.txt |
| Field-asset loader | FUN_8001F7C0 → FUN_8003E6BC | Path build, efect.dat at +0x12800, _DAT_8007B8D0 store | funcs/8001f7c0.txt |
| Descriptor walker + dispatcher | FUN_80020224 → FUN_8001F05C | Table at _DAT_8007B85C = 0x8015CBD0, static across saves | funcs/80020224.txt |
| Corpus scan | field_pack_real.rs | 4 stamped / 8 schema carriers; block hash identical across town01 and town0c | disc-gated test |
| Save diffs | real_saves.rs | Active scene label + RAM base; town01 vs town0c heap deltas | crates/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
| Region | Bytes changed | Interpretation |
|---|---|---|
0x800C505C..0x80139527 | ~402 KB | Shared scene-asset pool; ends just before town01's field-file base |
0x801853F5..0x801B93D0 | ~205 KB | Heap-resident sibling region |
0x8015CBD0..0x80184C89 | ~152 KB | Descriptor table contents |
0x80098900..0x800BE5FC | ~132 KB | Other heap-resident scene buffers |
0x80084140..0x80084398 | 526 B | Scene-bundle metadata |
0x801F3488..0x801F69D8 | 7.6 KB | Scratch 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.