Tier C - rewriting field-VM bytecode
What is in a chest, what a shop sells, which monsters a zone throws at you, which house door leads where: none of it is stored as data. Each is an operand inside the scene's script - the bytecode the game's field VM runs when you walk into a town. Editing it means treating the script as code: decode it, rewrite one instruction's argument, recompress into the original footprint.
At a glance
- Target
- An opcode's inline operand in a scene's MAN (the per-scene script-and-data bundle, LZS-compressed)
- Find
- Opcode-aware, dialogue-skipping walk with the field-VM disassembler (
legaia_asset::field_disasm) - Write
- Same-size operand overwrite → recompress → must fit the footprint budget
- Budget
- Distance from the MAN's
data_offsetto the next descriptor's - read from the boundary, never from a rewritten stream - Mods
--chests·--shops·--encounters·--house-doors- Confidence
- Confirmed - opcode encodings from the dispatcher; oracles re-decode every patched scene
- Oracles
chest_patch_real.rs·shop_patch_real.rs·encounter_patch_real.rs·house_door_patch_real.rs
Finding the operand
Field-VM bytecode is variable-width and interleaved with dialogue: runs of glyph segments introduced by 0x1F that are not opcodes. A chest's give almost always sits after the dialogue that announces it. The finder therefore:
- starts each record at its true entry point, from the MAN's record-offset table;
- decodes opcode by opcode with the disassembler;
- treats a decode error at a
0x1Fas a dialogue segment - skips to its terminating0x00and resumes; - stops on any other decode error, and never runs past the next record's start.
A chest carries two bytes that must move together: the GIVE_ITEM operand that grants the item, and a separate 0xC2 id dialogue token that renders the item's name in the announcement. Patch only the give and the text still names the old item.
| Opcode | Meaning | What the mod rewrites |
|---|---|---|
0x39 id | give item | the id, plus the paired 0xC2 id display token |
0x49 sub-op 0 | shop stock list | the inline item ids (the unsellable template tail is not stock) |
| encounter record | formation monster ids | the id bytes; count and reserved bytes preserved |
0xA3 0xF8 | intra-town warp | player-warp operands, class-preserving across IN/OUT records |
The footprint budget
After the edit the MAN is recompressed, and the new stream must still fit where the old one lived. The budget is read from the descriptor boundary, not from the just-written stream. Several passes (encounter, chest, shop) re-pack the same MAN; the project's packer is sometimes a touch tighter than the previous pass, so a budget read back from a shorter stream would shrink each time and a later pass would overflow a scene it should have edited.
Composition
Tier-C edits are same-size at the operand level, but several target the same scene. They compose under one rule: every pass reads the footprint budget from the descriptor boundary. The variable-length cousin - an edit that must change a byte count - is tier D.
How we know
| Claim | Evidence | Reference |
|---|---|---|
| Operand encodings | Field-VM dispatcher FUN_801DE840 (43 opcodes), overlay-resident | script-vm |
| Shop stock is a script operand, not a table | Op 0x49 sub-op 0 carries the ids; prices come from the SCUS item record | shop |
| Encounter record shape | Reader at FUN_801DA51C: [3 reserved][count][ids] | encounter |
| Chest give follows the dialogue | Disassembly of every chest site; the display token and the give are distinct bytes | chest_patch_real.rs |
History: the walk that stopped at the first dialogue
A finder that halts at the first 0x1F misses the give at most chest sites, silently and without error - the give sits after the announcement. Resuming after each dialogue segment keeps the inter-segment control bytes in sync and reaches the real give. A byte scan for 0x39 fails the other way: it matches glyphs and operands.
Details: adding a tier-C mod
- Find sites with an opcode-aware walk bounded to each record - never a raw byte scan.
- Patch any paired display token alongside the operand.
- Recompress, reading the budget from the descriptor boundary; skip scenes that overflow.
- Add a disc-gated oracle that re-decodes the scene.