At a glance

Dispatcher
FUN_801D362C(actor, op) in PROT 0897, 1293 instructions
Jump table
0x801CE868, 61 entries (file +0x50)
Sub-opcode
op[1], range 0x00..0x3C
Only caller
The jal at 0x80023AE0, the 0x2F arm of the move VM
Return value
Instruction width in halfwords, through the shared epilogue 0x801D4A3C
Shared state
Scratch table 0x801F3498, predicate DAT_801F22F4, cycle counter DAT_801F22F6, flag bank DAT_80085758
Port
engine-vm::move_vm::ext (dispatch), move_vm_overlay_ext::canonical_size (widths), move_ext_strip

Dispatch

SCUS_942.54, always resident Move VM FUN_80023070 71 opcodes Opcode 0x2F arm jal at 0x80023AE0 pc += returned width in u16 halfwords PROT 0897, field overlay only FUN_801D362C(actor, op) sub-opcode = op[1] sltiu 0x3D unsigned: rejects negatives Table 0x801CE868 61 entries Strip emitter FUN_801D31B0 from sub-op 0x2C only Shared epilogue 0x801D4A3C returns the arm's width; out of range returns 1
The call is to a fixed address, so opcode 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-opsWidthFamily
0x04 0x05 0x1B 0x1E 0x30 0x3A 0x3B3..5Self-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 / 0x077Branch when the player is outside / inside a box
0x38 / 0x394Branch when the actor is outside / inside a radius of the player
0x36 / 0x374Branch on the live actor count
0x08..0x0B2..3Set, clear and branch on the global predicate
0x13 0x14 0x1C 0x1D3..4Branch on, set and clear a flag in the system flag bank
0x0C..0x0E, 0x123..11Curve parameter and quadratic Bezier position
0x23 0x24 0x2A6..8Position lerps: toward operand coordinates, the map origin, or the player
0x25..0x28, 0x31..0x353..5Save / load pairs over the shared scratch table
0x0F..0x112Cycle counter: clear, wrap mod 16 and latch, save coordinates by it
0x17..0x1A5..8Object-effect parameter table: write, reset, add, yaw seat
0x1F / 0x205HSV ramp on one of the actor’s two colours
0x2B..0x2D6..7Scanline strip: set the slab, emit, add to the slab
0x2E13Build a draw-mode packet and link it into the ordering table
0x3C6Screen fade colour, immediate or ramped
0x0016No side effect: a bare jump to the epilogue. The widest arm
0x012Debug 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.