acdream/docs/research/2026-07-04-170-creature-run-handoff.md
Erik 1051fc83c6 fix(#170): armed moveto always ticks UseTime — the SERVERVEL branch starved the chase
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>
2026-07-04 21:59:59 +02:00

12 KiB
Raw Blame History

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 skips MoveToManager.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 runs MovementManager::UseTime unconditionally (UpdateObjectInternal 0x005156b0 @0x00515998). Fix: armed movetos always take the MOVETO leg (GameWindow TickAnimations, moveToArmed gate; register TS-41 narrowed, TS-42 added for the one-frame drain-order divergence). Current status + next steps live in docs/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-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.