The "sustain the run" residual. The handoff's "Ready stop-node backlog drains a beat slower than retail" framing was DISPROVEN: a new full-stack offline harness (RemoteChaseEndToEndHarnessTests — real MoveToManager + MotionInterpreter + AnimationSequencer + MotionTableDispatchSink + the manual omega integration, wired field-for-field like EnsureRemoteMotionBindings and ticked in TickAnimations' exact phase order) proves the Core turn/run/drain pipeline healthy: the chase turn completes in <1 s both directions, BeginMoveForward installs per arm, the run sustains across re-arms and attack swings, and pending_motions fully empties (retail cdb invariant add_to_queue == MotionDone). The real mechanism (launch-drainq.log, corrected per-guid attribution — the previous session's timeline mis-attributed [mvto] lines that fire in the network phase): funnel per chasing scamp was 16 mt-6 arms -> 11 dispatched turns -> ONE BeginMoveForward. Any NPC receiving UpdatePositions gets HasServerVelocity=true (synthesized from position deltas even when the wire carries no velocity), and the grounded per-tick branch routed those to the SERVERVEL leg, which SKIPS MoveToManager.UseTime — [npc-tick] literally logged "branch=SERVERVEL (skips UseTime) mtState=MoveToObject". The armed moveto was starved for exactly the duration of the server-side chase: legs stayed in Ready while the body glided on synthesized velocity (the #170 slide); the manager only woke in UP-silent gaps (creature stopped server-side) and its stale-heading turn was interrupted by the next UM before reaching BeginMoveForward. Retail runs MovementManager::UseTime UNCONDITIONALLY every tick (CPhysicsObj::UpdateObjectInternal 0x005156b0, call @0x00515998) and has no wire-velocity leg-driver anywhere; between UPs a moveto-driven body translates from the motion state (get_state_velocity) with UP hard-snaps correcting drift. Fix: an armed moveto (MovementTypeState != Invalid) always takes the MOVETO leg; SERVERVEL remains only as the legacy fallback for entities without a moveto (scripted paths / missiles). Register: TS-41 (the narrowed SERVERVEL stopgap), TS-42 (drain-order divergence also pinned this session: acdream drains AnimDone->MotionDone AFTER HandleTargetting/MoveTo.UseTime; retail's process_hooks @0x00512d3d runs BEFORE TargetManager/MovementManager in UpdateObjectInternal — one frame of extra latency, R6 scope). New conformance: RemoteChaseEndToEndHarnessTests (3 scenarios + theory) + RemoteChaseDrainBisectTests (the drain-chain pin; its first run also demonstrated the TS-40 InWorld=false link-strip wedge shape — harness bodies must replicate the live RemoteMotion construction). ISSUES #170 updated (awaiting user visual gate; probes stay until then); handoff doc superseded-note added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
Handoff — #170 creature chase renders as slide (PARTIAL FIX landed; residual = "sustain the run") — 2026-07-04
⚠ SUPERSEDED (2026-07-04, second session): the residual is FIXED — pending visual gate. The "Ready stop-node backlog drains a beat slower" framing below was DISPROVEN by a full-stack offline harness (
tests/AcDream.Core.Tests/Physics/Motion/RemoteChaseEndToEndHarnessTests.cs) plus corrected per-guid attribution of the launch-drainq.log evidence. The Core drain/turn/run machinery is healthy; both handoff hypotheses ((a) tick counts, (b) drain trigger rate) are moot. The real mechanism: the per-tick branch arbitration sent any UP-receiving NPC down the SERVERVEL leg, which skipsMoveToManager.UseTime— the armed moveto was starved for the whole server-side chase (funnel: 16 arms → 11 dispatched turns → 1 run install), legs stayed Ready while the body glided on UP-synthesized velocity. Retail runsMovementManager::UseTimeunconditionally (UpdateObjectInternal0x005156b0 @0x00515998). Fix: armed movetos always take the MOVETO leg (GameWindowTickAnimations,moveToArmedgate; register TS-41 narrowed, TS-42 added for the one-frame drain-order divergence). Current status + next steps live indocs/ISSUES.md#170. This doc remains as the evidence record for the flood fix (427332ac) and the cdb apparatus.
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-6MoveToObject stream:HandleUpdateTarget → MoveToObject_Internal → (TurnToHeading node completes via UseTime) → BeginMoveForward → _DoMotion(RunForward)sets the motion-table substate = RunForward (the legs).RunForwardis NEVER on the wire — the server only sends mt-6 + the attack UMs. MoveToManager.BeginTurnToHeading(retail0x00529b90) bails whileCMotionInterp.motions_pendingis non-empty (retail-faithful guard).- acdream's
pending_motionsexploded to ~1.3M entries because the NPC per-tick calledrm.Motion.apply_current_movementevery frame, re-dispatching the whole interpreted state (stance + attack + stops) and appending apending_motionsnode each time.MotionsPending()stayed permanently true → the chase turn never started →BeginMoveForward/RunForward~never fired → slide. - FIX (landed
427332ac): delete the per-frameapply_current_movementin 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 aReadypending_motionsnode (line 3292) whenever the stop succeeds (sink.StopMotion→CMotionTable.StopSequenceMotionreturns 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 queueReady. TheReadynodes that DO accumulate come from stops that succeed: chiefly the per-UM tailStopInterpretedMotion(TurnRight)(MotionInterpreter.cs:3008) stopping the MoveTo's OWN steering turn, plus the MoveTo-cancelStopCompletely.- Those
Readynodes drain viaMotionDone(fired 1:1 byMotionTableManagerper completed_pendingAnimationsentry —AnimationDoneper AnimDone hookGameWindow.cs:10306+UseTime→CheckForCompletedMotions0-tick sweep at10309). They drain a beat slower than they add → backlog grows (lag 1→10 over a chase). Retail hitsadd_to_queue == MotionDoneexactly (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):
- Tick count. acdream's
Readystop entries haveoutTicks > 0(fromStopSequenceMotionCase A →GetObjectSequence(styleDefault, stopCall:true, out outTicks)), so they wait forAnimationDoneinstead of draining immediately via the per-frame 0-tickCheckForCompletedMotions. Retail's may be 0-tick. → cdb: breakCMotionTable::StopObjectMotion/GetObjectSequenceon a live chasing monster, read the returned tick count for a turn-stop. - Drain trigger rate. Retail may fire
CheckForCompletedMotions/MotionDonemore aggressively than acdream's once-per-frameManager.UseTime. → cdb: countCMotionInterp::MotionDonevsadd_to_queue(already have the script:scratchpad/cdb-drain.cdb— retail = add==done) and addCPhysicsObj::CheckForCompletedMotions/MotionTableManager::UseTimecounts.
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.css_mvtoDiag:[mvto] BeginMoveForward cmd=…,HandleUpdateTarget … match=… init=… mtState=…,CancelMoveTo (real) wasType=…,UseTime enter/dispatch node=…,BeginTurnToHeading motionsPending=… curCmd=….MotionInterpreter.css_drainDiag:[drain] add=… done=… lag=… depth=…(aggregate add_to_queue vs MotionDone) +[drainq] depth=… q=[…](queue CONTENTS when depth≥2 — this is what namedReadyas the lingering node).GameWindow.cs:[npc-tick] guid=… branch=SERVERVEL|MOVETO(which per-tick branch),- the earlier
UM ↳ actions …inbound-action dump (capture aid).
- the earlier
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.exeMATCHESrefs/acclient.pdb(py tools/pdb-extract/check_exe_pdb.py "…/acclient.exe"). cdb atC:\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-qdafter Nmove_to_interpreted_statehits (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
d2ccc80evelocity fix (get_state_velocity→set_local_velocityeach grounded tick) is CORRECT but position-only — it does NOT fix the legs; keep it. CombatAnimationPlanner(#159, fixed2de5a011) 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'sif (motions_pending) returnguard is retail-faithful — do NOT patch it. Fix the DRAIN so the queue empties, don't remove the guard.- The per-frame
apply_current_movementdeletion 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 keptget_state_velocityvelocity refresh); ~10306/10309 (the pending-motions DRAIN:Manager.AnimationDoneper AnimDone hook +Manager.UseTime); ~4251EnsureRemoteMotionBindings(MoveToManager + TargetManager voyeur wiring,getSelfId=serverGuidcorrect).MoveToManager.cs:BeginMoveForward:741,BeginTurnToHeading:821(if (_interp.MotionsPending()) returnat ~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(addsReadyon 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.