# 06 — Acdream audit: player remote-entity motion code as it stands

Date: 2026-05-04. Scope: every piece of code that touches a *remote* player
character's motion between `OnLiveMotionUpdated`, `OnLiveVectorUpdated`,
`OnLivePositionUpdated`, and the per-tick `TickAnimations` loop. Inputs:

- `src/AcDream.App/Rendering/GameWindow.cs` (8346 LOC).
- `src/AcDream.Core/Physics/{MotionInterpreter,AnimationSequencer,
  PhysicsBody,InterpolationManager,PositionManager,
  ServerControlledLocomotion,RemoteMoveToDriver}.cs`.

Verdict labels used below: **PORT** (faithful retail port, retail address
cited), **HACK** (acdream-original logic with no retail equivalent), or
**BROKEN** (regressed/wrong vs. the retail spec; see ISSUES.md / file
header comments).

---

## 1. `GameWindow.RemoteMotion` (lines 224–432)

Per-remote nested struct stored on `_remoteDeadReckon[serverGuid]`. One
allocation per remote, lives until despawn. Owns:

| Field | Verdict | Notes |
|---|---|---|
| `PhysicsBody Body` | PORT | Retail `CPhysicsObj` (acclient @0x00510000 region). Constructed with `Contact|OnWalkable|Active` flags and `ReportCollisions` state — gravity OFF by default, "remotes don't simulate gravity" comment at L420. Per L.3 spec this is **the correct retail invariant**. |
| `MotionInterpreter Motion` | PORT | Retail `CMotionInterp`. Body wired in ctor. |
| `LastServerPosTime/LastServerPos` | HACK | Diagnostic + dt-source for the legacy `update_object` path. |
| `ServerVelocity / HasServerVelocity` | HACK | Acdream-only: synthesises velocity from `(pos - prevPos)/dt` because ACE rarely sets HasVelocity on player UPs. Used only by `ApplyServerControlledVelocityCycle`. **Not present in retail.** |
| `ServerMoveToActive` | PORT | Bridges `MovementType::MoveToObject/Position` (6/7) to per-tick driver. Retail sets equivalent in `MoveToManager`. |
| `LastUmUpdateTime` | HACK | 200 ms grace window on UM authority for player remotes. Workaround for "Shift toggles Run↔Walk without firing a fresh UM" (issue #39). **Not retail.** |
| `MoveToDestinationWorld / MoveToMinDistance / MoveToDistanceToObject / MoveToMoveTowards / HasMoveToDestination / LastMoveToPacketTime` | PORT | Phase L.1c MoveTo state. Used by `RemoteMoveToDriver.Drive`. |
| `TargetOrientation` | DEAD | Comment: "legacy field — no longer used for slerp". Should delete. |
| `ObservedOmega` | HACK | Per-tick rotation rate seeded from `(π/2)×TurnSpeed` formula in `OnLiveMotionUpdated`. Bypasses `PhysicsBody.update_object`'s 30 Hz quantum gate. **Necessary because `update_object` skips most 60 Hz frames** — a real port problem. |
| `CellId` | PORT | High 16 bits = LBxLBy; fed into `ResolveWithTransition`. |
| `Airborne` | HACK | Set by `OnLiveVectorUpdated` when launch velocity has +Z>0.5; cleared by post-resolve `IsOnGround && Vel.Z<=0`. Retail tracks airborne via `Contact|OnWalkable` transient bits + Gravity flag. We carry an extra bool because we toggle Gravity manually. |
| `InterpolationManager Interp` | PORT | Owned per-remote. Only consumed when `ACDREAM_INTERP_MANAGER=1`. |
| `PositionManager Position` | PORT | Same. |
| `LastServerZ` | HACK | Landing-fallback floor for env-var path (gravity drift recovery). Not retail. |
| `PrevServerPos / *Time / Last*LogTime / MaxSeqSpeedSinceLastUP` | DIAG | All gated on `ACDREAM_REMOTE_VEL_DIAG=1`. |

**Code-health note:** the struct has **31 public fields** spanning physics
state, server-snapshot cache, MoveTo path, diagnostic throttles, and
landing-fallback metadata. About half are workarounds for problems the
retail port doesn't have once we follow the spec; the L.3 refactor should
be able to remove `ServerVelocity/HasServerVelocity`, `LastUmUpdateTime`,
`LastServerZ`, `PrevServerPos*`, `Max*`, and `TargetOrientation`.

---

## 2. `GameWindow.OnLiveMotionUpdated` — L2591–3214

Inbound `0xF74C UpdateMotion` handler. Receives an `EntityMotionUpdate`
with stance + ForwardCommand + ForwardSpeed + SideStepCommand + TurnCommand
+ optional MoveTo path payload. Roughly 600 lines.

**Reads:** `update.MotionState` fields, `ae.Sequencer.{CurrentStyle,
CurrentMotion}`, `_remoteDeadReckon[guid]`, `_animatedEntities`, env vars.

**Writes:** sequencer via `SetCycle` and `RouteFullCommand`/`RouteWireCommand`;
`rm.Motion.InterpretedState.{ForwardCommand,ForwardSpeed}` (bulk-copy);
`rm.ServerMoveToActive`; `rm.LastUmUpdateTime`; `rm.MoveToDestination*`;
`rm.HasMoveToDestination`; `rm.ObservedOmega` (formula seed); also calls
`rm.Motion.DoInterpretedMotion`/`StopInterpretedMotion` for sidestep/turn axes.

### Verdict per sub-block

| Sub-block | L# | Verdict | Notes |
|---|---|---|---|
| Diag `[UM_RAW]` and `ACDREAM_DUMP_MOTION` blocks | 2616–2646 | DIAG | Throw-away. |
| Player-only RunRate echo via `ApplyServerRunRate` | 2649–2656 | PORT | Local player only. Out-of-scope for remote audit. |
| Style preservation when `stance==0` | 2667–2671 | PORT | Retail bulk-copy semantics confirmed by named decomp. |
| Stop signal: `command absent OR command.Value==0 → Ready` | 2685–2707 | PORT | Retail `FUN_0051F260` bulk-copy of Invalid. |
| MoveTo seed via `PlanMoveToStart(...)` | 2687–2703 | PORT | Wraps `ServerControlledLocomotion`; aligns with retail `MoveToManager::BeginMoveForward`. |
| Skip-self block at 2757 (don't echo SetCycle for local player) | 2757–2761 | PORT | Local UM is authoritative on the local sequencer. |
| Action/Modifier/ChatEmote overlay route | 2764–2767, 2896–2906 | PORT | `AnimationCommandRouter.Classify`. |
| InterpretedState bulk-copy of `ForwardCommand/ForwardSpeed` for ALL packets (including overlay) | 2842–2868 | PORT | Mirrors retail `copy_movement_from` (`acclient_2013_pseudo_c.txt:293301`). Speed sign preserved. |
| MoveTo path capture | 2870–2893 | PORT | World-converts `OriginX/Y/Z`. |
| Cycle picker: forward → sidestep → turn → Ready priority | 2918–2953 | HACK-ish | Retail's `apply_current_movement` doesn't pick cycles like this; the SUB_STATE animation choice in retail is driven by the wire's ForwardCommand directly. Acdream's picker exists because we synthesise locomotion velocity from the cycle — we need a cycle to play even when only Sidestep or Turn axes are populated. **Re-evaluate during port**. |
| Skip-cycle-swap when airborne (K-fix17) | 2966 | HACK | Workaround for ACE broadcasting UMs mid-arc that would otherwise stomp Falling. Retail handles this via the substate priority; we hack around it. |
| Cycle-fallback chain RunForward → WalkForward → Ready | 2989–3027 | HACK | Defensive against MotionTables missing the requested cycle. Retail behavior unknown — likely a port artifact from MoveTo always seeding RunForward. Could be removed if upstream cycle picker matches retail substate. |
| `DoInterpretedMotion(side/turn axis)` + `StopInterpretedMotion` for absent axes | 3066–3127 | PORT | Stops are explicit; mirrors retail StopMotion semantics. |
| `ObservedOmega` formula seed: `(π/2)×TurnSpeed` signed | 3110–3127 | HACK | Compensates for `update_object` MinQuantum 30 Hz gate. Comment at 6610 explicitly notes this is to bypass that gate. **A real retail port wouldn't need this** — it would call `UpdatePhysicsInternal` directly OR fix the `update_object` substepping. |
| `Commands[]` list iteration, **skipping SubState entries** | 3182–3213 | PORT-with-FIX | 2026-05-03 fix: ACE bundles a Ready into the Commands[] list of a RunForward UM, which our router used to re-cycle to Ready right after we set RunForward. Skipping SubState class entries restored the cycle. |
| `enteringLocomotion` timestamp refresh | 3142–3160 | HACK | Stop-detection timer reset; the legacy stop-detection loop has been removed but this remnant still pokes `_remoteLastMove` and `LastServerPosTime`. Should be removable. |
| Legacy non-sequencer path | 3217–3236 | DEAD-ish | All player remotes have a sequencer — only fired for entities without MotionTable (rare). |

**Critical observation:** every UM unconditionally bulk-copies into
`InterpretedState.{ForwardCommand,ForwardSpeed}`. That's correct vs. retail.
But it's the bulk-copy that arms `apply_current_movement` to write
`body.Velocity = RunAnimSpeed × ForwardSpeed`. Per the L.3 spec, **for
remote players body.Velocity should always be 0** — meaning `apply_current_movement`
must NOT be called per tick on remote bodies. The legacy path at L6599
calls it. The env-var path at L6174 explicitly does NOT (and clears
`body.Velocity` to zero each tick at L6205). The two paths are
philosophically opposed.

---

## 3. `GameWindow.OnLiveVectorUpdated` — L3259–3317

Inbound `0xF74E VectorUpdate` handler (jump/launch).

**Reads:** `update.{Velocity,Omega}`, `_remoteDeadReckon`,
`_entitiesByServerGuid`, `_animatedEntities`.

**Writes:** `rm.Body.{Velocity,Omega}`, `rm.Body.TransientState` (clears
Contact+OnWalkable), `rm.Body.State` (sets Gravity), `rm.Airborne=true`,
`ae.Sequencer.SetCycle(Falling, skipTransitionLink:true)`.

**Verdict: PORT.** Mirrors retail `SmartBox::DoVectorUpdate`
(@0x004521C0). Sets velocity AND omega, K-fix9 marks airborne, K-fix10
swaps Falling cycle, K-fix18 skips link. Threshold `Velocity.Z>0.5` to
gate Airborne is acdream-original but harmless.

**Skips:** local player guid (`_playerServerGuid`).

---

## 4. `GameWindow.ApplyServerControlledVelocityCycle` — L3325–3423

Helper that classifies a server-derived velocity into a Walk/Run/Ready
cycle and writes it to both the sequencer (visible cycle) and
`InterpretedState` (body velocity feed).

**Reads:** `rm.{Airborne,LastUmUpdateTime,ServerMoveToActive}`,
`ae.Sequencer.{CurrentMotion,CurrentStyle,CurrentSpeedMod}`, env vars.

**Writes:** `ae.Sequencer.SetCycle(...)`,
`rm.Motion.InterpretedState.{ForwardCommand,ForwardSpeed}`.

**Verdict: HACK.** This whole function exists because:

1. ACE doesn't broadcast HasVelocity on player UPs.
2. Retail clients don't broadcast a fresh UM on Shift-toggle Run↔Walk.

So we infer cycle from synthesised position-delta velocity. The 200 ms UM
grace + `IsPlayerGuid` gate are workarounds for ACE-vs-retail timing
asymmetries. **None of this exists in retail.** Issue #39.

The `IsPlayerGuid(serverGuid)` check at L3349 is one of three places we
use the player-vs-NPC distinction to gate behavior — see §10.

---

## 5. `GameWindow.OnLivePositionUpdated` — L3425–3824

Inbound `0xF748 UpdatePosition` handler. Roughly 400 lines, with TWO
distinct code paths (env-var ON vs OFF).

**Reads:** `update.{Guid,Position,IsGrounded,Velocity}`,
`_entitiesByServerGuid`, `_remoteDeadReckon`, `_liveCenterX/Y`,
`_playerController.{State,Position,StepUpHeight}`, `_physicsEngine`.

**Writes:** `entity.{Position,Rotation}`, `rmState.Body.{Position,
Velocity,Orientation,LastUpdateTime,TransientState,State}`,
`rmState.{Airborne,CellId,LastServerPos,LastServerPosTime,LastServerZ,
ServerVelocity,HasServerVelocity,TargetOrientation,Interp}`.

### Common prologue (L3432–3464)

- World-converts the LB-local position via `(lbX-_liveCenterX)*192`.
- Snaps `entity.Position`/`entity.Rotation` to server truth UNCONDITIONALLY.
- Updates `_physicsEngine.ShadowObjects` for non-self guids.

**Verdict: PORT** for the world conversion + entity snap. The
`ShadowObjects.UpdatePosition` mirrors retail `change_cell` /
`AddShadowObject`.

### Env-var ON branch (L3512–3626)

Only runs when `ACDREAM_INTERP_MANAGER=1`.

- Hard-snaps `Body.Orientation = rot` (L3516).
- Tracks `LastServerZ` only for grounded UPs (L3529).
- Diagnostic VEL_DIAG block (L3537–3562).
- **AIRBORNE NO-OP** at L3570: `if (!IsGrounded) return;` — mirrors retail
  `MoveOrTeleport` "no-op when has_contact==0" branch.
- **LANDING TRANSITION** at L3577: clears Airborne, restores ground
  flags, hard-snaps `Body.Position = worldPos`, clears `Interp` queue,
  resets sequencer cycle out of Falling.
- **GROUNDED ROUTING** at L3605: distance check `dist > 96f` →
  `SetPositionSimple`-style snap (clear + write Body.Position);
  otherwise enqueue waypoint via `Interp.Enqueue(...)`.

**Verdict: PORT** of `CPhysicsObj::MoveOrTeleport` (acclient @0x00516330)
**but with one regression**: the env-var per-tick path (§7) drops the
collision sweep — see ISSUES.md #40. The OnLivePositionUpdated side here
is correct; the regression is in TickAnimations.

### Env-var OFF branch (L3628–3761) — THE LEGACY PATH

- Synthesises `serverVelocity = (worldPos - rmState.LastServerPos)/dt`
  for ALL remotes when `update.Velocity` is null (L3634–3639).
- Sets `rmState.Body.Position = worldPos` UNCONDITIONALLY (L3650).
- Hard-snaps `rmState.Body.Orientation = rot` (L3686).
- Adopts `update.Velocity` if present, falls back to synth velocity for
  NPCs (L3705–3730).
- HasVelocity<0.2 m/s magnitude → `StopCompletely` + sequencer Ready
  (L3712–3725). **Verdict: PORT.**
- Calls `ApplyServerControlledVelocityCycle` for player remotes too
  (L3737–3757). **Verdict: HACK** (issue #39).
- Final `entity.Position = rmState.Body.Position` snap at L3759.

**Verdict overall: HACK.** The synth-velocity machinery is the
acdream-only solution to ACE's wire shape. Retail does NOT do this; retail
does pure waypoint-queue interpolation (the env-var branch).

---

## 6. `GameWindow.TickAnimations` — env-var path (L6118–6445)

Per-frame remote-motion tick when `ACDREAM_INTERP_MANAGER=1`. **Marked
DO-NOT-ENABLE** per ISSUES.md #40.

**dt source:** `OnRender` passes `(float)deltaSeconds` from Silk.NET's
`OnRender(double deltaSeconds)` callback (L5610). **This is the
render-frame dt — variable, not stable. Typically ~16 ms at 60 Hz, but
spikes during landblock loads, GC, stalls.**

### Step-by-step

| Step | Verdict | Notes |
|---|---|---|
| 1. Force `Contact+OnWalkable+Active` for grounded; clear `body.Velocity = 0` (L6194–6206) | PORT | "Body Velocity should be 0 for grounded remotes" — exactly the L.3 spec invariant. |
| 2. `PositionManager.ComputeOffset(...)` returns either queue catch-up OR animation root motion; `body.Position += offset` (L6225–6232) | PORT | Mirrors `CPhysicsObj::UpdatePositionInternal` + `InterpolationManager::adjust_offset`. The REPLACE semantics in `PositionManager.ComputeOffset` match retail. |
| 2.5. Apply `ObservedOmega` (or seqOmega) via manual `Quaternion.Concatenate` (L6247–6277) | HACK | Manual integration to bypass MinQuantum gate. |
| 3. `body.calc_acceleration()` (L6283) | PORT | Retail `FUN_00511420`. |
| 4. `body.UpdatePhysicsInternal(dt)` (L6286) | PORT | Retail `FUN_005111D0`. With `body.Velocity=0` set in step 1, this is a no-op for grounded remotes — only airborne picks up gravity. |
| 4b. `_physicsEngine.ResolveWithTransition(preIntegrate, postIntegrate, ...)` (L6288–6373) | PORT | Added by Commit B (039149a) to fix the missing-collision regression. Mirrors retail `FUN_005148A0`. Sphere dims 0.48 m radius / 1.2 m height / 0.4 m step-up/down. |
| 5. Landing fallback: if airborne and `Body.Z < LastServerZ - 0.5`, force-land (L6387–6421) | HACK | Defensive against ACE not sending IsGrounded promptly. |
| 6. `MaxSeqSpeedSinceLastUP` diag (L6432–6441) | DIAG | Tracks max body-velocity magnitude for the VEL_DIAG ratio. |
| Final: `ae.Entity.Position = rm.Body.Position; ae.Entity.Rotation = rm.Body.Orientation` (L6443–6444) | PORT | The renderable consumes body state. |

**Why marked broken:** ISSUES.md #40 says the staircase-on-slope and
position-blip bugs persist even with Commit B's collision sweep ported in.
Comment at L6131–6141 acknowledges that body.Velocity=0 means
pre/postIntegrate sweep input is just queue catch-up, which itself snaps
in 1 Hz UP-cadence steps.

---

## 7. `GameWindow.TickAnimations` — legacy default path (L6446–6764)

Per-frame remote-motion tick when env var is unset. **The CURRENT
default in production builds.**

### Step-by-step

| Step | Verdict | Notes |
|---|---|---|
| 1. Force grounded transient flags (L6488–6492) | HACK | Stomps any airborne flag fluctuation; "remotes are server-authoritative". |
| 1a. NPC HasServerVelocity branch: stale-after-X seconds → zero + cycle to Ready; otherwise `body.Velocity = ServerVelocity` (L6493–6511) | HACK | Synth-velocity continuation. |
| 1b. NPC ServerMoveToActive branch with destination: `RemoteMoveToDriver.Drive` + `apply_current_movement` + `ClampApproachVelocity` (L6512–6587) | PORT | Phase L.1c MoveTo per-tick steering. |
| 1c. `ServerMoveToActive` without destination → `body.Velocity = 0` (L6588–6596) | PORT | Conservative hold. |
| 1d. ELSE branch (everything else, including ALL player remotes): `rm.Motion.apply_current_movement(...)` (L6599) | **BROKEN per L.3 spec** | This is the bug. `apply_current_movement` reads `InterpretedState.ForwardCommand=RunForward + ForwardSpeed` and writes `body.Velocity = RunAnimSpeed × ForwardSpeed × orientation`. Per L.3 the player remote body should NEVER have non-zero velocity from this path; velocity should come solely from animation root motion + interpolation queue catch-up. |
| 2. Manual omega integration via `ObservedOmega` (L6622–6631) | HACK | Same MinQuantum bypass as env-var path. |
| 3. `body.calc_acceleration()` + `body.UpdatePhysicsInternal(dt)` (L6651–6653) | PORT | But because of 1d, body.Velocity is non-zero, so this **double-integrates** — the velocity drives translation here, then `ResolveWithTransition` resolves the Δ. |
| 4. `_physicsEngine.ResolveWithTransition(...)` (L6674–6760) | PORT | Same call as env-var path. |
| 4b. K-fix15 post-resolve landing (L6717–6759) | HACK | Same purpose as env-var step 5. |
| Final: `ae.Entity.Position = rm.Body.Position` (L6762) | PORT | |

**This is the path the user sees in production.** The `apply_current_movement`
call on every tick at L6599 is **the central thing that the L.3 port has
to remove for player remotes.** Then we replace the per-tick translation
source with animation-root-motion + Interpolation-queue catch-up
(`PositionManager.ComputeOffset` from the env-var path) — but this time
WITH the collision sweep retained.

---

## 8. `MotionInterpreter` (full file, 1023 LOC)

**Verdict: PORT.** All key methods cite retail addresses:

- `PerformMovement` (FUN_00529a90), `DoMotion` (FUN_00529930),
  `DoInterpretedMotion`, `StopMotion`, `StopInterpretedMotion`,
  `StopCompletely` (FUN_00528a50), `get_state_velocity` (FUN_00528960),
  `apply_current_movement` (FUN_00529210), `jump` (FUN_00529390),
  `get_jump_v_z`, `get_leave_ground_velocity`, `jump_is_allowed`,
  `contact_allows_move`, `LeaveGround`, `HitGround`, `GetMaxSpeed`
  (CMotionInterp::get_max_speed @0x00527cb0).

`apply_current_movement` (L653–673) gates on `PhysicsObj.OnWalkable`
and calls `set_local_velocity(get_state_velocity())`. This is the
**single function that translates `InterpretedState` into
`body.Velocity`**. Per the L.3 spec, **this must NOT be called per tick on
remote players' bodies** — only the local player.

`GetMaxSpeed()` (L972–985) returns `RunAnimSpeed × runRate` (≈ 11.76 m/s
for run-skill 200). This is the value passed to
`InterpolationManager.AdjustOffset` as the catch-up speed cap.

Question 1 answer: **`body.Velocity` is currently NON-ZERO for player
remotes in the legacy default path**, set every tick by
`apply_current_movement` at L6599. Per L.3 spec it should always be 0.
This is the regression to fix.

Question 2 answer: `apply_current_movement` is called from:

- `PlayerMovementController.cs:273` (local player — correct).
- `GameWindow.cs:6567` (NPC MoveTo steering — correct).
- `GameWindow.cs:6599` (legacy default per-tick for ALL non-MoveTo
  remotes — **incorrect per L.3 spec**).
- `MotionInterpreter` internally inside `DoInterpretedMotion` /
  `StopInterpretedMotion` / `HitGround` (called from
  `OnLiveMotionUpdated`'s sidestep+turn axis loops).

It produces: `body.Velocity = world_rotation × Vector3(SidestepAnimSpeed×SideStepSpeed,
WalkOrRunAnimSpeed×ForwardSpeed, 0)`, clamped to `RunAnimSpeed × runRate`.

---

## 9. `AnimationSequencer` (relevant fields, 1455 LOC total)

**Verdict: PORT** of retail Sequence. Relevant API surface for the
remote-motion port:

- `CurrentVelocity` (L246) — sequence-wide latest MotionData.Velocity ×
  speedMod, body-local. **Synthesised** for known locomotion cycles
  (Walk/Run/SideStep) at L614–646 because the Humanoid MotionTable
  ships HasVelocity=0 on those cycles. Synthesised values match retail
  constants (RunAnimSpeed=4.0, WalkAnimSpeed=3.12, SidestepAnimSpeed=1.25).
- `CurrentMotion` / `CurrentStyle` / `CurrentSpeedMod` — sequencer's
  active cycle. Read by everywhere.
- `SetCycle(style, motion, speedMod, skipTransitionLink)` — has fast-path
  for identical motion (sign-matched) → `MultiplyCyclicFramerate`. Cycle
  sign-flip path (e.g. positive→negative speed) takes the full rebuild
  branch. **PORT**.
- `MultiplyCyclicFramerate(factor)` — scales every cyclic node's framerate
  AND scales `CurrentVelocity *= factor`, `CurrentOmega *= factor`. Mirrors
  retail `multiply_cyclic_animation_framerate`. **PORT**.
- `CurrentOmega` — synthesised from TurnRight/TurnLeft motion (`±π/2 ×
  speedMod`) when MotionData.Omega is silent. **PORT.**

`CurrentVelocity` is **the canonical source for per-tick remote
translation** under the L.3 design. Already wired to
`MotionInterpreter.GetCycleVelocity` for the local player path; for
remotes, `PositionManager.ComputeOffset` reads it directly.

---

## 10. `PhysicsBody` (full file, 436 LOC)

**Verdict: PORT.** Every method cites a retail FUN address.

Key fields used by the per-tick remote path:
- `Position` — written by `OnLivePositionUpdated` (hard-snap) AND by
  per-tick `Position += offset` in env-var path AND by `UpdatePhysicsInternal`'s
  `Position += Velocity*dt + 0.5*Accel*dt²` AND by `ResolveWithTransition`.
- `Velocity` — written by `OnLiveVectorUpdated` (jump launch),
  `OnLivePositionUpdated` (HasVelocity adoption), `apply_current_movement`
  via `set_local_velocity`, post-resolve landing (zero), and the env-var
  per-tick path (forced to 0 each tick).
- `Orientation` — hard-snap on UP, manual integration via ObservedOmega
  in both per-tick paths.
- `update_object` — has a `MinQuantum=1/30s` early-return that **silently
  skips integration on 60 Hz frames**. This is why we call
  `UpdatePhysicsInternal` directly from per-tick paths and do manual omega
  integration. The retail intent was sub-stepping for variable dt; our
  60 Hz tick happens to fall under the gate.

Question 3 answer (`body.Position` write sites): see §6 list. Counted
21 distinct write sites in `GameWindow.cs` alone. The L.3 port should
collapse this to a single canonical write per tick + a single hard-snap
write in OnLivePositionUpdated.

Question 4 answer (per-tick render dt): L5610 — passed straight from
Silk.NET `OnRender(double deltaSeconds)`. **It is render-frame-rate-
dependent and not stable.** This is a known issue: any code that uses
this dt to integrate motion (e.g. `body.UpdatePhysicsInternal(dt)`,
`PositionManager.ComputeOffset(dt, ...)`, manual omega step) is
implicitly tied to render rate. Retail's `update_object` clamps dt to
[MinQuantum, MaxQuantum=0.1, HugeQuantum=2.0] and sub-steps; we bypass
that gate per §10.

---

## 11. `InterpolationManager` (full file, 329 LOC)

**Verdict: PORT.** Retail `InterpolationManager::adjust_offset`
(@0x00555D30) + UseTime stall/blip (@0x00555F20). Constants verified
from named binary. Only consumed when `ACDREAM_INTERP_MANAGER=1` (i.e.
the env-var path).

API surface used:
- `Enqueue(targetPosition, heading, isMovingTo)` — called from
  `OnLivePositionUpdated` env-var path L3623.
- `AdjustOffset(dt, currentBodyPosition, maxSpeedFromMinterp)` —
  called from `PositionManager.ComputeOffset`.
- `Clear()` — on landing, on far-snap, on UP after stale.
- `IsActive` — diagnostic.

Stall detection (5-frame window, `progress_quantum`, `node_fail_counter`)
is faithfully ported. **This whole machinery is currently unused in the
default build** — it's the env-var path's centerpiece.

---

## 12. `PositionManager` (76 LOC)

**Verdict: PORT.** Retail `CPhysicsObj::UpdatePositionInternal`
(@0x00512c30) + `InterpolationManager::adjust_offset` (@0x00555D30).

`ComputeOffset(dt, currentBodyPosition, seqVel, ori, interp, maxSpeed)`:
1. Calls `interp.AdjustOffset(...)` for the queue catch-up vector.
2. If catch-up is non-zero (queue active and body far from head),
   **returns the catch-up directly** — REPLACES the offset, doesn't add.
3. If catch-up is zero (queue empty or body within DesiredDistance),
   **returns animation root motion**: `Vector3.Transform(seqVel * dt,
   ori)`.

**This is the canonical L.3 per-tick offset.** Currently only consumed
in the env-var path.

---

## 13. `ServerControlledLocomotion` (129 LOC)

**Verdict: PORT-ish + HACK.** Two functions:

- `PlanMoveToStart(moveToSpeed, runRate, canRun)` — seeds RunForward (or
  WalkForward if !canRun) for an inbound MoveTo packet. Retail
  `MoveToManager::BeginMoveForward` + `MovementParameters::get_command`.
  **PORT.**
- `PlanFromVelocity(worldVelocity, currentMotion)` — classifies a
  velocity into Ready/Walk/Run with hysteresis bands (Walk→Run at 3.90,
  Run→Walk at 3.43). **HACK** — this is the workaround for "ACE rarely
  sets HasVelocity on player UPs." Retail doesn't classify like this; it
  just plays the cycle the wire told it to play.

---

## 14. `RemoteMoveToDriver` (304 LOC)

**Verdict: PORT.** Retail `MoveToManager::HandleMoveToPosition`
(@0x00529d80). Steers body orientation toward destination, fires
arrival predicate, ports the ±20° HeadingSnapToleranceRad fudge. Used
only for NPC MoveTo packets — not on the player-remote path. Out of
scope for this audit's primary concern but listed because the spec
asked.

`ClampApproachVelocity` (L260–293) is acdream-original belt-and-braces
to prevent overshoot in the final tick. **HACK** but harmless.

---

## Specific question answers (§§ refs in §1–§14)

1. **Is `body.Velocity` ever non-zero for player remotes?**
   Yes, every tick of the legacy default path (L6599
   `apply_current_movement`) writes `body.Velocity = RunAnimSpeed×ForwardSpeed`
   in world frame. Also briefly during jumps (`OnLiveVectorUpdated`) and
   during stop UPs with `HasVelocity~0`. Per L.3 spec this should be 0
   except during airborne arcs. The legacy path is the regression.

2. **Where is `apply_current_movement` called?** See §8. Three live
   call sites: PlayerMovementController (correct), GameWindow MoveTo
   steering (correct), GameWindow legacy per-tick for player remotes
   (wrong per L.3). It produces body-local
   `(SidestepAnimSpeed×SideStepSpeed, RunAnimSpeed×ForwardSpeed, 0)`
   then rotates by orientation.

3. **Where is `body.Position` written?** 21 sites in `GameWindow.cs`:
   2 in `OnLiveVectorUpdated`-adjacent code (none direct on Position),
   ~5 in `OnLivePositionUpdated` (hard-snap), ~14 in `TickAnimations`
   (env-var: 2 direct + 1 via Resolve; legacy: 2 + 1 via Resolve;
   plus airborne/landing fallbacks). See L3585, L3614, L3650, L6232,
   L6332, L6397, L6699 + the `ae.Entity.Position = rm.Body.Position`
   mirrors at L3759, L6443, L6762.

4. **Per-tick render dt source?** Silk.NET `OnRender(double deltaSeconds)`
   → `TickAnimations((float)deltaSeconds)` at L5610. **Variable; tied to
   render rate; no clamp before reaching `UpdatePhysicsInternal` /
   `ComputeOffset`.** `PhysicsBody.update_object` would clamp this if
   used, but we bypass it via direct `UpdatePhysicsInternal` calls.

5. **Env-var vs legacy default relationship?** Two parallel per-tick
   implementations forked at L6118 in `TickAnimations`. Env-var path
   (L6118–6445) clears `body.Velocity=0` each tick and translates via
   `PositionManager.ComputeOffset` (anim root motion OR queue catch-up).
   Legacy path (L6446–6764) calls `apply_current_movement` to write
   `body.Velocity` from InterpretedState then integrates via
   `UpdatePhysicsInternal`. They have parallel `ResolveWithTransition`
   collision sweeps (env-var added in Commit B as a regression fix).
   The env-var path is the L.3 architecture but is currently REGRESSED
   (issue #40 — staircase + blips). The legacy path is the production
   default but is fundamentally wrong vs L.3 spec.

6. **What does `entity.Position` (renderable) read from?** `body.Position`
   only. Final assignment at L6443 (env-var) or L6762 (legacy) per tick;
   hard-snap to server `worldPos` in `OnLivePositionUpdated` (L3451 then
   over-written to `rmState.Body.Position` at L3759).

7. **`IsPlayerGuid` gate sites:** L706 (definition), L3349
   (`ApplyServerControlledVelocityCycle` UM-grace branch), L3727
   (`OnLivePositionUpdated` velocity-adoption fallback), L6493/L6512/L6588
   (legacy per-tick branch selection between NPC paths and the catch-all
   `apply_current_movement` else-branch). **All five are guards that route
   player remotes through the broken `apply_current_movement` path.** A
   port that drops the special player-vs-NPC distinction at the per-tick
   layer would invert all five.

---

# Summary

## (a) Code health for player remote motion

**Mixed-to-poor.** The retail-port primitives — `MotionInterpreter`,
`AnimationSequencer`, `PhysicsBody`, `InterpolationManager`,
`PositionManager`, and the inbound packet handlers' bulk-copy semantics
— are individually faithful and well-cited. But the per-tick integration
in `GameWindow.TickAnimations` has forked into two parallel paths
(env-var-gated `ACDREAM_INTERP_MANAGER=1` and legacy default), neither
of which currently ships the right behavior. The legacy default calls
`apply_current_movement` every tick on player remotes — directly violating
the L.3 invariant that body.Velocity should be 0 for grounded remotes —
and the env-var path drops too much (no body integration, body.Velocity
forced to 0 even mid-jump-arc until the recent Commit B partial-fix). Worse,
`OnLiveMotionUpdated` carries ~600 lines of cycle-picker logic, missing-
cycle-fallback chains, ObservedOmega formula seeding, and `IsPlayerGuid`-
gated workarounds that compensate for ACE wire shape vs. retail. The
`RemoteMotion` struct has 31 fields, half of which are workaround state
that the retail port shouldn't need.

## (b) Top 3 things that need to change

1. **Remove `apply_current_movement` from the per-tick remote path
   entirely.** Replace with `PositionManager.ComputeOffset(dt, body.Position,
   sequencer.CurrentVelocity, body.Orientation, interp, GetMaxSpeed())`
   — the env-var path's translation source — but keep the
   `ResolveWithTransition` collision sweep that the legacy path correctly
   includes. `body.Velocity` stays at 0 except during airborne arcs.
2. **Collapse the env-var branch into the default and delete legacy.**
   The fork is the bug; both paths should converge on the L.3 design with
   `InterpolationManager` queue + animation root motion + collision sweep.
   Remove `_remoteDeadReckon`'s `ServerVelocity / HasServerVelocity /
   LastUmUpdateTime / LastServerZ / PrevServerPos*` workaround fields.
3. **Drop the `IsPlayerGuid` per-tick gate.** Retail runs the same motion
   pipeline for every entity; the special-casing in
   `ApplyServerControlledVelocityCycle` (issue #39 hysteresis) and the 3
   per-tick branch sites exist only because we synthesise velocity from
   position deltas. Once the per-tick translation is anim-root-motion
   driven, players and NPCs share one path and the gates can be inverted
   or removed.

## (c) Path to written audit

`docs/research/2026-05-04-l3-port/06-acdream-audit.md` (this file).
