docs(research): investigation prompt for remote-anim-cycle bug
Hand-off briefing for the remaining "observed retail char's leg cycle doesn't visibly switch in acdream" bug. Captures everything we learned today including: - All 8 commits shipped today (turn-sign, observed-velocity revert, retail-faithful tick, Commands-list SubState skip, currNode reset) - Confirmed wins: body translation, run-in-circles, jump landing position + animation, turn-left direction - Confirmed remaining bug: walk/run/idle leg cycle on observed remotes + residual steady-state blippiness - Diagnostic infrastructure (FWD_WIRE, CMD_LIST, HASCYCLE, SETCYCLE, SEQSTATE, TURN_WIRE, OMEGA_DIAG, VEL_DIAG) and how to reproduce - cdb live trace findings (retail uses additive add_to_queue with no truncate; we have ClearCyclicTail + rebuild) - Six concrete next-step hypotheses - A self-contained prompt for the next research agent - Notes on rejected approaches (link-skip, full-reset, scaling hack) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
357dcc0547
commit
7f1bd1809a
1 changed files with 253 additions and 0 deletions
|
|
@ -0,0 +1,253 @@
|
||||||
|
# Remote-entity animation-cycle bug — investigation prompt
|
||||||
|
|
||||||
|
**Hand-off date:** 2026-05-03
|
||||||
|
**Status:** open. Multiple shipped fixes today reduced the remote-entity motion problem to a single residual symptom — the **leg-cycle on observed remotes does not visibly switch between Walk / Run / Ready** even though every signal says it should. Plus minor blippiness in steady motion.
|
||||||
|
|
||||||
|
This document is a self-contained briefing for an agent (or fresh session) picking this up.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What problem are we trying to solve?
|
||||||
|
|
||||||
|
When acdream observes another player driven by a parallel **retail** acclient.exe (connected to the same local ACE server), the remote character's **leg animation cycle** does not visibly change when that retail player switches between Run / Walk / Idle. The remote's **body** moves at the right speed (translation works), but the **legs keep playing whatever cycle was active before**.
|
||||||
|
|
||||||
|
User test: drive `+Acdream` (or any retail char) through `Press W (run) → release → Press shift+W (walk) → release` while observing in acdream's window. The body moves correctly but the leg cycle stays in idle pose / walk pose / whatever it was.
|
||||||
|
|
||||||
|
User-confirmed working perspectives:
|
||||||
|
- Local +Acdream's transitions in acdream **work** ✓
|
||||||
|
- +Acdream observed FROM a parallel retail client **work** ✓ (proves our outbound is fine)
|
||||||
|
|
||||||
|
So the bug is **specifically** in how acdream renders the visual cycle for an observed remote-driven character.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What we shipped today (commits in chronological order)
|
||||||
|
|
||||||
|
```
|
||||||
|
0997f96 fix(motion): landing fallback + TurnLeft omega sign + vel diagnostic (L.3.2)
|
||||||
|
9960ce3 fix(motion): preserve signed TurnSpeed for remote turn animations
|
||||||
|
842dfcd fix(motion): retail-faithful per-frame remote tick (L.3.2 follow-up)
|
||||||
|
b1d8e12 research(motion): cdb live trace of retail walk-to-run transition
|
||||||
|
a45c21e fix(motion): retail-faithful remote tick — clear body.Velocity, drive via seqVel
|
||||||
|
c06b6c5 fix(motion): full queue reset on locomotion-cycle direct transitions [partly reverted]
|
||||||
|
a2ae2ae revert: AnimationSequencer locomotion-cycle full-reset and link-skip
|
||||||
|
357dcc0 fix(motion): SetCycle forces _currNode onto first newly-enqueued node;
|
||||||
|
skip SubState commands in UM Commands list iteration
|
||||||
|
```
|
||||||
|
|
||||||
|
**User-confirmed wins from the above:**
|
||||||
|
- Body translation no longer races (was 2× server pace; now matches)
|
||||||
|
- Run-in-circles smooth (rectangle-effect gone — body rotates properly between UPs)
|
||||||
|
- Jump landing position correct (no mid-air force-land)
|
||||||
|
- Jump landing animation works (Falling → Ready visible)
|
||||||
|
- Turn-left visibly turns left (was animating right with snap-back)
|
||||||
|
- Signed TurnSpeed preserved (ACE encodes TurnLeft as `TurnCommand=TurnRight, Speed=negative`)
|
||||||
|
|
||||||
|
**User-confirmed remaining bugs:**
|
||||||
|
1. **Walk↔Run leg cycle on observed remotes does not visibly switch.** Body advances at correct new speed but legs continue playing previous cycle.
|
||||||
|
2. **Residual small "blip" corrections during steady-state motion** (run, walk, strafe). User describes this as a periodic micro-jitter — small but visible.
|
||||||
|
3. **(Possible)** ~20% steady-state walk overshoot (`maxSeqSpeed=3.120, serverSpeed≈2.6`) per VEL_DIAG measurements — not yet root-caused. May or may not be related to (2).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What we proved about bug 1 (the cycle-doesn't-switch)
|
||||||
|
|
||||||
|
Per the diagnostic infrastructure built today:
|
||||||
|
|
||||||
|
| Signal | Result |
|
||||||
|
|---|---|
|
||||||
|
| `[FWD_WIRE]` — wire-arrival ForwardCommand transitions | ✅ ACE delivers `WalkForward → RunForward` (and direct walk↔run) correctly |
|
||||||
|
| `[CMD_LIST]` — Commands list at receive time | Empty for walk/run UMs; contains Ready/Action class for some others |
|
||||||
|
| `[HASCYCLE]` — does the dat have the requested cycle | ✅ True for both `0x44000007` (Run) and `0x45000005` (Walk) on style `0x8000003D` (NonCombat Humanoid) |
|
||||||
|
| `[SETCYCLE]` — animCycle picker calls into AnimationSequencer.SetCycle | ✅ Fires with correct (style, motion, speed) |
|
||||||
|
| `[SEQSTATE]` — per-tick `ae.Sequencer.CurrentMotion` for the observed remote | ✅ Holds the new motion correctly (e.g. shows `0x44000007 speed=2.939` after Run press, then `0x41000003 speed=1.000` after release) |
|
||||||
|
|
||||||
|
So:
|
||||||
|
- ACE wire data is correct.
|
||||||
|
- Our parser updates `InterpretedState` correctly.
|
||||||
|
- `OnLiveMotionUpdated` calls `SetCycle` with correct args.
|
||||||
|
- `SetCycle` updates the sequencer's `CurrentMotion` correctly.
|
||||||
|
- The cycle data the sequencer would play exists in the dat.
|
||||||
|
|
||||||
|
**But the visible leg cycle does NOT update.** Therefore the bug is **downstream of `ae.Sequencer.CurrentMotion`** — somewhere between the sequencer's internal state and the rendered MeshRefs:
|
||||||
|
- `AnimationSequencer.Advance(dt)` returning frames from the wrong node
|
||||||
|
- `BuildBlendedFrame()` reading from a stale `_currNode`
|
||||||
|
- `_currNode` advancing through stale link/head frames before reaching the new cycle
|
||||||
|
- Or how the per-part transforms returned by Advance get applied to the entity's `MeshRefs` for remote entities
|
||||||
|
|
||||||
|
We attempted a fix in `357dcc0` that forces `_currNode` onto the first newly-enqueued node in SetCycle — user reports **no visible change** after this fix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's different between local (works) and remote (doesn't)
|
||||||
|
|
||||||
|
Both call **the same `AnimationSequencer.SetCycle` method** in `src/AcDream.Core/Physics/AnimationSequencer.cs:360`. So the sequencer code itself is shared.
|
||||||
|
|
||||||
|
Local +Acdream path:
|
||||||
|
- `PlayerMovementController` → `UpdatePlayerAnimation` (in `GameWindow.cs:6664`) → resolves cycle → `ae.Sequencer.SetCycle(...)`
|
||||||
|
- Fast-path early-return when cmd+speed unchanged (line 6713-6714)
|
||||||
|
- `OnLiveMotionUpdated` skips wire-echo SetCycle for the local player guid (line 2707)
|
||||||
|
|
||||||
|
Remote (observed retail char) path:
|
||||||
|
- Wire arrives → `OnLiveMotionUpdated` (`GameWindow.cs:3203`)
|
||||||
|
- "animCycle picker" at line 2842-2867 chooses the cycle based on Forward / Sidestep / Turn priority
|
||||||
|
- HasCycle fallback chain at line 2939
|
||||||
|
- `ae.Sequencer.SetCycle(fullStyle, cycleToPlay, animSpeed)` at line 2988
|
||||||
|
- Then iterates `update.MotionState.Commands` and routes each through `AnimationCommandRouter` (357dcc0 made this skip SubState class)
|
||||||
|
- Then ALSO updates `remoteMot.Motion.InterpretedState.ForwardCommand/ForwardSpeed` for body.Velocity computation
|
||||||
|
- Then ALSO calls `remoteMot.Motion.DoInterpretedMotion(...)` for sidestep/turn axes
|
||||||
|
|
||||||
|
**Hypotheses to investigate:**
|
||||||
|
|
||||||
|
A) After `SetCycle` fires, some other call in `OnLiveMotionUpdated` re-cycles the sequencer back. We've eliminated the `Commands` list (357dcc0 skip-SubState). Other candidates: `PlayAction` calls inside `RouteWireCommand`, the spawn-time SetCycle at line 2313, or something in `ApplyServerControlledVelocityCycle` (line 3238).
|
||||||
|
|
||||||
|
B) `_currNode` actually IS in the right place after SetCycle but `Advance(dt)` doesn't read from it correctly. Maybe a thread-safety issue (SetCycle on net thread, Advance on render thread, partial state visible).
|
||||||
|
|
||||||
|
C) `Advance` returns the right frames but `seqFrames` are not applied to the entity's `MeshRefs` for the remote entity specifically. Look at `GameWindow.cs:6510-6589` — the per-part transform application loop. There's no obvious local-vs-remote branch but worth tracing.
|
||||||
|
|
||||||
|
D) The MeshRefs themselves get rebuilt each frame and the rebuild reads from a different source for remotes. The `newMeshRefs` list is built per-frame at line 6567.
|
||||||
|
|
||||||
|
E) Local player's `ae.Sequencer.SetCycle` is called at a higher rate than remote's (per-input vs per-UM). Maybe the queue stays cleaner with frequent calls, and the bug is exposed only when SetCycle is sparse.
|
||||||
|
|
||||||
|
F) **Most likely** based on what we've seen: `Advance` plays through stale link frames before reaching the cycle. Our 357dcc0 fix forces `_currNode` onto the first newly-enqueued node — but for `Ready→Run`, the newly-enqueued sequence is `[Ready→Run link, Run cycle]`. `_currNode` lands on the **link**, the link plays for ~0.5–1 second, then the run cycle starts. User perceives the link's "transition pose" as "still walking / still idle."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diagnostic infrastructure available
|
||||||
|
|
||||||
|
All env-var gated on `ACDREAM_REMOTE_VEL_DIAG=1`:
|
||||||
|
|
||||||
|
| Diag | Where | What it shows |
|
||||||
|
|---|---|---|
|
||||||
|
| `[FWD_WIRE]` | `GameWindow.cs:2793-2800` | Each ForwardCommand transition received per remote |
|
||||||
|
| `[CMD_LIST]` | `GameWindow.cs:3119-3133` | Commands list contents at UM receive time |
|
||||||
|
| `[HASCYCLE]` | `GameWindow.cs:2939-2947` | HasCycle result for the requested cycle |
|
||||||
|
| `[SETCYCLE]` | `GameWindow.cs:2972-2986` | Each animCycle picker → SetCycle call |
|
||||||
|
| `[SEQSTATE]` | `GameWindow.cs:6520-6532` | Per-tick `ae.Sequencer.CurrentMotion` (1Hz throttled) |
|
||||||
|
| `[TURN_WIRE]` | `GameWindow.cs:3050-3057` | TurnCommand wire arrivals with signed speed |
|
||||||
|
| `[OMEGA_DIAG]` | `GameWindow.cs:5901-5912` | Per-tick omega being applied to body |
|
||||||
|
| `[VEL_DIAG]` | `GameWindow.cs:3327-3343` | Server-broadcast speed vs maxSeqSpeed per UP |
|
||||||
|
|
||||||
|
Also gated on `ACDREAM_INTERP_MANAGER=1` is the entire retail-faithful per-tick remote motion path. Set both env vars when reproducing.
|
||||||
|
|
||||||
|
The repo has `tools/cdb-scripts/` set up for live tracing of retail acclient.exe via cdb.exe. Two trace scripts already proven working:
|
||||||
|
- `walk_run_motion_trace.cdb` + `walk_run_motion_trace.log` — captured the exact retail walk→run sequence and proved retail uses `MotionTableManager::add_to_queue` without `truncate_animation_list`.
|
||||||
|
|
||||||
|
To launch retail tracing: have user start retail and connect, then in PowerShell:
|
||||||
|
```
|
||||||
|
& "C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe" `
|
||||||
|
-pn acclient.exe -cf "tools\cdb-scripts\walk_run_motion_trace.cdb" *>&1 |
|
||||||
|
Tee-Object -FilePath "tools\cdb-scripts\walk_run_motion_trace.log.console"
|
||||||
|
```
|
||||||
|
Auto-detaches at 200 hits via `.detach` (do NOT use `qd` per CLAUDE.md gotcha — silently ignored). NEVER `Stop-Process` cdb — takes retail down with it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What retail actually does (from cdb live trace)
|
||||||
|
|
||||||
|
For a walk→run direct transition retail's call sequence is:
|
||||||
|
|
||||||
|
```
|
||||||
|
[79] CPhysicsObj::DoInterpretedMotion: motion=45000005 walk start (shift+W)
|
||||||
|
[82] CMotionTable::DoObjectMotion: motion=45000005
|
||||||
|
[83] MotionTableManager::add_to_queue: arg1=45000005 arg2=00000001 ← walk added looping
|
||||||
|
|
||||||
|
[89] CPhysicsObj::DoInterpretedMotion: motion=44000007 run start (release shift)
|
||||||
|
[92] CMotionTable::DoObjectMotion: motion=44000007
|
||||||
|
[93] MotionTableManager::add_to_queue: arg1=44000007 arg2=00000001 ← run added looping
|
||||||
|
|
||||||
|
[104] CMotionTable::StopObjectMotion: motion=44000007 run end (release W)
|
||||||
|
```
|
||||||
|
|
||||||
|
`MotionTableManager::truncate_animation_list` was on bp the entire trace and **never fired**. Retail just appends new motions to the queue and lets `MotionTableManager::CheckForCompletedMotions` (`0x0051BE00`) and `MotionTableManager::remove_redundant_links` (`0x0051BF20`) handle the natural progression — neither of which we have ported.
|
||||||
|
|
||||||
|
This suggests our `AnimationSequencer.SetCycle` rebuild semantics (ClearCyclicTail + enqueue link + enqueue cycle) is fundamentally different from retail's "append-only" `MotionTableManager`. May not matter for visual output as long as our queue manipulations land in the same end state, but it's a structural mismatch worth exploring if the tactical fixes don't pan out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File locations
|
||||||
|
|
||||||
|
- **`src/AcDream.Core/Physics/AnimationSequencer.cs`** — SetCycle (line 360), Advance (690), BuildBlendedFrame (1254), ClearCyclicTail (1117), AdvanceToNextAnimation (1150), EnqueueMotionData (1101), LoadAnimNode (1037)
|
||||||
|
- **`src/AcDream.App/Rendering/GameWindow.cs`** — OnLiveMotionUpdated (3203), TickAnimations (5851), animCycle picker block (2842-2988), the seqFrames-to-MeshRefs application loop (6510-6635), UpdatePlayerAnimation (6664)
|
||||||
|
- **`src/AcDream.Core/Physics/AnimationCommandRouter.cs`** — RouteWireCommand (53), Classify (29)
|
||||||
|
- **`src/AcDream.Core/Physics/MotionInterpreter.cs`** — get_state_velocity (587), GetMaxSpeed (968), apply_current_movement (653), HitGround (924)
|
||||||
|
- **`src/AcDream.Core/Physics/PositionManager.cs`** — ComputeOffset (37) (the per-tick combiner)
|
||||||
|
- **`src/AcDream.Core/Physics/InterpolationManager.cs`** — Enqueue, AdjustOffset (224), stall detection
|
||||||
|
- **Reference decomp:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` (1.4M-line pseudo-C with full PDB names)
|
||||||
|
- **Symbols index:** `docs/research/named-retail/symbols.json` (greppable name → address)
|
||||||
|
- **Verbatim retail headers:** `docs/research/named-retail/acclient.h` (struct field offsets)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concrete next steps for the bug
|
||||||
|
|
||||||
|
1. **Add a per-tick diag that prints `_currNode.Anim.Id` + `_framePosition` for the observed remote.** This will conclusively answer whether `_currNode` is on the new cycle, on a stale link, or somewhere else. Implement near the existing SEQSTATE diag in `GameWindow.cs:6520`. Ask user to do the precise test sequence (W only, then shift+W only, no turns/no mouse) and read the log.
|
||||||
|
|
||||||
|
2. **Add a diag that prints `seqFrames[0].Origin` and `seqFrames[0].Orientation`** (the result of Advance) before applying to MeshRefs. If the values change meaningfully between cycles → bug is in MeshRefs application. If they're stuck → bug is in Advance/BuildBlendedFrame.
|
||||||
|
|
||||||
|
3. **Compare the call ORDER of SetCycle for local vs remote.** Maybe local's UpdatePlayerAnimation calls SetCycle then immediately also re-resolves cycle data and passes it through. Or local has frame-resolution state we lack for remotes.
|
||||||
|
|
||||||
|
4. **Try the retail-faithful additive `add_to_queue` semantics:** modify SetCycle to skip ClearCyclicTail and just append new motion data. The `MotionTableManager::CheckForCompletedMotions` cleanup we don't port might be needed — but a primitive version (drop nodes whose `IsLooping=true` count exceeds 1, keeping the newest) might suffice as a starting point.
|
||||||
|
|
||||||
|
5. **Trace retail's CSequence::update / update_internal calls live** with cdb to see what frames ARE returned per tick for a remote running and transitioning. We have the cdb toolchain set up; pattern existing scripts in `tools/cdb-scripts/`.
|
||||||
|
|
||||||
|
6. **If all else fails, dispatch a research agent** with the prompt below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## For the next research agent — exact assignment
|
||||||
|
|
||||||
|
> Read this entire document.
|
||||||
|
>
|
||||||
|
> Read `src/AcDream.Core/Physics/AnimationSequencer.cs` end-to-end, focusing on:
|
||||||
|
> - `SetCycle` (line 360-560) — what state it mutates and in what order
|
||||||
|
> - `Advance` (line 690-784) — how it consumes the queue and what it returns
|
||||||
|
> - `BuildBlendedFrame` (line 1254-1313) — how the visible per-part transforms are computed
|
||||||
|
> - `ClearCyclicTail` (line 1117-1140) and `AdvanceToNextAnimation` (line 1150-1166) — node lifecycle in the queue
|
||||||
|
>
|
||||||
|
> Then read `src/AcDream.App/Rendering/GameWindow.cs:5851-6635` — the `TickAnimations` method including the dead-reckoning blocks, sequencer Advance call, and the seqFrames-to-MeshRefs application loop.
|
||||||
|
>
|
||||||
|
> Answer:
|
||||||
|
>
|
||||||
|
> 1. After `SetCycle` is called for `RunForward` (with `linkData != null` and `cycleData != null`), what is the precise queue state, the value of `_currNode`, and the value of `_framePosition` immediately after SetCycle returns? Trace step by step including ClearCyclicTail's effect on `_currNode`. Cite line numbers.
|
||||||
|
>
|
||||||
|
> 2. On the next render tick when `Advance(dt=0.0167)` is called, what does it do? Specifically, does it advance through the link frames first, or skip them, or play them and stop at the cycle? What pose does `BuildBlendedFrame` return at the end?
|
||||||
|
>
|
||||||
|
> 3. Is there any code path between `SetCycle` returning and the next `Advance` call that could RESET `_currNode` back to a stale node? List every SetCycle call site (there are ~12 in GameWindow.cs) and identify any that fire on the per-tick path (not just on UM receive).
|
||||||
|
>
|
||||||
|
> 4. Is there any difference in how `seqFrames` is consumed for the local player vs a remote-observed entity in the loop at lines 6566-6635? Both use `if (seqFrames is not null) { origin = seqFrames[i].Origin; ... }`. Find any conditional branch that bypasses seqFrames for remotes.
|
||||||
|
>
|
||||||
|
> Output: a concise (<800 word) report with line citations and a clear hypothesis for the root cause of the visible-cycle-doesn't-switch bug. Do NOT modify any code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick reproduction recipe
|
||||||
|
|
||||||
|
1. Start local ACE server (user has this running on `127.0.0.1:9000`).
|
||||||
|
2. Start a parallel **retail** acclient.exe and connect with a different character (NOT `+Acdream`).
|
||||||
|
3. Build acdream: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`
|
||||||
|
4. Launch acdream from the main repo dir with both env vars:
|
||||||
|
```powershell
|
||||||
|
$env:ACDREAM_INTERP_MANAGER = "1"
|
||||||
|
$env:ACDREAM_REMOTE_VEL_DIAG = "1"
|
||||||
|
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||||
|
$env:ACDREAM_LIVE = "1"
|
||||||
|
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||||||
|
$env:ACDREAM_TEST_PORT = "9000"
|
||||||
|
$env:ACDREAM_TEST_USER = "testaccount"
|
||||||
|
$env:ACDREAM_TEST_PASS = "testpassword"
|
||||||
|
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
|
||||||
|
Tee-Object -FilePath launch.log
|
||||||
|
```
|
||||||
|
5. From the retail client, drive the test character: stand 2s, press W (run) 4s, release, press shift+W (walk) 4s, release.
|
||||||
|
6. Observe the test character in the acdream window. Bug: leg cycle does NOT visibly switch between idle / run / walk poses.
|
||||||
|
7. Read diags from `launch.log` (UTF-16 — use `Get-Content -Encoding Unicode`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes on what NOT to do
|
||||||
|
|
||||||
|
- **Do not pass `skipTransitionLink: true` unconditionally to SetCycle** — tried in commit `c06b6c5` (link skip), broke landing-from-jump, sit-down, and every other transition that needs its dat link to play. Reverted in `a2ae2ae`.
|
||||||
|
- **Do not full-reset the queue on every motion change** — same commit, also reverted. Side effect: removed end-animations everywhere.
|
||||||
|
- **Do not "scale body.Velocity by observed serverSpeed/predictedSpeed"** — tried during the day, user explicitly rejected as a hack. Always use predicted velocity from `get_state_velocity` (= `RunAnimSpeed × ForwardSpeed`).
|
||||||
|
- **Do not `Stop-Process cdb`** while it's attached to retail — takes retail down with it (CLAUDE.md). Use `.detach` inside bp actions for graceful exit.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue