acdream/docs/research/2026-07-04-170-creature-run-handoff.md
Erik e744192361 docs(#170): session handoff + pickup prompt for the "sustain the run" residual
Detailed handoff (docs/research/2026-07-04-170-creature-run-handoff.md) + paste
prompt (2026-07-04-170-pickup-prompt.md) for continuing #170 in a fresh session.
Records the full root-cause chain (retail cdb + acdream probes), the landed partial
fix (427332ac: per-frame apply_current_movement flood deleted → pending_motions
1.3M→~1, run installs, stuck-attack gone), the one residual (Ready-stop drain lag →
run not fully sustained), the exact next steps (cdb the retail Ready-stop drain), the
apparatus (env-gated probes + cdb scripts), the file:line map, and the DO-NOT-RETRY
list (superseded MovementManager-coexistence hypothesis; keep the velocity fix; don't
patch the retail-faithful motions_pending guard). ISSUES #170 updated to PARTIAL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:31:40 +02:00

11 KiB
Raw Blame History

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.StopMotionCMotionTable.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 + UseTimeCheckForCompletedMotions 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 ~12
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_velocityset_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.