Move VM: the 0x2F overlay extension
The move VM’s opcode 0x2F hands the instruction to a second dispatcher that lives in the field overlay rather than the main executable. Its 61 sub-opcodes are the vocabulary field effects need and battle effects do not: player-relative tests, story-flag branches, shared scratch slots, self-modifying bytecode, curve motion and colour ramps. Every field scene’s ambient effect tree is written in it.
At a glance
- Dispatcher
FUN_801D362C(actor, op)in PROT 0897, 1293 instructions- Jump table
0x801CE868, 61 entries (file+0x50)- Sub-opcode
op[1], range0x00..0x3C- Only caller
- The
jalat0x80023AE0, the0x2Farm of the move VM - Return value
- Instruction width in halfwords, through the shared epilogue
0x801D4A3C - Shared state
- Scratch table
0x801F3498, predicateDAT_801F22F4, cycle counterDAT_801F22F6, flag bankDAT_80085758 - Port
engine-vm::move_vm::ext(dispatch),move_vm_overlay_ext::canonical_size(widths),move_ext_strip
Dispatch
0x2F is only executable while the field overlay is resident: in any other overlay generation the same address holds unrelated bytes. Battle-side move records therefore cannot use it.- No out-of-bounds jump exists. The sub-opcode is loaded sign-extended and compared unsigned, so a negative value reads as huge and is rejected with a width of 1. That matters because the bytecode is writable by the program itself.
- One copy, one caller. The six capture-derived dumps of this address disassemble byte-identically to the static one: it is one function seen under different scenario labels, not a per-overlay family. A five-form reference scan over the executable, every overlay image and every raw archive entry finds one reference to it.
- Read each width from the disassembly. The decompiled C hides it; each arm loads its own return value before the shared epilogue.
Sub-opcode families
Operands start at op[2]. Width is the fall-through width in halfwords.
| Sub-ops | Width | Family |
|---|---|---|
0x04 0x05 0x1B 0x1E 0x30 0x3A 0x3B | 3..5 | Self-modifying bytecode: write a world position, a random value, a copy, an increment, the angle to the player or a party member’s position into later operands |
0x06 / 0x07 | 7 | Branch when the player is outside / inside a box |
0x38 / 0x39 | 4 | Branch when the actor is outside / inside a radius of the player |
0x36 / 0x37 | 4 | Branch on the live actor count |
0x08..0x0B | 2..3 | Set, clear and branch on the global predicate |
0x13 0x14 0x1C 0x1D | 3..4 | Branch on, set and clear a flag in the system flag bank |
0x0C..0x0E, 0x12 | 3..11 | Curve parameter and quadratic Bezier position |
0x23 0x24 0x2A | 6..8 | Position lerps: toward operand coordinates, the map origin, or the player |
0x25..0x28, 0x31..0x35 | 3..5 | Save / load pairs over the shared scratch table |
0x0F..0x11 | 2 | Cycle counter: clear, wrap mod 16 and latch, save coordinates by it |
0x17..0x1A | 5..8 | Object-effect parameter table: write, reset, add, yaw seat |
0x1F / 0x20 | 5 | HSV ramp on one of the actor’s two colours |
0x2B..0x2D | 6..7 | Scanline strip: set the slab, emit, add to the slab |
0x2E | 13 | Build a draw-mode packet and link it into the ordering table |
0x3C | 6 | Screen fade colour, immediate or ramped |
0x00 | 16 | No side effect: a bare jump to the epilogue. The widest arm |
0x01 | 2 | Debug print of the world position |
The random draws in 0x05 and 0x30 are the BIOS rand. 0x05’s divide is signed, and a zero divisor traps on retail.
The shared scratch table
Sixteen slots of eight bytes at 0x801F3498, shared across actors. That is how parts of one effect tree hand each other positions, tween sources, render banks and scales: one part saves, a sibling loads. The cycle counter wraps mod 16 for the same reason - it indexes this table.
Why programs rewrite themselves
Seven sub-ops write a value computed at run time - a world position, a random value, a copy, an increment, the angle to the player, a party member’s position - into the operands of a later instruction of the same program. A program can therefore fly a part toward where the player is by writing that position into the lerp that follows it. A static disassembly of such a program shows the placeholder, not the value; the bounds check above is what keeps a rewritten sub-opcode from jumping out of the table.
See also
Every sub-opcode with its width and effect, the ten conditional-branch arms, the object-effect table, the HSV ramps and the strip emitter are in docs/subsystems/move-vm-overlay-ext.md.