How it works

Each script entry is 1 + N bytes:

+0  u8 op_byte         ; bit 0x7F = opcode, bit 0x80 = "select target"
+1  u8 target_id       ; only present if bit 0x80 set in op_byte
+N  u8 operand[...]    ; opcode-specific operands

When the high bit is set, the VM resolves a target actor before applying the body. 0xF8 resolves to "this actor", 0xFB follows a linked list at _DAT_8007c34c looking for a matching record-class signature, and any other id linearly scans the actor list at _DAT_8007c354 matching against the actor's id field at +0x14.

Opcodes

byteretail addrnamesemantics
0x3780037894TranslateYaccumulate Y axis by per-frame speed
0x3880037de0RotateToAngleyaw rotates toward absolute angle; 12-bit fixed-point math, shortest-path CCW/CW select (body0 & 0x80), explicit direction override (body1 & 0x80), proportional interpolation step, 16-entry ANGLE_TABLE at DAT_80073f04
0x4180037894TranslateXaccumulate X axis by per-frame speed
0x4380037f5cNoOptick budget consumed, no actor mutation
0x4780037ba8MoveTowardTargetstep actor XZ toward (tx, tz)
0x4C80037de0FaceTargetyaw rotates to bearing computed via atan2(dx, dz) (bearing_to_yaw); sub-modes 0x85 / 0x8E / 0x8F gate which component is rotated
(default)Doneterminate

Clean-room port

legaia_engine_vm::motion_vm in crates/engine-vm/src/motion_vm.rs is the clean-room port. All opcodes are ported: 0x37 TranslateY, 0x38 RotateToAngle, 0x41 TranslateX, 0x43 NoOp, 0x47 MoveTowardTarget, 0x4C FaceTarget. The angle-math opcodes use the 12-bit fixed-point shortest-path quadrant logic from FUN_8003774C lines 690+.

Camera integration

The runtime Camera in engine-core::camera consumes:

  • The field-VM op-0x45 event stream (CameraConfigure / CameraSave / CameraLoad / CameraApply) for the high-level camera state.
  • The motion VM (optional) for cinematic pre-baked camera paths via Camera::tick_script.

The default mode follows a target actor slot at a configured distance + height — same shape as the retail "follow the player" camera.