# acdream CURRENT inbound remote-entity motion/animation pipeline — as-is map Repo: `C:/Users/erikn/source/repos/acdream/.claude/worktrees/vigorous-joliot-f0c3ad` Scope: REPORT-ONLY. Describes what the code does TODAY, not what it should do. --- ## 0. File inventory (all line numbers verified against current worktree) - `src/AcDream.Core.Net/Messages/UpdateMotion.cs` (333 lines) — inbound 0xF74C parser. - `src/AcDream.Core.Net/Messages/UpdatePosition.cs` (177 lines) — inbound 0xF748 parser. - `src/AcDream.Core.Net/Messages/MoveToState.cs` (108 lines) — **OUTBOUND ONLY** (0xF61C, client→server). Not part of the inbound path; included by the task's starting-point list but has no inbound role. - `src/AcDream.App/Rendering/GameWindow.cs` (13,759 lines) — `OnLiveMotionUpdated` (4230–4967), `OnLivePositionUpdated` (5334–~5773), `ApplyServerControlledVelocityCycle`/`ApplyPlayerLocomotionRefinement` (5112–5333), `TickAnimations` (9673–~10267+), `RemoteMotion` class (401–~592). - `src/AcDream.Core/Physics/AnimationSequencer.cs` (1583 lines) — `SetCycle` (401–788), `Advance` (862–957), helpers. - `src/AcDream.Core/Physics/MotionInterpreter.cs` (1255 lines) — `DoInterpretedMotion`/`StopInterpretedMotion`/`StopCompletely`/`get_state_velocity`/`adjust_motion`/`apply_raw_movement`/`apply_current_movement`/`GetMaxSpeed`. - `src/AcDream.Core/Physics/RemoteMoveToDriver.cs` (340 lines) — per-tick steering for MoveToObject/MoveToPosition (movementType 6/7) remotes. - `src/AcDream.Core/Physics/ServerControlledLocomotion.cs` (87 lines) — velocity→cycle classifier for non-player remotes (`PlanFromVelocity`) and MoveTo-start seed (`PlanMoveToStart`). - `src/AcDream.Core/Physics/InterpolationManager.cs` (389 lines) — retail `CPhysicsObj` position-waypoint queue port (FIFO cap 20, catch-up, stall detection, blip-to-tail). - `src/AcDream.Core/Physics/PositionManager.cs` (108 lines) — per-frame REPLACE combiner (queue-catchup vs anim-root-motion), used only for grounded, non-airborne PLAYER remotes. **No `pending_motions` / `MotionDone` lifecycle exists anywhere in the codebase.** `grep -rn "pending_motion|MotionDone|PendingMotion" src/` returns zero matches. Confirmed as a genuine gap (matches the animation-sequencer-deep-dive research doc's "missing pending_motions HIGH" finding). --- ## Q1 — INBOUND ENTRY: wire → motion interpreter, for a REMOTE object 1. **Wire parse.** `AcDream.Core.Net.Messages.UpdateMotion.TryParse` (`UpdateMotion.cs:70-253`) decodes opcode `0xF74C` into `Parsed(uint Guid, CreateObject.ServerMotionState MotionState)`. Extracts: `Guid`, `currentStyle` (Stance), and — when `movementType == 0` (InterpretedMotionState) — optional `ForwardCommand`/`ForwardSpeed`/`SideStepCommand`/`SideStepSpeed`/`TurnCommand`/`TurnSpeed` plus a `Commands` list (actions/emotes), gated by a 7-bit flags dword (`UpdateMotion.cs:145-226`). When `movementType is 6 or 7` (MoveToObject/MoveToPosition), it instead parses a `MoveToPathData` payload via `TryParseMoveToPayload` (`UpdateMotion.cs:255-331`): target guid (type 6 only), Origin cell+xyz, `MovementParameters` dword, distance/min/fail distances, speed, walkRunThreshold, desiredHeading, runRate. - **Fields parsed then discarded / not used for pose**: `instanceSequence` (u16, `UpdateMotion.cs:85-87`, "tracked but not used for pose" — actually not even stored, just skipped), `movementSequence`+`serverControlSequence` (u16+u16 inside the 6-byte MovementData header, `UpdateMotion.cs:89-106`, skipped wholesale via `pos += 6`), `isAutonomous` (u8, same 6-byte skip — never separately read), `_motionFlags` (u8, read into a local but only used for the diagnostic dump at `UpdateMotion.cs:111,115-122`, otherwise discarded), `distanceToObject`/`failDistance`/`walkRunThreshold`/`desiredHeading` inside `TryParseMoveToPayload` (parsed at `UpdateMotion.cs:305-306,309,313,315` into locals, never placed on `MoveToPathData` — only `distanceToObject`(as `DistanceToObject`), `minDistance`, `failDistance`(discarded — not passed to the record), `walkRunThreshold` and `desiredHeading` ARE passed to `MoveToPathData` ctor at `UpdateMotion.cs:319-329` but `MoveToPathData` doesn't expose `FailDistance` in the earlier local list — check record fields separately if exact retention matters). - Similarly, `UpdatePosition.TryParse` (`UpdatePosition.cs:77-175`) decodes 0xF748 into guid, `ServerPosition` (cellId+xyz+quat), optional `Velocity`, optional `PlacementId`, `IsGrounded` flag, and three of four trailing u16 sequence numbers (`instSeq`, `teleSeq`, `forceSeq` — the second one, "positionSequence", is explicitly skipped: `UpdatePosition.cs:156` comment "not tracked by movement"). None of `instSeq`/`teleSeq`/`forceSeq` are consulted anywhere downstream for freshness/ordering (comment at `UpdatePosition.cs:29-31`: "We don't currently check these for freshness but we must consume them to walk the buffer correctly"). 2. **Session dispatch → GameWindow event.** (Not directly inspected this pass, but referenced via `_liveSession.MotionUpdated += OnLiveMotionUpdated;` at `GameWindow.cs:2633`.) The parsed `UpdateMotion.Parsed` becomes an `AcDream.Core.Net.WorldSession.EntityMotionUpdate` and fires the `MotionUpdated` event. 3. **`GameWindow.OnLiveMotionUpdated`** (`GameWindow.cs:4230`) is the sole consumer. - Early-outs: `_dats is null` (4232), entity not found by `update.Guid` in `_entitiesByServerGuid` (4233), or no `AnimatedEntity` for it in `_animatedEntities` (4234). - Stamps `rmStateForUm.LastUMTime` on the `_remoteDeadReckon[update.Guid]` entry if present (4243-4247) — used later to gate the velocity-refinement grace window (Q7 / `UmGraceSeconds`). - Reads `stance = update.MotionState.Stance` and `command = update.MotionState.ForwardCommand` (nullable) (4259-4260). - **Sequencer path** (the only path exercised in practice — `ae.Sequencer is not null` at 4327) reconstructs a full 32-bit `fullMotion` command from the wire's 16-bit `ForwardCommand` via `MotionCommandResolver.ReconstructFullCommand` (4372-4378), OR seeds a MoveTo-derived motion via `ServerControlledLocomotion.PlanMoveToStart` when `IsServerControlledMoveTo` and no forward command (4356-4363), OR falls back to `0x41000003` (Ready) when the command is null/0 and not a MoveTo (4364-4367 — "retail stop signal"). - **Branches on `update.Guid == _playerServerGuid`**: the LOCAL player's own echoed UM is NOT applied to the sequencer (comment 4401-4416: "UpdatePlayerAnimation is the authoritative driver ... skip the wire-echo SetCycle"). Everything from here on (Q2-Q7) describes the REMOTE (`else` branch at 4493) path. - For a genuine remote: bulk-copies `fullMotion`/`speedMod` into `remoteMot.Motion.InterpretedState.ForwardCommand`/`ForwardSpeed` UNCONDITIONALLY (4572-4598) — this is the direct feed into `MotionInterpreter.get_state_velocity()` (Q5). Then classifies via `AnimationCommandRouter.Classify(fullMotion)` into `forwardIsOverlay` (Action/Modifier/ChatEmote — routed via `RouteFullCommand`, an overlay call, 4626-4636) vs SubState (the normal walk/run/turn/ready path, 4637 `else` block — this is Q2/Q3/Q4's territory). --- ## Q2 — TRANSITION: walk↔run while already moving (no stop) Frame-by-frame, for the SubState (non-overlay) branch at `GameWindow.cs:4637-4859`: 1. **Cycle selection** (4648-4683): `animCycle = fullMotion`, `animSpeed = speedMod` by default. If the forward command is NOT run/walk/walkback (`fwdIsRunWalk` false), fall back to sidestep or turn cycle. **For a plain walk↔run toggle, `fullMotion` IS run/walk, so this stays the forward cycle** — no fallback triggered. 2. **Airborne guard** (4696): if `remoteIsAirborne`, the cycle swap is SKIPPED entirely (Falling cycle preserved) — not relevant to a grounded walk↔run toggle. 3. **Cycle-existence fallback chain** (4728-4757): if the MotionTable lacks `(fullStyle, cycleToPlay)`, falls back RunForward→WalkForward→Ready→(no-op, leave sequencer alone). Only matters if the creature's table is missing the target cycle; for the player Humanoid table both Walk and Run exist. 4. **`ae.Sequencer.SetCycle(fullStyle, cycleToPlay, animSpeed)`** is called (4769) — this is the entry into `AnimationSequencer.SetCycle` (`AnimationSequencer.cs:401`). ### Inside `SetCycle` (AnimationSequencer.cs:401-788) — what actually happens on Walk→Run or Run→Walk: - `adjust_motion` remap (407-423): TurnLeft/SideStepLeft/WalkBackward get mirrored to their positive counterpart with negated speed. RunForward(0x07)/WalkForward(0x05) pass through unchanged (`adjustedMotion = motion`). - **Fast-path check** (440-468): fires only if `CurrentStyle == style && CurrentMotion == motion` (same exact motion command) AND sign of speedMod matches. **On an actual Walk→Run toggle, `CurrentMotion` (0x45000005 WalkForward) != `motion` (0x44000007 RunForward), so the fast path does NOT fire** — this is a FULL REBUILD, not a framerate-only rescale. (The fast path only fires when the SAME command re-arrives with a different speed, e.g. a run-rate echo while already running.) - **Full-rebuild path** (470-788): - `GetLink(style, CurrentMotion=WalkForward, CurrentSpeedMod, adjustedMotion=RunForward, adjustedSpeed)` looks up a transition-link MotionData from the MotionTable's `Links` dict (470-480, method at 1159-1203). Both speeds positive here, so it uses the forward-direction lookup: `Links[(style<<16)|WalkForward][RunForward]`. - `ClearCyclicTail()` (511) — removes the OLD cycle's looping node(s) from the queue; non-cyclic (link) frames not yet played are left alone. - `ClearPhysics()` (529) — zeroes `CurrentVelocity`/`CurrentOmega` before rebuild. - Enqueues the link's MotionData (non-looping) THEN the new cycle's MotionData (looping) via `EnqueueMotionData` (547-554). - **Direct cyclic→cyclic transition special-case** (585-635, "Fix B" 2026-05-06): if BOTH the previous motion (WalkForward) AND the new motion (RunForward) are recognized locomotion low-bytes (`IsLocomotionCycleLowByte`, checks against 0x05/0x06/0x07/0x0F/0x10 — `AnimationSequencer.cs:1509-1513`), the code DROPS the just-enqueued transition link from the queue entirely (629-632) and forces `_currNode = _firstCyclic` directly onto the new RunForward cycle (633-634), skipping the link animation and its frame-position start entirely. This is explicitly a divergence from the general link-transition mechanism, justified by a live cdb trace (609-618 comment) showing retail's `add_to_queue` sequence for a Walk→Run direct transition never fires `truncate_animation_list` and just appends the new cycle directly — i.e., **acdream's current behavior for walk↔run is: NO visible link/blend animation, instant hard-cut cycle swap to the new cycle's start frame** (`_framePosition = _firstCyclic.Value.GetStartFramePosition()`, 634). - If NOT both locomotion (e.g. Ready→WalkForward), the link IS played (`_currNode = firstNew` at 636-640) — that's the smooth-transition path used elsewhere, just not for walk↔run. - `CurrentStyle`/`CurrentMotion`/`CurrentSpeedMod` updated (682-684). - **Velocity synthesis** (686-754): because retail's Humanoid MotionData ships `Flags=0x00` (no `HasVelocity`) for locomotion cycles, `CurrentVelocity` is unconditionally re-synthesized here from hardcoded constants (`WalkAnimSpeed=3.12f`, `RunAnimSpeed=4.0f`, `SidestepAnimSpeed=1.25f` — 793-795) times `adjustedSpeed`, based on the NEW motion's low byte. **This happens regardless of whether the link or the cycle is what's currently playing** — i.e. `CurrentVelocity` snaps to the new cycle's steady-state speed the instant `SetCycle` returns, even in the general (non-locomotion-direct) case where a link animation is still visually playing. - Omega synthesis (756-787) similarly, only for turn commands (not relevant to walk/run). ### Frame-by-frame summary for Walk→Run toggle while moving: - **Frame N (UM arrives)**: `OnLiveMotionUpdated` fires → `SetCycle(style, RunForward, speedMod)` called synchronously within the same event handler call (not queued/deferred) → full rebuild → cyclic→cyclic special case detected → link dropped, `_currNode` snapped directly to Run cycle's start frame, `CurrentVelocity` synthesized to `RunAnimSpeed × speedMod` — ALL of this happens on the SAME UM-processing call, before the next `Advance()`/tick. - **Frame N (same tick, later in loop)**: `TickAnimations` (`GameWindow.cs:9673`) runs later that frame (or next frame depending on event/tick ordering) and calls `Advance(dt)` on the sequencer, which now plays from the Run cycle's first frame — no partial-frame blend from the old Walk pose. - **Is there blending?** NO cross-fade/blend for the walk↔run case specifically — only the (skipped) link animation would have provided a transitional pose, and it's explicitly dropped for this case. - **Is the new motion appended/queued, or does it replace?** It REPLACES: `ClearCyclicTail()` always removes the old cycle before the new one is enqueued (511), and for the walk↔run case the link itself is also stripped from the queue (629-632) so only the new cycle remains. - **Speed**: `CurrentVelocity`/animation framerate both change immediately and atomically inside `SetCycle` — no interpolation of speed either (the fast-path's `MultiplyCyclicFramerate` smooth-rescale only applies when `CurrentMotion` is unchanged, i.e. NOT for an actual Walk↔Run motion-command swap, only for a same-command speed update). --- ## Q3 — PENDING/DONE (pending_motions + MotionDone lifecycle) **NONE.** There is no `pending_motions` queue and no `MotionDone` callback/event anywhere in the codebase (confirmed via `grep -rn "pending_motion|MotionDone|PendingMotion" src/` — zero hits). What DOES exist that's adjacent: - `AnimationSequencer._queue` (a `LinkedList`, `AnimationSequencer.cs:297`) holds link-then-cycle nodes, but it is not a "pending motion command" queue in the retail sense — it's purely an animation-frame playback queue (link frames + one looping tail node), rebuilt wholesale on every `SetCycle` call. There's no notion of a QUEUED but not-yet-active MOTION COMMAND (e.g. "play attack after current swing finishes") — `SetCycle` always either fast-paths (same command) or fully rebuilds (different command) synchronously the instant the wire event arrives. - `AnimationSequencer.ConsumePendingHooks()` (964-972) exists and drains `_pendingHooks`, a `List` accumulated during `Advance()` when integer frame boundaries are crossed (`Advance`, 862-957, calls `ExecuteHooks` at 913/939). This is a HOOK dispatch mechanism (audio/VFX/damage triggers baked into animation frames), NOT a motion-command completion signal. - `AnimationDoneSentinel` (`AnimationSequencer.cs:1128-1129`, a static `AnimationHookDir.Both` hook) is added to `_pendingHooks` when a non-cyclic (link) node drains naturally and the sequence is about to wrap (948-951, inside `Advance`). This is the closest thing to a "link finished" signal, but it's a generic hook fired into the SAME `_pendingHooks` list as animation-authored hooks (footsteps, damage triggers) — there's no separate consumer that reacts specifically to "this MOTION COMMAND completed" the way retail's `pending_motions`/`MotionDone` state machine would (e.g. to know when to pop the next queued attack, or to notify the wire layer that a swing animation finished so the next command can be sent). - **Consequence for callers**: nobody appends anything to a "next motion" queue and nobody pops one on completion. Every UM (or per-tick refinement call) calls `SetCycle`/`DoInterpretedMotion` directly and unconditionally; the ONLY "which motion wins" arbitration is (a) the SubState-vs-Overlay classification in `OnLiveMotionUpdated` (4626 vs 4637) and (b) `SetCycle`'s own state (`CurrentMotion`/`CurrentStyle` comparison) — there's no notion of "motion command A is still pending completion, defer motion command B." --- ## Q4 — CYCLE SWAP mechanics: does frame index carry over? Governed entirely by `AnimationSequencer.SetCycle` (see Q2 for the walk→run specific path) and `AdvanceToNextAnimation` (`AnimationSequencer.cs:1344-1364`): - **General case (non-locomotion-direct, e.g. Ready→WalkForward)**: `_currNode` is set to the FIRST NEWLY-ENQUEUED node (`firstNew`, 583, 636-640) — i.e. the link animation, if one exists, plays from ITS `GetStartFramePosition()` (583+639: `_framePosition = _currNode.Value.GetStartFramePosition()`), which for positive framerate is `StartFrame` (frame 0 of the link) — **frame index does NOT carry over from the old cycle; it restarts at the link's (or cycle's, if no link) start frame.** When the link naturally exhausts during subsequent `Advance()` calls, `AdvanceToNextAnimation()` (1344) walks to `_currNode.Next`, or if null, wraps to `_firstCyclic` — again restarting `_framePosition` at `GetStartFramePosition()` of that new node (1362-1363). - **Locomotion-direct case (Walk↔Run and similar, per the "Fix B" special-case, `AnimationSequencer.cs:619-635`)**: `_currNode` is forced DIRECTLY onto `_firstCyclic` (the new cycle's looping node), with `_framePosition = _firstCyclic.Value.GetStartFramePosition()` (634) — i.e. **frame index is reset to 0 (or `EndFrame` for negative-speed cycles) of the NEW cycle, NOT carried over from the OLD cycle's current frame position.** There is no "keep the current phase, just swap animation data" — every `SetCycle` transition, link or no link, hard-resets `_framePosition` to a start boundary of whichever node becomes current. - **No "link animation between the two loops" mechanism for locomotion-direct swaps** — the whole point of the "Fix B" branch is to SKIP the link (i.e. skip the transitional pose entirely) for cyclic→cyclic transitions, per the cdb-trace-derived retail-matching behavior documented in the comment (609-618). - Net: cycle swap between two looping cycles is an **instant hard-cut with no phase continuity and (for locomotion-direct transitions) no transitional/link animation.** This differs from a "true" retail per-limb blend if such a thing exists in the more complete engine — acdream's sequencer is a single active `AnimNode` playback engine, not a blend-tree. --- ## Q5 — POSITION DRIVE between inbound position packets Two structurally DIFFERENT paths exist, selected in `TickAnimations` (`GameWindow.cs:9717-9722`) by: ```csharp if (ae.Sequencer is not null && serverGuid != 0 && serverGuid != _playerServerGuid && _remoteDeadReckon.TryGetValue(serverGuid, out var rm)) { if (IsPlayerGuid(serverGuid) && !rm.Airborne) // ← Path A: grounded PLAYER remotes else // ← Path B: NPCs + airborne player remotes (legacy) } ``` ### Path A — grounded player remotes (`GameWindow.cs:9722-9920`) Uses `PositionManager.ComputeOffset` (`PositionManager.cs:51-107`), which is explicitly a **REPLACE, not additive** combiner: 1. `InterpolationManager.AdjustOffset(dt, currentBodyPosition, maxSpeed)` (called at `PositionManager.cs:84`) is tried FIRST. - If the queue has waypoints AND the body is farther than `DesiredDistance` (0.05 m, `InterpolationManager.cs:79`) from the head waypoint, it returns a non-zero "catch-up" delta: `direction × min(catchUpSpeed × dt, dist)`, where `catchUpSpeed = maxSpeedFromMinterp × MaxInterpolatedVelocityMod (2.0)` or a `MaxInterpolatedVelocity` (7.5 m/s) fallback (`InterpolationManager.cs:262-362`). **This REPLACES animation root motion entirely for that frame** (`PositionManager.cs:84-86`: `if (correction.LengthSquared() > 0f) return correction;`). - If the queue is empty OR the head has been reached (dist ≤ 0.05m, which pops the head via `NodeCompleted`), `AdjustOffset` returns `Vector3.Zero`, and `PositionManager.ComputeOffset` falls through to animation root motion: `seqVel × dt`, rotated by body orientation, optionally slope-projected (`PositionManager.cs:88-106`). 2. `seqVel` is `ae.Sequencer.CurrentVelocity` (body-local, from the sequencer synthesis in Q2/Q4) — this is the ANIMATION-DERIVED velocity, NOT `MotionInterpreter.get_state_velocity()`. 3. `omegaToApply` is `rm.ObservedOmega` (seeded formulaically from wire TurnCommand+TurnSpeed in `OnLiveMotionUpdated`, `GameWindow.cs:4841-4847`) preferred over `seqOmega` (`GameWindow.cs:9842-9845`). 4. `rm.Body.calc_acceleration()` + `rm.Body.UpdatePhysicsInternal(dt)` still run (9878, 9881) for gravity/vertical integration, but `ResolveWithTransition` (collision sweep) is INTENTIONALLY SKIPPED for this path (comment 9883-9890: "the server has already collision-resolved the broadcast position"). 5. **Dominance**: for grounded player remotes, the InterpolationManager queue DOMINATES whenever it's non-empty and the body hasn't caught up; the sequencer's `CurrentVelocity` (animation root motion) only drives position when the queue is idle/caught-up. ### Path B — NPCs and airborne player remotes (legacy path, `GameWindow.cs:9933-10267+`) This is the ORIGINAL / "unchanged until Task 8 cleanup" path (comment 9923): 1. `rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false)` (10074, the general branch) — calls `MotionInterpreter.get_state_velocity()` (`MotionInterpreter.cs:639-688`), which reads `InterpretedState.ForwardCommand`/`ForwardSpeed` (bulk-copied from the wire in `OnLiveMotionUpdated`, Q1/Q2) and returns `WalkAnimSpeed/RunAnimSpeed × ForwardSpeed` UNLESS the sequencer's `CurrentVelocity.Y` is non-zero (`GetCycleVelocity` accessor, wired — see below), in which case it prefers that (`MotionInterpreter.cs:653-668`, "Option B — MotionData-sourced forward velocity"). Result is written directly to `PhysicsBody.set_local_velocity` via `apply_current_movement` (`MotionInterpreter.cs:905-909`, gated on `PhysicsObj.OnWalkable`). 2. Alternative sub-branches at this same call site (`GameWindow.cs:9963-10076`) select DIFFERENT velocity sources depending on state: - If `rm.HasServerVelocity` and NOT a player guid (9968): uses `rm.ServerVelocity` (synthesized from consecutive UP position deltas, or wire `Velocity` field when present) directly as `rm.Body.Velocity` — BYPASSING `apply_current_movement`/`get_state_velocity` entirely — UNLESS the velocity is stale (`velocityAge > ServerControlledVelocityStaleSeconds = 0.60s`, 926), in which case it's zeroed and `ApplyServerControlledVelocityCycle` is invoked to reset the cycle to Ready. - If `rm.ServerMoveToActive && rm.HasMoveToDestination` (9987): uses `RemoteMoveToDriver.Drive` to steer orientation, then `apply_current_movement` for velocity magnitude (10042), clamped via `RemoteMoveToDriver.ClampApproachVelocity` (10052-10059) to avoid overshoot. - Else (general fallback, 10074): `apply_current_movement` as described in step 1. 3. Manual per-tick rotation integration (`GameWindow.cs:10097-10106`) using `rm.ObservedOmega`. 4. `rm.Body.UpdatePhysicsInternal(dt)` (10128) — Euler integration. 5. **Collision sweep IS run for Path B** (`_physicsEngine.ResolveWithTransition`, 10156-10181) — unlike Path A, NPCs/airborne players DO get a client-side sweep between server updates (with a fixed sphere radius/height, step-up/down heights). ### Which dominates? - Path A (player remotes, grounded): InterpolationManager queue dominates when active; sequencer's synthesized `CurrentVelocity` (animation root motion) is the fallback when the queue is idle. - Path B (NPCs / airborne players): `MotionInterpreter.get_state_velocity()` (fed by `InterpretedState.ForwardCommand/ForwardSpeed` set from the wire) is the PRIMARY driver for NPCs without explicit server velocity; when the sequencer's `CurrentVelocity.Y` is non-zero, `get_state_velocity` PREFERS it over the hardcoded WalkAnimSpeed/RunAnimSpeed constants (`MotionInterpreter.cs:646-668` "Option B"). Wire-synthesized `ServerVelocity` from position deltas takes priority over both when present and fresh. - **Neither `apply_raw_movement` nor the raw `adjust_motion`(ref motion, ref speed, holdKey) entry point is called anywhere in the inbound remote path** (confirmed via `grep -rn "apply_raw_movement|get_state_velocity()"` across `src/` outside `MotionInterpreter.cs` — only `src/AcDream.App/Input/PlayerMovementController.cs` calls them, exclusively for the LOCAL player's own body, lines 761/880/909/958/974/1009-1014). The D6.2 `apply_raw_movement`/`adjust_motion` port (per CLAUDE.md's "D6.2a/D6.2b" commits) is LOCAL-PLAYER-ONLY; remote entities never route through it. Remotes instead get `InterpretedState.ForwardCommand`/`ForwardSpeed` bulk-copied directly (mirroring retail's `copy_movement_from`, per the comment at `GameWindow.cs:4536-4544,4552-4558`), bypassing `adjust_motion`'s left→right/backward-forward normalization and run-hold-key promotion — that normalization already happened SERVER-SIDE (ACE) before the wire command arrived, per the code comments. --- ## Q6 — CORRECTION (how inbound UpdatePosition corrects accumulated error) Handled in `OnLivePositionUpdated` (`GameWindow.cs:5334-~5773`). The correction strategy differs by whether the remote is a PLAYER or not, and whether it's airborne: ### Player remotes (`IsPlayerGuid(update.Guid)` branch, `GameWindow.cs:5460-5635`) Ports retail's `CPhysicsObj::MoveOrTeleport` (0x00516330): 1. **Airborne no-op** (5512-5521): if `!update.IsGrounded`, body position/velocity are NOT touched by the UP at all — gravity integration continues locally; only the render entity is synced to the current body position (undoing the unconditional top-of-function hard-snap). 2. **Landing transition** (5527-5553): first grounded UP after airborne clears `Airborne`, zeroes velocity, hard-snaps `rmState.Body.Position = worldPos` directly (no queue), clears the interpolation queue, and resets the sequencer cycle from Falling to whatever `InterpretedState.ForwardCommand` implies. 3. **Grounded routing** (5555-5581): distance check against the LOCAL player's position (`MaxPhysicsDistance = 96f`, 5556): - `dist > 96`: hard `SetPositionSimple`-style snap — `rmState.Interp.Clear(); rmState.Body.Position = worldPos;` (no smoothing at all). - `dist <= 96`: `rmState.Interp.Enqueue(worldPos, headingFromQuat, isMovingTo:false, currentBodyPosition:rmState.Body.Position)` — feeds `InterpolationManager.Enqueue` (`InterpolationManager.cs:170-231`), which itself has three sub-branches: - **Far branch** (dist from last-tail/body-position to new target > `AutonomyBlipDistance = 100m`): enqueues AND pre-arms an immediate blip (`_failCount = StallFailCountThreshold + 1 = 4`) so the very next `AdjustOffset` call snaps straight to the tail. - **Already-close branch** (body-to-target ≤ `DesiredDistance = 0.05m`): wipes the queue entirely, no enqueue (`Clear()`). - **Near/not-close branch**: tail-prune loop collapses consecutive stale entries within 0.05m of the new target, caps queue at 20 (drops head if full), then appends. - Every-tick catch-up happens later in `TickAnimations` Path A via `PositionManager.ComputeOffset`→`InterpolationManager.AdjustOffset` (Q5). There is a secondary "stall" detector inside `AdjustOffset` (`InterpolationManager.cs:292-337`): every 5 frames it checks cumulative progress against `MinDistanceToReachPosition = 0.20m` and a secondary ratio check (`cumulative/progressQuantum/dt >= 0.30`); repeated stall failures (`_failCount > StallFailCountThreshold = 3`) trigger a hard SNAP to the tail waypoint (`InterpolationManager.cs:345-350`) and clear the queue. 4. **Velocity-fallback cycle refinement** (5582-5625): synthesizes velocity from consecutive server positions (`(worldPos - PrevServerPos) / dtPos`) and feeds `ApplyServerControlledVelocityCycle` → `ApplyPlayerLocomotionRefinement` (Q7-adjacent — see below) to correct the VISIBLE CYCLE (not just position) when retail's wire is silent on a HoldKey-only Shift toggle. 5. `entity.SetPosition(rmState.Body.Position)` at the end (5633) — the render entity always mirrors the body, never `worldPos` directly, for player remotes within the near-branch (queue chase still in progress). ### Non-player remotes (`GameWindow.cs:5637-5767`) NO InterpolationManager/PositionManager usage at all — this is the older "legacy" correction: 1. `serverVelocity` sourced from wire `update.Velocity` if present, else synthesized from position delta if `rmState.LastServerPosTime > 0` (5638-5646). 2. `rmState.Body.Position = worldPos` — **UNCONDITIONAL HARD SNAP, every single UpdatePosition, no soft-lerp, no distance check** (5657). Comment at 5679-5682 confirms: "Retail authoritatively hard-snaps cell membership here too." 3. `rmState.Body.Orientation = rot` — also hard-snapped unconditionally (5693, comment 5685-5692: rotation-rate between UPs instead comes from the formula-seeded `ObservedOmega`, not from UP deltas — an earlier attempt to derive omega from UP deltas caused a "halved observed rate" bug on the first UP after a turn). 4. `rmState.Body.Velocity = svel` if wire `Velocity` present (5711-5713); **`svel.LengthSquared() < 0.04f` is treated as an explicit stop signal** — triggers `rmState.Motion.StopCompletely()` (Q7) PLUS a direct `SetCycle` to Ready (5714-5731). This is the ONLY place `StopCompletely()` is called anywhere in the inbound path. 5. Between UPs (Path B in `TickAnimations`), NPCs get a client-side `ResolveWithTransition` collision sweep — see Q5. ### Named thresholds used for correction | Constant | Value | File:line | Purpose | |---|---|---|---| | `SnapHardSnapThreshold` | 20.0 m | `GameWindow.cs:614` | (declared, comment references retail `GetAutonomyBlipDistance`; NOT directly read in the reviewed OnLivePositionUpdated flow for non-player remotes — hard snap there is unconditional every UP) | | `MaxPhysicsDistance` | 96 m | `GameWindow.cs:5556` | Player-remote far-vs-near routing (SetPositionSimple vs Interp.Enqueue) | | `InterpolationManager.AutonomyBlipDistance` | 100 m | `InterpolationManager.cs:105` | Far-branch pre-arm blip in `Enqueue` | | `InterpolationManager.DesiredDistance` | 0.05 m | `InterpolationManager.cs:79` | "Reached"/"already close" radius | | `InterpolationManager.MinDistanceToReachPosition` | 0.20 m | `InterpolationManager.cs:73` | 5-frame stall-check primary threshold | | `InterpolationManager.StallFailCountThreshold` | 3 (fires on >3, i.e. 4th fail) | `InterpolationManager.cs:98` | Snap-to-tail trigger | | `ServerControlledVelocityStaleSeconds` | 0.60 s | `GameWindow.cs:926` | NPC server-velocity staleness → zero + Ready | | `RemoteMoveToDriver.StaleDestinationSeconds` | 1.5 s | `RemoteMoveToDriver.cs:136` | MoveTo destination staleness → stand down | --- ## Q7 — STOP (motion → Ready/stand) There are **THREE distinct, independently-triggered stop paths** in the current code, none of which are unified: ### 1. UM-driven stop (`OnLiveMotionUpdated`, the "retail stop signal") When the wire's `ForwardCommand` flag is absent or explicitly 0 AND it's not a MoveTo packet (`GameWindow.cs:4347,4364-4367`): `fullMotion = 0x41000003u` (Ready) is used directly as the SetCycle target. This flows through the normal `SetCycle` full-rebuild path (Q2/Q4) — Ready is NOT flagged as a "locomotion" low-byte (`IsLocomotionCycleLowByte` only covers 0x05/0x06/0x07/0x0F/0x10, `AnimationSequencer.cs:1509-1513`), so a Run→Ready transition DOES get the link animation this time (the "Fix B" cyclic-direct bypass only applies when BOTH old and new are locomotion). `remoteMot.Motion.InterpretedState.ForwardCommand`/`ForwardSpeed` are ALSO bulk-copied to `fullMotion`/`speedMod` = Ready/1.0 UNCONDITIONALLY at the same time (4590,4598) — so `MotionInterpreter.get_state_velocity()` will subsequently return `Vector3.Zero` (neither WalkForward nor RunForward matches Ready) for Path B (NPC) velocity. **No explicit `StopCompletely()`/`StopInterpretedMotion()` call happens in this path** — it's purely a state overwrite via the same "InterpretedState = wire's forward/speed" bulk-copy used for every other UM. ### 2. UP-driven stop for non-player remotes (near-zero wire velocity) `GameWindow.cs:5711-5731`: only fires when the wire's `UpdatePosition.Velocity` field is explicitly present AND its length² < 0.04 (i.e. |v| < 0.2 m/s). Calls `rmState.Motion.StopCompletely()` (`MotionInterpreter.cs:562-587`) which resets BOTH `RawState` and `InterpretedState` to Ready/1.0/0/0/0/1.0 across all three axes (forward/sidestep/turn) and calls `PhysicsObj.set_velocity(Vector3.Zero)` directly — this IS an explicit velocity zero. Additionally forces `ae.Sequencer.SetCycle(curStyle, readyCmd)` (5729) so the visible cycle snaps to Ready too, independent of whatever UM might arrive later. Comment (5700-5710) notes this UP-absent-velocity path was previously a bug source ("fired StopCompletely every UP → intermittent run") and is now scoped ONLY to the explicit-near-zero case, NOT absent-velocity. ### 3. Player-remote UM stop (no distinct path — same as #1, but note the render/position split) For player remotes, `OnLiveMotionUpdated`'s player branch (4417-4492) explicitly SKIPS the sequencer entirely for the LOCAL player's OWN echoed UM. For OTHER players (genuine remotes), the same #1 mechanism applies. **Position, however, is decoupled**: even after a Ready UM zeroes `InterpretedState`/switches the sequencer to Ready, the `InterpolationManager` queue (Path A, Q5) may still hold unconsumed waypoints from the last few UPs sent WHILE the player was still moving (the network is asynchronous — UM and UP for the same "I stopped" event may arrive in either order or with a gap). Since `PositionManager.ComputeOffset` prefers the queue's catch-up delta over `seqVel` whenever the queue is non-empty (`PositionManager.cs:84-86`), **the body can continue sliding toward the last few queued waypoints via `InterpolationManager.AdjustOffset`'s catch-up motion EVEN AFTER the sequencer has already switched to a zero-velocity Ready cycle** — because the queue-driven correction is independent of `CurrentVelocity`. This is a structural potential for residual sliding: the stop signal (UM→Ready) zeroes ANIMATION velocity but does NOT clear `rmState.Interp`'s queue; the queue only self-empties via normal `NodeCompleted` head-pop as the body reaches each remaining waypoint (`InterpolationManager.cs:278-282`), or by explicit `Clear()` calls which only happen at: (a) far-branch enqueue is NOT a clear, (b) already-close enqueue (0.05m gate), (c) `AdjustOffset`'s stall-blip clear, (d) the landing-transition block (5534, airborne case only). **There is no explicit `rm.Interp.Clear()` call anywhere in the UM-driven stop path (#1) or in the near-zero-UP stop path (#2)** — confirmed by reading `GameWindow.cs:4230-4967` (OnLiveMotionUpdated, no `Interp.Clear` call) and `GameWindow.cs:5711-5731` (the UP near-zero-velocity StopCompletely block, no `Interp.Clear` call there either — only the airborne-landing block at 5534 calls `rmState.Interp.Clear()`). ### Residual-sliding mechanisms explicitly present elsewhere (not stop-specific) - `SnapResidualDecayRate = 8.0f` (1/sec, `GameWindow.cs:597`) is DECLARED with a comment describing an exponential decay of "soft-snap residual" (100ms half-life) — but this constant is not exercised in any of the code paths read in this pass (`OnLivePositionUpdated`/`OnLiveMotionUpdated`/`TickAnimations` Path A/B); it may be vestigial or used in a renderer-only smoothing pass not covered by this trace. Flagging as unconfirmed — did not find a live call site using this constant during this investigation. ### Summary answers - (a) **Where remote DR velocity comes from**: for grounded PLAYER remotes, `AnimationSequencer.CurrentVelocity` (synthesized constants × speedMod, set inside `SetCycle`, NOT from `MotionInterpreter.get_state_velocity()`), used ONLY when the InterpolationManager queue is idle; when the queue is active, DR position instead comes from `InterpolationManager.AdjustOffset`'s catch-up formula (`maxSpeedFromMinterp × 2.0`, itself sourced from `MotionInterpreter.GetMaxSpeed()` = `RunAnimSpeed(4.0) × runRate`). For NPCs (and airborne players), DR velocity comes from `MotionInterpreter.get_state_velocity()` (which itself prefers the sequencer's `CurrentVelocity.Y` when non-zero, else falls back to `WalkAnimSpeed/RunAnimSpeed × InterpretedState.ForwardSpeed`), OR directly from `rm.ServerVelocity` (wire `Velocity` field or position-delta synthesis) when `HasServerVelocity` is true and fresh. - (b) **When DR velocity changes after a new UM**: SAME FRAME / same synchronous call — `OnLiveMotionUpdated` bulk-copies `InterpretedState.ForwardCommand/ForwardSpeed` immediately (`GameWindow.cs:4590,4598`) and calls `SetCycle` immediately (4769), which synthesizes `CurrentVelocity` immediately inside the same call (`AnimationSequencer.cs:722-754`). There is no queueing/deferral — the very next `TickAnimations`/`Advance()` call (next frame) will observe the new velocity. - (c) **Can animation phase and DR velocity disagree, for how long**: YES, structurally, in at least two ways: (1) For player remotes, `PositionManager` can be driving position via the InterpolationManager QUEUE (catch-up toward stale waypoints) while the SEQUENCER has already advanced to a new cycle/phase from a fresher UM — these two systems are not coupled; the queue drains independently at its own catch-up rate (up to `GetMaxSpeed()×2.0` or 7.5 m/s fallback) regardless of what the currently-playing animation phase implies. (2) The `SetCycle` cyclic-direct hard-cut (Q2/Q4) resets `_framePosition` to a start boundary instantly on every walk↔run toggle, so the visible LEG PHASE has zero continuity with the body's actual accumulated forward distance at the moment of the toggle — there's no phase-matching/re-sync logic. Duration of disagreement is unbounded in principle (bounded in practice only by how quickly the InterpolationManager queue catches up, or by the next UM/UP hard-snap). - (d) **How stop zeroes velocity, what can leave residual sliding**: `StopCompletely()` zeroes `PhysicsObj.set_velocity(Vector3.Zero)` directly on the body AND resets `InterpretedState`, but this ONLY fires from the UP near-zero-velocity path (#2 above) — the far more common UM-driven Ready transition (#1) only zeroes the ANIMATION's synthesized `CurrentVelocity` (via `SetCycle`'s Ready branch, which is not a "locomotion low byte" so gets no velocity synthesis at all — `CurrentVelocity` simply stays whatever it was set to by `ClearPhysics()` at the top of `SetCycle`, i.e. `Vector3.Zero`, UNLESS a link with `HasVelocity` overrides it — Ready transitions DO get a link per Q2, and if that link's MotionData has `HasVelocity` set, `CurrentVelocity` would be non-zero for the DURATION of that link before wrapping to the (velocity-less, non-locomotion) Ready cycle). Meanwhile, for player remotes specifically, **NEITHER of the two stop paths clears `rmState.Interp`'s waypoint queue** — so if the queue still holds unreached waypoints from before the stop signal, `PositionManager.ComputeOffset`/`InterpolationManager.AdjustOffset` will continue producing non-zero catch-up deltas each tick (up to the stall-detection kicking in after 5 frames / `_failCount` exceeding 3), producing exactly the "residual sliding after stop" symptom the task is asking about. This is a genuine, code-confirmed gap: stop-signal handling and interpolation-queue clearing are two independent systems that are not wired together outside of the airborne-landing special case. --- ## Cross-cutting notes for synthesis - **Two entirely separate motion-drive architectures coexist**: the newer InterpolationManager/PositionManager "queue + REPLACE" model (grounded player remotes only) vs. the older `MotionInterpreter.apply_current_movement`/`get_state_velocity` "continuous velocity from InterpretedState" model (NPCs + airborne remotes). They share almost no code and have different stop/correction semantics. - **`MotionInterpreter`'s D6.2 `adjust_motion`/`apply_raw_movement` port is entirely unused by the inbound remote path** — remotes get server-pre-normalized commands bulk-copied directly into `InterpretedState`, bypassing the retail-faithful raw→interpreted normalization pipeline that exists in the codebase but is wired to the LOCAL player only (`PlayerMovementController.cs`). - **No unified "stop" concept**: three independent triggers (UM Ready, UP near-zero-velocity StopCompletely, airborne-landing zero) each partially zero different pieces of state (animation velocity only / full body+interpreted-state velocity / position+velocity+cycle), and none of them touch the InterpolationManager queue except the airborne-landing case. - **`SetCycle`'s cyclic-locomotion-direct special case (Q2/Q4) is itself a documented, intentional divergence from the general link-transition mechanism**, justified in-code by a cdb trace of retail's `add_to_queue` behavior — i.e. per CLAUDE.md's divergence-register discipline, this specific behavior claims retail-fidelity (not an acknowledged approximation), though this investigation did not independently verify the cdb trace's conclusiveness.