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.

Field-NPC walking

World::tick_field_npc_motions (engine-core) drives MAN-placed field NPCs through the 0x47 MoveTowardTarget pursue step, one motion-VM step per field tick, writing the live position back into World::field_npc_positions so the moving NPC's ±40-unit collision box and its interact box follow it. Three start paths feed it: autonomous patrol routes (each placement's own pre-text 0x4C 0x51 move-to-tile ops, decoded by man_field_scripts::placement_motion_route; opt-in via play-window --live-npcs, paused while a dialogue is up), interaction-prologue runs (a record's own 0x4C 0x51 walks the interacted NPC), and the actor-VM start_motion glide (op 0x09, retail FUN_800358c0). Residue: the exact retail per-NPC glide speed is unpinned (the engine paces NPCs at the player walking step), and per-actor field-VM channels are not executed - the engine loops the decoded waypoint list.

See also