# Full motion port — master plan **Date:** 2026-05-04 **Scope:** Every motion-related code path. No bandaids, no workarounds. **Source research:** all 14 docs in this directory. --- ## Critical reconciliation (research-vs-research) Two prior research findings appeared to conflict: - **Agent 02** (UM handling): "Retail does NOT velocity-dead-reckon walking remotes. m_velocityVector stays at zero." - **Agent 09** (CPartArray): "Retail's actual locomotion comes from `CMotionInterp::get_state_velocity` → `CPhysicsObj::set_velocity` → `CTransition::transitional_insert`, NOT CSequence." - **Agent 11** (UM dispatch deep): "`CPhysicsObj::DoInterpretedMotion` both pushes velocity (`set_local_velocity(get_state_velocity())`) AND drives the animation sequencer's per-axis cycle slot." **Reconciliation:** Agent 02 was wrong. `m_velocityVector` IS non-zero for walking remotes — it gets written by `set_local_velocity` inside `DoInterpretedMotion`, which fires per-axis when an UM arrives AND on every per-tick `apply_current_movement` call. The original cdb trace agent 02 cited (`docs/research/2026-05-02-remote-entity-motion/resolved-via-cdb.md`) likely sampled at a moment when `DoInterpretedMotion` had not yet fired (e.g., immediately post-spawn). **Correct retail model for walking remote per-tick:** ``` 1. Frame local = identity // var_40 cached 2. CPartArray::Update writes animation root motion into local - Humanoid locomotion (Walk/Run/Sidestep): dat ships zero, so local.origin stays 0 - Non-locomotion cycles with baked PosFrames: local.origin = baked delta 3. PositionManager::adjust_offset: - InterpolationManager::adjust_offset: when distance(body, head) >= 0.05m, OVERWRITES local.origin with `direction × min(catchUpSpeed × dt, distance)` (catch-up REPLACES animation root motion this frame) - StickyManager::adjust_offset: when stuck, writes XY step + heading toward target - ConstraintManager::adjust_offset: when leashed and grounded, scales/zeros local.origin 4. Frame::combine(out, m_position.frame, local) - out.origin = m_position.origin + rotate(local.origin by m_position.l2gv) - out.rotation = m_position.rotation · local.rotation 5. UpdatePhysicsInternal: gated on velocity² > 0 - out.origin += m_velocityVector × dt + 0.5 × accel × dt² - For grounded walking remote: m_velocityVector = walk_pace_world (set per-tick by apply_current_movement → set_local_velocity → get_state_velocity). NON-ZERO. - For idle (Ready cycle): get_state_velocity returns 0 → m_velocityVector = 0 → no-op. 6. transition() — sphere sweep from m_position to out 7. set_frame(resolved) — commit ``` **Implication for L.3 port already shipped (commit 91bf1e0):** The L.3 commit ZEROS `body.Velocity` for player remotes and removes `apply_current_movement` from per-tick. This is **wrong direction**. It causes: - Body advances only via queue catch-up (no velocity integration). Steady run lags. - Orientation changes don't propagate (since velocity isn't recomputed per-tick to use latest orientation). Drift during turning. - Stop case is fine (cycle goes Ready → seqVel=0 → no motion) but drift case is broken. **Action:** revert the body.Velocity=0 change. Restore per-tick `apply_current_movement` for all remotes. Layer queue catch-up as ADDITIONAL local-frame contribution that REPLACES animation root motion (which is 0 for locomotion anyway). Sequence is exactly as agent 01 documented. --- ## What needs porting (consolidated) ### Phase A: Per-tick pipeline correction (Critical — fixes L.3 regression + drift) 1. **Restore per-tick `apply_current_movement` for all grounded remotes.** Remove the `IsPlayerGuid` zero-velocity branch added in commit 91bf1e0. 2. **Layer InterpolationManager catch-up before Frame::combine.** Local frame = identity, optionally overwritten by adjust_offset. Then composed with m_position. Then velocity integrates additively into the composed frame. 3. **Implement `Frame::combine` semantics correctly.** Out.origin = m_position.origin + rotate(local.origin by m_position.l2gv basis). Out.rotation = m_position.rotation · local.rotation. ### Phase B: Substepping (eliminates ObservedOmega) 4. **Port full `update_object` substepping with EPSILON / MinQuantum / MaxQuantum / HugeQuantum.** Constants: 0.0002s / (1/30)s / 0.1s / 2.0s. 5. **Eliminate manual `ObservedOmega` integration.** Once substepping works, retail's `omega` field on Body is integrated correctly per substep — no need for the formula seed `(π/2 × turnSpeed)`. 6. **Drop `RemoteMotion.ObservedOmega` field.** ### Phase C: UM handling rewrite (collapses 600-line cycle picker) 7. **Port `apply_interpreted_movement` (acclient @ 0x00528600).** Per-axis dispatch: STYLE → FORWARD/Falling → SIDESTEP/StopSideRight → TURN/StopTurnRight+Ready_to_queue. 8. **Port `MotionTableManager`, `CMotionTable`, `MotionState`.** Each remote has its own (style, substate, substate_mod, modifier_head, action_head). Cycle decision per-axis through table lookups, not priority picker. 9. **Port `GetObjectSequence` (@ 0x00522860).** Class-bit dispatch: 0x40000000 substate (replace), 0x20000000 modifier (additive blend + add to modifier_head), 0x10000000 action (overlay with substate restore). 10. **Collapse `OnLiveMotionUpdated` to `move_to_interpreted_state(wireInterpretedState)`.** Drop the 600-line cycle picker (forward → sidestep → turn → Ready priority). Drop the RunForward → WalkForward → Ready fallback chain (acdream-original, doesn't exist in retail). 11. **Port `adjust_motion` (FUN_00528010).** WalkBackward → WalkForward + speed×-0.65; SideStepLeft → SideStepRight + speed×-1. This is sender-side normalization. Local player path needs it too (drops hand-rolled overrides at PlayerMovementController.cs:378-411). ### Phase D: UP routing — full Branch A (hard teleport) 12. **Plumb `TeleportSequence` (u16) from `UpdatePosition` parser through `EntityPositionUpdate` to `OnLivePositionUpdated`.** Currently dropped at WorldSession.cs:110-114, 712-716. 13. **Port `update_times[]` array on RemoteMotion.** 4 slots: instance(8), position(0), teleport(4), force_position(6). Each is `u16` with 16-bit-wrap forward-test via `newer_event` helper. 14. **Implement Branch A:** when `newer_event(TELEPORT_TS, wire.teleport_seq)` OR `cell == 0` → call `teleport_hook` (CancelMoveTo + UnStick + StopInterpolating + UnConstrain + ClearTarget + report_collision_end) + `SetPosition` with flags 0x1012. 15. **Implement Branch B (queue) and Branch C (slide-snap)** correctly — these are ALREADY in commit 91bf1e0 modulo using `player_distance` (already fixed via S1). Keep. ### Phase E: VectorUpdate (jump) corrections 16. **Drop `OnLiveVectorUpdated`'s extra writes** (Gravity flag toggle, transient_state Contact/OnWalkable clear). Retail's `DoVectorUpdate` only writes `set_velocity` + `set_omega`. The Gravity flag transition is driven by `set_on_walkable(false)` 1→0 edge inside the per-tick path (post-LeaveGround). 17. **Port `set_on_walkable` 0→1 / 1→0 edge detection.** Drives LeaveGround/HitGround automatically without explicit Airborne flag. 18. **Drop `RemoteMotion.Airborne` bool.** Replace with `(state & Gravity) != 0` test. 19. **Replace K-fix15 landing heuristic** (Z-velocity-based) with retail's `contact_plane.N.z >= floor_z` (cosine of 49°) gate. 20. **Port `jumped_this_frame` write in `set_velocity`.** Currently missing in PhysicsBody. ### Phase F: Sticky + Constraint (port from scratch) 21. **Port `StickyManager` (acclient ~0x???)** — sticks body to target object at fixed cylinder radius. Activates via MoveToManager when `__inner0` bit 0x80 set. adjust_offset writes XY step + heading rotation; Z always zero. 22. **Port `ConstraintManager`** — leash to fixed Position. adjust_offset gated on Contact bit. Soft band (start..max) scales offset; past max zeros it. 23. **Wire both into `PositionManager.adjust_offset` chain** in fixed order: Interp → Sticky → Constraint. ### Phase G: NPC convergence 24. **Drop `RemoteMotion.HasServerVelocity` / `ServerVelocity` synth machinery.** NPCs go through same per-tick pipeline as player remotes once Phase A lands. 25. **`RemoteMoveToDriver` audit:** add UseFinalHeading (bit 0x40 post-arrival rotation), Sticky-after-arrival (bit 0x80 latch-on), Contact-bit gating during mid-air. Per agent 07 research §"3 minor port gaps". ### Phase H: Local player cleanup (low-priority) 26. **Wire `PlayerDescription` (0xF7B0 / 0x0013) → `_weenie.SetSkills(...)`.** Drops `ACDREAM_RUN_SKILL` env var workaround (issue #7). 27. **Gate `AutonomousPosition` heartbeat on motion.** Don't send 1Hz heartbeat at rest (cdb confirmed retail doesn't). 28. **Apply `adjust_motion` to local player input.** Drops hand-rolled velocity overrides at `PlayerMovementController.cs:378-411` and `:466-501`. ### Phase I: Cleanup 29. **Delete env-var fork** (`ACDREAM_INTERP_MANAGER=1` path, ~430 lines of dead code). 30. **Remove `RemoteMotion` workaround fields:** `ServerVelocity`, `HasServerVelocity`, `LastServerZ`, `PrevServerPos*`, `Max*`, `TargetOrientation`, `ObservedOmega`. Down from 31 fields to ~10. 31. **Remove all `IsPlayerGuid` per-tick gates** (5 sites in audit). Same pipeline for all entities. 32. **Remove `ServerControlledLocomotion.PlanFromVelocity`** if `ApplyServerControlledVelocityCycle` is the only caller (already removed in commit 91bf1e0). Verify other consumers. ### Phase J: Tests + verification 33. **Update existing tests for new architecture.** InterpolationManager tests already updated. Add per-axis dispatch tests for apply_interpreted_movement. 34. **Run full test suite, verify 0 new regressions.** 35. **Code review subagent on full port.** 36. **Visual verify with user — full motion test matrix.** --- ## Implementation order (strict serial) Bottom-up, each phase must build green before next: 1. **Phase A** (per-tick correction) — undoes the L.3 regression. Smallest, surgical. Highest priority. 2. **Phase D** (hard-teleport branch) — additive plumbing, no regression risk. 3. **Phase B** (substepping) — replaces ObservedOmega. Medium risk. 4. **Phase E** (VectorUpdate corrections) — drops acdream workarounds. Medium risk. 5. **Phase G** (NPC convergence) — depends on Phase A solid. 6. **Phase F** (Sticky + Constraint) — additive ports. 7. **Phase C** (UM handling rewrite + cycle picker) — biggest single change. Replaces 600 lines. 8. **Phase H** (local player cleanup) — independent of others. 9. **Phase I** (cleanup) — last, after everything works. 10. **Phase J** (tests + review + verify) — gating ship. --- ## Acceptance - All 3 reported user issues fixed: 1. Stop after running settles within ≤1 UP cycle (~200ms) — apply_current_movement reads Ready → velocity 0 → body stops. 2. Backward walk plays backward animation (no flip to forward run) — verified by ApplyServerControlledVelocityCycle removal in 91bf1e0. 3. Long co-run drift bounded by DesiredDistance (0.05m) — apply_current_movement per-tick keeps velocity rotated by current orientation. - Build green. - All existing tests pass (8 pre-existing failures unchanged; new tests added for per-axis dispatch). - Code review passes. - Visual verify by user.