MAN relocation Confirmed
Every town and dungeon ships a MAN - the per-scene data file that carries, among other things, the scene's event scripts, including its doors. A door is a tiny script op (0x3F) that names its destination scene inline, as a length-prefixed string - so re-pointing a door at a scene with a longer or shorter name physically changes the record's size, and every byte-offset stored elsewhere in the file that points past the edit goes stale. This page is how to resize such a record and rebuild the MAN so every internal offset stays valid.
At a glance
- Magic
- none - operates on a decompressed scene MAN (header, three partition record-offset tables, record region, then the section chain)
- Where on disc
- Every scene bundle's LZS-compressed MAN, described by the scene_asset_table descriptor word
(type<<24)|size - Stride
- u24LE record-offset entries from
0x2B; the data region starts at0x2B + 3 × total_records - Parser / editor
legaia_asset::man_edit(apply_dest_edits,apply_text_edits,rebuild_man,validate)- Confidence
- Confirmed - dispatch path pinned by a live PCSX-Redux trace; disc-wide corpus census; round-trip backstop re-walks every record (how we know)
- Used by
- Door randomizer, Translation packs
Why resize a record at all?
Two consumers force the problem:
- The door randomizer. Its door shuffle re-points a scene exit at a different destination. Because the destination is an inline name, swapping
town01for a longer scene name grows the record; without the fixups below, every record after it - and the tables that index them - would decode as garbage. - Translation. A language pack's dialog line that runs longer than the English original grows its record the same way. Growth is budgeted by the MAN's on-disc footprint: the recompressed file must still fit its original slot.
A resize is safe because the doors are reached at runtime through an offset table the format already exposes, not through absolute addresses scattered in code. Fix the table and every door stays addressable.
Relocation surface
A mid-buffer insert or delete of delta bytes at a record needs four fixups. Everything before the edit stays put; everything after it shifts.
rebuild_man applies after a record changes size. Highlighted boxes are the bytes that are rewritten.| Fixup | Where | Rule |
|---|---|---|
u24_at_28 | MAN[0x28..0x2B], u24LE, relative to data_region | The section chain sits after the records, so it shifts by the total delta |
| Partition record-offset tables | MAN[0x2B..], u24LE entries relative to data_region | Bump every entry whose record starts after the edit by delta. This table is the door dispatch index |
| Intra-record relative jumps | Ops 0x26 / 0x42 / 0x4D / 0x4E / 0x70 in the edited record | Recompute the stored u16 delta when source and target straddle the edit; a jump wholly on one side is unaffected. The delta field sits at the jump's relative base, so the rewrite is op-agnostic |
| External descriptor | scene_asset_table word (type<<24)|size | The decompressed size lives only here - rewrite it after recompressing |
How a door is reached: the partition-2 table
The field VM is the interpreter that runs a scene's event bytecode. A MAN's records are grouped into three partitions; partition 2 holds the named event records, doors included. A 0x3F op carries its destination inline:
| Offset | Size | Field | Meaning |
|---|---|---|---|
+0 | u8 | opcode | 0x3F - named scene change |
+1 | i16 | index | Destination-scene id passed to the warp packet - not a record selector |
+3 | u8 | name_len | Length of the inline scene name - the field that makes the record variable-length |
+4 | name_len | name | Destination scene name (CDNAME symbol) |
| after name | 3 fields | entry_x, entry_z, dir | Where the player appears and faces on arrival |
On a transition the field controller sets the VM bytecode base to man_base + data_region + partition2[slot] and runs that record by fall-through - flag sets, an effect, a yield, the 0x3F warp, its entry trailer, then a self-loop. Selection is by stable slot index, so the offset table is the door index and resizing a record is safe once the table is fixed.
Safety
- Absolute-reference ops (
0x45 0xC0camera-apply,0x4Eabs-jump) are record-local and cannot be relocated blindly.apply_dest_editserrors out if it finds one in an edited record - none exist in the retail corpus - and the caller leaves that scene unchanged. - Validate.
man_edit::validatere-parses and re-walks the rebuilt MAN and confirms each edited op still decodes as a0x3Fcarrying the intended name. - Footprint. The recompressed MAN must fit the original asset's on-disc footprint (the gap to the next descriptor). A scene that cannot grow in place - the big overworld hubs, whose next asset is flush after the MAN - is skipped and reported rather than relocating the whole bundle.
Generalized interior-text growth
The same machinery generalizes from a door's destination name to any interior byte run - the dialog text a localization grows or shrinks. man_edit::apply_text_edits takes a set of TextEdit { offset, old_len, new_bytes } and rebuilds the MAN through the identical rebuild_man path. A dialog segment is a field-VM 0x49 0x00 message op whose text is framed 0x1F <text> 0x00; the decoder recovers the op's width by walking to the <= 0x1E terminator, so a grown run keeps the fall-through decode in sync.
Two invariants keep this safe:
- Record-region gate. Every edit must lie strictly before section 0, inside a partition record with no absolute-reference op. Dialog is field-VM script, never a data section, so an edit that would touch the section chain or an abs-jump / camera-apply target is refused.
- Round-trip backstop.
man_edit::text_edits_preserve_scripts(original, rebuilt)re-walks every record in both buffers and requires an identical instruction stream - same opcodes in order, every control-flow target resolving to the same instruction ordinal. A mis-relocated jump is caught; the caller drops the growth and falls back to same-size abbreviation for that scene.
Budget = the MAN's on-disc footprint. Across the retail USA disc the scene-bundle entries are sector-aligned with zero padding and each compressed MAN already fills its footprint, so in-place growth fits only when the rewritten MAN recompresses no larger than the original - a small fraction of scenes. The rest are sector-crossers whose deficit is under one 2048-byte sector; supplying it is a disc-level relayout, which is what the PAL discs did at mastering (disc relayout, translation tooling).
How we know
| Evidence | Address / source | What it proves |
|---|---|---|
| Field VM | FUN_801DE840 | The interpreter that executes the door record by fall-through; 0x3F is the named scene change |
| Warp packet | FUN_8001FD44 | Receives the op's index as a destination-scene id, not as a record selector |
| Runtime record resolver | FUN_8003C8F0 | Indexes records by partition slot through the record-offset tables |
| Live dispatch trace | autorun_door_dispatch_trace.lua on the drake_castle_to_worldmap capture | The executing op's bytecode base minus the MAN base equals data_region + partition2[0] exactly |
| Corpus census | clean partition walk, disc-wide | 160 destination ops across 48 scenes, 153 in partition 2; zero absolute-reference ops at or after any destination op; 37 ops carry a straddling relative jump |
| Round-trip tests | crates/patcher/tests/*_real.rs (doors), translation import | Rebuilt MANs re-walk to identical instruction streams; touched sectors stay EDC/ECC-valid |
Full page: docs/formats/man-relocation.md.