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

172 lines
11 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
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.