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