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 means the record physically changes 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. Engine: legaia_asset::man_edit.
Why resize a record at all?
Nobody edits a shipped game file for fun - two real consumers force the problem:
- The door randomizer. The randomizer's 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. The same machinery generalizes from a door's destination name to any interior byte run: a community 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 on the disc - so a wordier localization only fits when the rewritten MAN recompresses no larger than the original.
The reason a resize is safe at all is structural: the doors are reached at runtime through an offset table the format already exposes (next section), not through absolute addresses scattered in code - fix the table and every door stays addressable.
The destination table is the partition-2 record-offset table
(Terms: the field VM is the interpreter that runs a scene's event bytecode - FUN_801DE840 in Ghidra's naming, where FUN_80xxxxxx labels a traced function by address. 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:
0x3F [i16 index] [u8 name_len] [name bytes] [entry_x] [entry_z] [dir]
The ops are partition-2 MAN records (the named records - see the MAN layout). They are reached at runtime through the partition-2 record-offset table: on a transition the field controller sets the VM bytecode base to man_base + data_region + partition2[slot] and runs that record's small script by fall-through (SysFlag.Set; CFlag.Set; EFFECT; yield; 0x3F; entry-trailer; JmpRel-1 self-loop), whose 0x3F op warps by the inline name. Selection is by stable slot index, so the op's index field is only the destination-scene id passed to the warp packet (FUN_8001FD44), not a record selector.
Pinned by a live PCSX-Redux dispatch trace (autorun_door_dispatch_trace.lua on the drake_castle_to_worldmap capture): the executing op's bytecode base minus the MAN base equalled 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/after any destination op. The consequence: the destination "index" is a structural offset table the MAN parser already exposes, so resizing a record is safe - fix the table and every door stays addressable.
Relocation surface
A mid-buffer insert/delete of delta bytes at a destination op needs these fixups:
- Partition record-offset tables (
MAN[0x2B..], u24LE entries relative todata_region_offset): bump every entry whose record starts after the edit bydelta. This table is the door dispatch index. u24_at_28(header section-0 offset, relative todata_region): the section chain sits after the records, so it shifts by the total delta.- Intra-record relative jumps in the edited record whose source/target straddle the edit (
0x26/0x42/0x4D/0x4E/0x70): the stored u16 delta is recomputed; a jump wholly on one side is unaffected. The delta field sits at the jump's relative base (target - delta), so the rewrite is op-agnostic. (37 of 160 corpus ops have such a jump.) - External descriptor (
scene_asset_table): the MAN's decompressed size is stored only in the scene-bundle descriptor word(type<<24)|size- rewrite it after recompressing.
data_region_offset is derived (0x2B + 3*total_records) and does not move.
Safety
Absolute-reference ops (0x45 0xC0 camera-apply, 0x4E abs-jump) are record-local and can't be relocated blindly; apply_dest_edits errors out if it finds one in an edited record (none exist in the retail corpus), and the caller leaves that scene unchanged. man_edit::validate re-parses + re-walks the rebuilt MAN and confirms each edited op still decodes as a 0x3F carrying the intended name. The recompressed MAN must fit the original asset's on-disc footprint (the gap to the next descriptor); a scene that can't 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.
See also
- Randomizer - the door feature this enables.
- Encounter record - the MAN multi-section layout.
- Field/event script VM - the opcode decoder the jump fixup uses.