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

188 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.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** | ~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_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.