acdream/docs/research/2026-05-04-l3-port/00-master-plan.md
Erik de129bc164 feat(motion): L.3 M1 — fresh InterpolationManager port + retail spec
Rewrites src/AcDream.Core/Physics/InterpolationManager.cs from the spec
in docs/research/2026-05-04-l3-port/04-interp-manager.md. Public API
preserved (Vector3-returning AdjustOffset, Enqueue, Clear, IsActive,
Count) so PositionManager + GameWindow callers continue to compile;
internals are full retail spec.

Bug fixes vs prior port (audit 04-interp-manager.md § 7):

  #1  progress_quantum accumulates dt (sum of frame deltas), not step
      magnitude. Retail line 353140; the prior port's `+= step` made
      the secondary stall ratio meaningless.

  #3  Far-branch Enqueue (dist > AutonomyBlipDistance = 100m) sets
      _failCount = StallFailCountThreshold + 1 = 4, so the next
      AdjustOffset call's post-stall check fires an immediate blip-to-
      tail snap. Retail line 352944. Prior port silently drifted
      toward far targets at catch-up speed instead of teleporting.

  #4  Secondary stall test ports the retail formula verbatim:
      cumulative / progress_quantum / dt < CREATURE_FAILED_INTERPOLATION_PERCENTAGE.
      Audit notes the units are 1/sec (likely Turbine bug or x87 FPU
      misread by Binary Ninja) — mirrored byte-for-byte regardless.

  #5  Tail-prune is a tail-walking loop, not a single-tail compare.
      Multiple consecutive stale tail entries within DesiredDistance
      (0.05 m) of the new target collapse together. Retail line 352977.

  #6  Cap-eviction at the HEAD when count reaches 20 (already correct
      in the prior port; verified).

New API: Enqueue gains an optional `currentBodyPosition` parameter so
the far-branch detection can reference the body when the queue is
empty. Backward-compatible (default null = pre-far-branch behavior).

UseTime collapsed into AdjustOffset's tail (post-stall blip check)
since acdream has no per-tick UseTime call separate from
adjust_offset; identical semantic outcome.

State fields renamed to retail names with sentinel values:
  _frameCounter, _progressQuantum, _originalDistance (init = 999999f
  sentinel per retail line 0x00555D30 ctor), _failCount.

Tests:
- 17/17 InterpolationManagerTests green.
- New test Enqueue_FarBranch_PrearmsImmediateBlipOnNextAdjustOffset
  pins the bug #3 fix: enqueueing 150 m away triggers a same-tick
  blip (delta length ≈ 150 m), and the queue clears.

Spec tree: 17 research docs (00–14) under docs/research/2026-05-04-l3-port/.
00-master-plan + 00-port-plan describe the 8-phase rollout. 01-per-tick,
03-up-routing, 04-interp-manager, 05-position-manager-and-partarray,
06-acdream-audit, 14-local-player-audit are the L.3 spec used by this
commit and the M2 follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:56:42 +02:00

150 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.