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>
18 KiB
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:
- Walk↔Run leg cycle on observed remotes does not visibly switch. Body advances at correct new speed but legs continue playing previous cycle.
- Residual small "blip" corrections during steady-state motion (run, walk, strafe). User describes this as a periodic micro-jitter — small but visible.
- (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
InterpretedStatecorrectly. OnLiveMotionUpdatedcallsSetCyclewith correct args.SetCycleupdates the sequencer'sCurrentMotioncorrectly.- 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 nodeBuildBlendedFrame()reading from a stale_currNode_currNodeadvancing 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
MeshRefsfor 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(inGameWindow.cs:6664) → resolves cycle →ae.Sequencer.SetCycle(...)- Fast-path early-return when cmd+speed unchanged (line 6713-6714)
OnLiveMotionUpdatedskips 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.Commandsand routes each throughAnimationCommandRouter(357dcc0made this skip SubState class) - Then ALSO updates
remoteMot.Motion.InterpretedState.ForwardCommand/ForwardSpeedfor 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 usesMotionTableManager::add_to_queuewithouttruncate_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
-
Add a per-tick diag that prints
_currNode.Anim.Id+_framePositionfor the observed remote. This will conclusively answer whether_currNodeis on the new cycle, on a stale link, or somewhere else. Implement near the existing SEQSTATE diag inGameWindow.cs:6520. Ask user to do the precise test sequence (W only, then shift+W only, no turns/no mouse) and read the log. -
Add a diag that prints
seqFrames[0].OriginandseqFrames[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. -
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.
-
Try the retail-faithful additive
add_to_queuesemantics: modify SetCycle to skip ClearCyclicTail and just append new motion data. TheMotionTableManager::CheckForCompletedMotionscleanup we don't port might be needed — but a primitive version (drop nodes whoseIsLooping=truecount exceeds 1, keeping the newest) might suffice as a starting point. -
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/. -
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.csend-to-end, focusing on:
SetCycle(line 360-560) — what state it mutates and in what orderAdvance(line 690-784) — how it consumes the queue and what it returnsBuildBlendedFrame(line 1254-1313) — how the visible per-part transforms are computedClearCyclicTail(line 1117-1140) andAdvanceToNextAnimation(line 1150-1166) — node lifecycle in the queueThen read
src/AcDream.App/Rendering/GameWindow.cs:5851-6635— theTickAnimationsmethod including the dead-reckoning blocks, sequencer Advance call, and the seqFrames-to-MeshRefs application loop.Answer:
After
SetCycleis called forRunForward(withlinkData != nullandcycleData != null), what is the precise queue state, the value of_currNode, and the value of_framePositionimmediately after SetCycle returns? Trace step by step including ClearCyclicTail's effect on_currNode. Cite line numbers.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 doesBuildBlendedFramereturn at the end?Is there any code path between
SetCyclereturning and the nextAdvancecall that could RESET_currNodeback 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).Is there any difference in how
seqFramesis consumed for the local player vs a remote-observed entity in the loop at lines 6566-6635? Both useif (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
- Start local ACE server (user has this running on
127.0.0.1:9000). - Start a parallel retail acclient.exe and connect with a different character (NOT
+Acdream). - Build acdream:
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug - Launch acdream from the main repo dir with both env vars:
$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 - From the retail client, drive the test character: stand 2s, press W (run) 4s, release, press shift+W (walk) 4s, release.
- Observe the test character in the acdream window. Bug: leg cycle does NOT visibly switch between idle / run / walk poses.
- Read diags from
launch.log(UTF-16 — useGet-Content -Encoding Unicode).
Notes on what NOT to do
- Do not pass
skipTransitionLink: trueunconditionally to SetCycle — tried in commitc06b6c5(link skip), broke landing-from-jump, sit-down, and every other transition that needs its dat link to play. Reverted ina2ae2ae. - 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 cdbwhile it's attached to retail — takes retail down with it (CLAUDE.md). Use.detachinside bp actions for graceful exit.