# Handoff — #170 creature chase renders as slide (PARTIAL FIX landed; residual = "sustain the run") — 2026-07-04 Fresh session picks up HERE for #170. Worktree `vigorous-joliot-f0c3ad`, branch `claude/vigorous-joliot-f0c3ad`. Tree CLEAN at **`427332ac`** (the partial fix + env-gated probes). This session root-caused #170 end-to-end with **live retail cdb tracing** + acdream runtime probes and landed a **verified partial fix**. One residual remains (the run isn't fully sustained). This doc is the SSOT; the paste prompt is `docs/research/2026-07-04-170-pickup-prompt.md`. ## TL;DR A monster chasing a fleeing player renders as a **slide** in acdream (glides toward you in an idle/attack pose) vs **runs to close, stops to swing** in retail (same local ACE). Root cause, proven: - The chase **run cycle is manufactured client-side** from the `mt-6` MoveToObject stream: `HandleUpdateTarget → MoveToObject_Internal → (TurnToHeading node completes via UseTime) → BeginMoveForward → _DoMotion(RunForward)` sets the motion-table **substate = RunForward** (the legs). `RunForward` is NEVER on the wire — the server only sends mt-6 + the attack UMs. - `MoveToManager.BeginTurnToHeading` (retail `0x00529b90`) bails while `CMotionInterp.motions_pending` is non-empty (**retail-faithful guard**). - acdream's `pending_motions` **exploded to ~1.3M entries** because the NPC per-tick called `rm.Motion.apply_current_movement` **every frame**, re-dispatching the whole interpreted state (stance + attack + stops) and appending a `pending_motions` node each time. `MotionsPending()` stayed permanently true → the chase turn never started → `BeginMoveForward/RunForward` ~never fired → slide. - **FIX (landed `427332ac`):** delete the per-frame `apply_current_movement` in the grounded remote-NPC path (`GameWindow.cs` ~9992). Retail dispatches per motion **event** (per UM), never per frame. **Result (verified live):** flood 1.3M → depth ~1 (add≈done); "stuck in attack animation" GONE (user-confirmed); run cycle installs (`BeginMoveForward` 1→10, `RunForward` held 0→7). **PARTIAL** — still not fully sustained (below). ## The one remaining residual (= the next session's job) The run isn't sustained: `BeginTurnToHeading` is still blocked `motionsPending=True` **256/272 (94%)** of the time, because a small **`Ready` (0x41000003) stop-node backlog** keeps `pending_motions` from ever fully emptying between swings. acdream gets ~10 run-starts to **retail's 21**, so it now **twitches forward + glides** (short run bursts + idle) instead of a clean run-then-stop. **Where the `Ready` backlog comes from (traced, not guessed):** - `MotionInterpreter.StopInterpretedMotion` (`MotionInterpreter.cs:3254`) appends a `Ready` `pending_motions` node (line 3292) whenever the stop **succeeds** (`sink.StopMotion` → `CMotionTable.StopSequenceMotion` returns true). - `StopSequenceMotion` (`CMotionTable.cs:559`) returns **false** for a *redundant* sidestep/turn stop (Case A: not our substate; Case B: no matching modifier) — so those correctly DON'T queue `Ready`. The `Ready` nodes that DO accumulate come from stops that **succeed**: chiefly the per-UM tail `StopInterpretedMotion(TurnRight)` (`MotionInterpreter.cs:3008`) stopping the MoveTo's OWN steering turn, plus the MoveTo-cancel `StopCompletely`. - Those `Ready` nodes drain via `MotionDone` (fired 1:1 by `MotionTableManager` per completed `_pendingAnimations` entry — `AnimationDone` per AnimDone hook `GameWindow.cs:10306` + `UseTime`→`CheckForCompletedMotions` 0-tick sweep at `10309`). They drain **a beat slower** than they add → backlog grows (lag 1→10 over a chase). Retail hits `add_to_queue == MotionDone` **exactly** (cdb-proven). **The decisive open question for the fix:** WHY do acdream's `Ready` stop-nodes drain slower than retail's? Two concrete hypotheses to test with a retail cdb trace (retail is the oracle; do NOT guess in this verbatim-ported R2/R3 machinery): 1. **Tick count.** acdream's `Ready` stop entries have `outTicks > 0` (from `StopSequenceMotion` Case A → `GetObjectSequence(styleDefault, stopCall:true, out outTicks)`), so they wait for `AnimationDone` instead of draining immediately via the per-frame 0-tick `CheckForCompletedMotions`. Retail's may be 0-tick. → cdb: break `CMotionTable::StopObjectMotion`/`GetObjectSequence` on a live chasing monster, read the returned tick count for a turn-stop. 2. **Drain trigger rate.** Retail may fire `CheckForCompletedMotions`/`MotionDone` more aggressively than acdream's once-per-frame `Manager.UseTime`. → cdb: count `CMotionInterp::MotionDone` vs `add_to_queue` (already have the script: `scratchpad/cdb-drain.cdb` — retail = add==done) and add `CPhysicsObj::CheckForCompletedMotions`/`MotionTableManager::UseTime` counts. Target acceptance: acdream's `pending_motions` drains to `add==done` (fully empties between swings like retail) → `BeginTurnToHeading` proceeds per mt-6 arm → `BeginMoveForward ≈ MoveToObject` (retail was 21≈22) → the creature runs to close distance, plants to swing. **Visual gate:** user runs acdream + retail side-by-side. ## Evidence table (live traces this session) | Metric | retail (cdb) | acdream BEFORE fix | acdream AFTER fix (`427332ac`) | |---|---|---|---| | `pending_motions` add vs done | **254 == 254** | 1.37M vs 5.7K | 425 vs 424 | | max `pending_motions` depth | shallow | **1,332,575** | ~1–2 | | `BeginMoveForward` (run installs) / chase | 21 | 1 | 10 | | `MoveToObject` arms / chase | 22 | (7 in an attack-heavy cap) | 32 | | `HandleUpdateTarget` (voyeur re-drive) | 689 | 44 | fires | | `BeginTurnToHeading` motionsPending True/False | (empties often) | n/a | **256 / 16** | | lingering queue contents | — | 0x8000003C×671K (stance) | **0x41000003 (Ready) backlog** | ## Apparatus (all in place at `427332ac`; reuse, then strip when #170 closes) **acdream env-gated probes** (`ACDREAM_MVTO_DIAG=1`, alongside `ACDREAM_DUMP_MOTION=1`): - `MoveToManager.cs` `s_mvtoDiag`: `[mvto] BeginMoveForward cmd=…`, `HandleUpdateTarget … match=… init=… mtState=…`, `CancelMoveTo (real) wasType=…`, `UseTime enter/dispatch node=…`, `BeginTurnToHeading motionsPending=… curCmd=…`. - `MotionInterpreter.cs` `s_drainDiag`: `[drain] add=… done=… lag=… depth=…` (aggregate add_to_queue vs MotionDone) + `[drainq] depth=… q=[…]` (queue CONTENTS when depth≥2 — this is what named `Ready` as the lingering node). - `GameWindow.cs`: `[npc-tick] guid=… branch=SERVERVEL|MOVETO` (which per-tick branch), + the earlier `UM ↳ actions …` inbound-action dump (capture aid). **Launch (user manages lifecycle; graceful close):** ``` $env:ACDREAM_DUMP_MOTION="1"; $env:ACDREAM_MVTO_DIAG="1" # + ACDREAM_LIVE/DAT_DIR/host/port/user/pass dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object launch.log ``` In-world: **aggro a Mite Scamp at the wilderness spawn (~0xADAF), RUN AWAY so it CHASES** (the bug is the chase, not attack-in-place — no chase ⇒ no `[mvto]` lines). Logs are UTF-16 → `tr -d '\000'` before grep. **Retail cdb toolchain** (Step -1; retail is the oracle for the residual): - Binary `C:\Turbine\Asheron's Call\acclient.exe` MATCHES `refs/acclient.pdb` (`py tools/pdb-extract/check_exe_pdb.py "…/acclient.exe"`). cdb at `C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe`. - Working scripts saved in the scratchpad this session: `cdb-lookup.cdb` (symbol names), `cdb-count.cdb` / `cdb-chase.cdb` (BeginMoveForward/MoveToObject/ HandleUpdateTarget/RunForward-dispatch counts), `cdb-drain.cdb` (add_to_queue vs MotionDone vs motions_pending). Pattern: count + `gc`, auto-`qd` after N `move_to_interpreted_state` hits (user makes a monster chase retail during the trace). Key addrs: `MoveToManager::BeginMoveForward 0x00529a00`, `_DoMotion 0x00529010`, `CancelMoveTo 0x00529930`, `MoveToObject 0x00529680`, `HandleUpdateTarget 0x0052a7d0`, `UseTime 0x0052a780`; `CMotionInterp::move_to_interpreted_state 0x005289c0`, `DoInterpretedMotion 0x00528360`, `add_to_queue 0x00527b80`, `MotionDone 0x00527ec0`, `motions_pending 0x00527fe0`, `get_state_velocity 0x00527d50`. ## DO-NOT-RETRY / superseded - **The `eb423fb7` "MovementManager coexistence / R5-V4" hypothesis is WRONG** — the attack UM cancelling the MoveTo is retail-faithful and NOT the cause. Superseded by this doc. (Register/ISSUES updated.) - The `d2ccc80e` velocity fix (`get_state_velocity` → `set_local_velocity` each grounded tick) is CORRECT but **position-only** — it does NOT fix the legs; keep it. - `CombatAnimationPlanner` (#159, fixed `2de5a011`) was a **red herring** for the Mite Scamp (its 0x62/63/64 attacks were always in the correct block; the planner is unwired). Real #170 is the MoveTo/pending_motions chain above. - `BeginTurnToHeading`'s `if (motions_pending) return` guard is **retail-faithful** — do NOT patch it. Fix the DRAIN so the queue empties, don't remove the guard. - The per-frame `apply_current_movement` deletion is the ROOT fix for the flood (retail dispatches per UM). Do NOT re-add it. ## Key file:line map - `GameWindow.cs` ~9982 (grounded remote-NPC branch; the deleted per-frame apply_current_movement + the kept `get_state_velocity` velocity refresh); ~10306/10309 (the pending-motions DRAIN: `Manager.AnimationDone` per AnimDone hook + `Manager.UseTime`); ~4251 `EnsureRemoteMotionBindings` (MoveToManager + TargetManager voyeur wiring, `getSelfId=serverGuid` correct). - `MoveToManager.cs`: `BeginMoveForward:741`, `BeginTurnToHeading:821` (`if (_interp.MotionsPending()) return` at ~833), `HandleTurnToHeading:1164`, `HandleUpdateTarget:1229`, `MoveToObject_Internal:1333`, `UseTime:953`, `CancelMoveTo:1475`. - `MotionInterpreter.cs`: `AddToQueue:2297` (pending_motions add), `MotionsPending:2298`, `MotionDone:2318` (pops one head), `ApplyInterpretedMovement:2920` (style→forward→sidestep-stop→turn-stop), `StopInterpretedMotion:3254` (adds `Ready` on success, line 3292). - `CMotionTable.cs`: `StopSequenceMotion:559` (false for redundant stop), `StopObjectMotion:640`, `GetObjectSequence` (Branch 1 substate write; Branch 3 action overlay — verified faithful). - `MotionTableManager.cs`: `AnimationDone:290`, `CheckForCompletedMotions:322`, `UseTime:342`, `PerformMovement:409` (StopInterpreted → AddToQueue(Ready,ticks)). ## Session gotchas - Client + retail both hit the SAME local ACE (127.0.0.1:9000). acdream char `+Acdream` (0x5000000A) spawns in the wilderness (~0xADAF) with Mite Scamps. - Two workflow tracers returned junk/schema-cap this session; the synthesis+verify carried it BUT its "run cycle from mt-6/BeginMoveForward" map was later REFUTED for attack-in-place (BeginMoveForward=0) and CONFIRMED only for the CHASE — always trace the CHASE scenario, and trust the live cdb over the decomp synthesis.