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_scene_formats::man_edit(apply_dest_edits,apply_text_edits,rebuild_man,validate)- Confidence
- Confirmed - dispatch path pinned by a live PCSX-Redux trace plus a disc-wide corpus census; every rebuild is re-walked against the original (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.
Highlighted cells are the bytes rebuild_man rewrites. data_region = 0x2B + 3 × total_records is derived and never moves. Fixup 4 lives outside the MAN, in the scene bundle's descriptor word.
| Fixup | Where | Rule |
|---|---|---|
1. 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 |
| 2. 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 |
| 3. Intra-record relative jumps | Ops 0x26 / 0x42 / 0x4D / 0x4E (conditional sub-ops) / 0x70 in the edited record | Recompute the stored 16-bit 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. Deltas are signed: a high-bit delta is a backward jump, such as the 26 FE FF park loop |
| 4. 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:
indexis the destination-scene id handed to the warp packet - not a record selector.name_len+name(a CDNAME symbol) are what make the record variable-length.- The trailer sets where the player appears and which way they face 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.
The table is flat across the three partitions and the runtime resolver indexes it that way, but each partition prefixes its records with a different header, so a record's script start is not a function of the index alone (field VM).
Safety
- No absolute PC exists. The field VM stores no absolute script address anywhere, so a same-record shift preserves every control-flow field. The halfword trailing
0x45 0xC0(camera apply) looks like one but is a camera parameter - the arm passes it to the camera configure call and falls through four bytes - so the editor leaves it alone. - Validate.
man_edit::validatere-parses and re-walks the rebuilt MAN and confirms each edited op still decodes as a0x3Fcarrying the intended name. - Edit the carrier, not the scene name. An edit is keyed to the PROT entry the MAN actually lives in - a scene may have no MAN, a bundle MAN, or a bundle MAN plus a streaming variant. Every carrier is byte-unique, so two names resolving the same MAN is a framing bug, and editing under the wrong one would rewrite a neighbour's sectors.
- 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 0x1F <text> 0x00 run in the script stream - most often a bare segment the actor-dialog state machine parks on, otherwise the inline text of a 0x49 0x00 state-resume or a 0x4C E1 balloon. The decoder walks bare segments and the 0x27..0x2A picker jump tables as strides of the stream, so a record's walk crosses its dialogue, a grown run keeps the fall-through decode in sync, and a picker's i16 entries (each relative to its own offset) are relocated with the jumps. The walk starts at each partition's own script start.
Three invariants keep this safe:
- Record-region gate. Every edit must lie strictly before section 0, inside a partition record. Dialog is field-VM script, never a data section, so an edit that would touch the section chain (whose length prefixes this pass does not fix) 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. - Text-site gate.
man_edit::text_siteclassifies a run by where it sits on the record's walk:Segment(text an instruction carries - dialog),Operand(an instruction spans it, e.g.CC 1F 50 4B 00reads like text but is a menu-control op) orUnreached. Operand runs are refused outright; unreached runs are written same-size only.
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).
The same question for a mesh pack
A scene's TMD pack poses the relocation question in its simplest form, and the answer runs the other way: nothing outside the pack holds a byte offset into it, so growing a member costs a rebuild of the pack entry and nothing else. The worked case is the Nivora Ravine duel scene's NPC mesh pack, whose 112 members tile its declared stream exactly - the first begins where the header ends and the last ends at the declared length, with no gap between any pair.
Every reference from outside is an index. The runtime mesh pool is simply the descriptor walk's registration order, one slot per member; a placement names a pool slot; a scene animation record names a record number; and the scene bundle's own descriptors carry offsets into the bundle, which is a different entry entirely. A byte scan across every entry on the disc agrees and sharpens it: each of the three Delilas members' offsets occurs exactly once, in the pack's own table, and the pack's declared length occurs nowhere outside the header that encodes it.
So the cost of growing a member is: rewrite the offset table from that member onward, rewrite the declared length, and keep the result inside the entry's sector footprint. The same pack carried inside a bundle adds two words in the carrying entry - that descriptor's decompressed size and the header's size sum - and the requirement that the recompressed stream still fits its span. The budget is the one the MAN faces above: retail's scene entries are sector-aligned with no padding, so a member that grows is paid for by another member shrinking.
How we know
Provenance: the VM, the warp packet, the resolver, a live trace and a corpus census
| 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 | PCSX-Redux breakpoint trace of a door transition out of Drake Castle | 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; 37 carry a straddling relative jump |
Full page: docs/formats/man-relocation.md.