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 at 0x2B + 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 town01 for 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.

0x00 0x28 0x2B data_region header 40 bytes u24_at_28 section 0 partition tables u24 x records records before unchanged edited record + delta records after shift + delta section chain shift + delta 1. section-0 offset += total delta 2. table entries after edit += delta 3. straddling jumps recompute u16 delta 4. descriptor word size in scene bundle data_region = 0x2B + 3 x total_records - derived, never moves. Fixup 4 lives outside the MAN: the scene_asset_table word (type<<24)|size is rewritten after recompressing.
The four fixups rebuild_man applies after a record changes size. Highlighted boxes are the bytes that are rewritten.
FixupWhereRule
u24_at_28MAN[0x28..0x2B], u24LE, relative to data_regionThe section chain sits after the records, so it shifts by the total delta
Partition record-offset tablesMAN[0x2B..], u24LE entries relative to data_regionBump every entry whose record starts after the edit by delta. This table is the door dispatch index
Intra-record relative jumpsOps 0x26 / 0x42 / 0x4D / 0x4E / 0x70 in the edited recordRecompute 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 descriptorscene_asset_table word (type<<24)|sizeThe 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:

OffsetSizeFieldMeaning
+0u8opcode0x3F - named scene change
+1i16indexDestination-scene id passed to the warp packet - not a record selector
+3u8name_lenLength of the inline scene name - the field that makes the record variable-length
+4name_lennameDestination scene name (CDNAME symbol)
after name3 fieldsentry_x, entry_z, dirWhere 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 0xC0 camera-apply, 0x4E abs-jump) are record-local and cannot 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.
  • Validate. man_edit::validate re-parses and re-walks the rebuilt MAN and confirms each edited op still decodes as a 0x3F carrying 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:

  1. 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.
  2. 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

EvidenceAddress / sourceWhat it proves
Field VMFUN_801DE840The interpreter that executes the door record by fall-through; 0x3F is the named scene change
Warp packetFUN_8001FD44Receives the op's index as a destination-scene id, not as a record selector
Runtime record resolverFUN_8003C8F0Indexes records by partition slot through the record-offset tables
Live dispatch traceautorun_door_dispatch_trace.lua on the drake_castle_to_worldmap captureThe executing op's bytecode base minus the MAN base equals data_region + partition2[0] exactly
Corpus censusclean partition walk, disc-wide160 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 testscrates/patcher/tests/*_real.rs (doors), translation importRebuilt MANs re-walk to identical instruction streams; touched sectors stay EDC/ECC-valid

Full page: docs/formats/man-relocation.md.

See also