# Locomotion-cycle transitions on observed remotes — investigation prompt **Hand-off date:** 2026-05-06 **Status:** open. ISSUES.md #39 captured the Run↔Walk-forward variant; this prompt expands to the full locomotion transition matrix and lays out a TTD-driven investigation against retail to ground the fix. This document is a self-contained briefing for an agent (or fresh session) picking this up. The TTD toolchain landed in commit `e3e5bf5` (#44) and is the primary investigative tool here. --- ## What problem are we trying to solve? When acdream observes a remote-driven player character (typically a parallel retail acclient.exe connected to the same local ACE), the visible leg-cycle animation does not always switch when the actor changes locomotion mode. The body translates at the right speed (server-driven velocity is fine), but the legs keep playing whichever cycle was active before. The full set of transitions where the bug may surface: | # | Transition | Wire change | Likely cause | |---|---|---|---| | 1 | Run forward ↔ Walk forward (Shift toggle while W held) | HoldKey + ForwardSpeed only | ACE doesn't broadcast UM (no ForwardCommand change); cycle refinement must come from UP-derived velocity | | 2 | Run backward ↔ Walk backward (Shift toggle while S held) | HoldKey + ForwardSpeed only | Same as #1, backward axis | | 3 | Forward ↔ Backward (W→S or S→W direct flip) | ForwardCommand changes (e.g. `WalkForward` → `WalkBackward`) | ACE broadcasts a UM; cycle should update from UM directly | | 4 | Fast strafe-left ↔ Slow strafe-left (Shift toggle while A held) | HoldKey + SideSpeed only | Same as #1 / #2 — speed-bucket only | | 5 | Fast strafe-right ↔ Slow strafe-right (Shift toggle while D held) | HoldKey + SideSpeed only | Same | | 6 | Strafe-Left ↔ Strafe-Right (A↔D direct flip) | SideCommand changes | ACE broadcasts UM | | 7 | Forward ↔ Strafe (W↔A, W↔D, etc.) | ForwardCommand or SideCommand changes | UM broadcast | #39 in ISSUES.md focused on #1. This investigation widens scope to the whole matrix because the underlying wire pattern is the same shape: half the transitions broadcast a UM, the other half rely on UP-derived velocity. A correct fix should handle both classes uniformly. --- ## What we already know (don't re-discover) From the prior 2026-05-03 investigation (`docs/research/2026-05-03-remote-anim-cycle/`) and #39 in `docs/ISSUES.md`: - **ACE only broadcasts a fresh UpdateMotion when the wire's `ForwardCommand` byte changes** — i.e. on direction-key state changes. Toggling Shift while W held changes `ForwardSpeed` and `HoldKey` but NOT `ForwardCommand`, so ACE does NOT broadcast a UM for the demote/promote. - **Speed changes still propagate via UpdatePosition (UP).** Position-delta velocity changes between Run-pace and Walk-pace, confirmed via `[VEL_DIAG]` serverSpeed varying ~2.5 m/s (walk) ↔ ~9 m/s (run). - **Retail's inbound code uses UP-derived velocity to refine the visible cycle when no UM tells it.** Acdream has the equivalent function — `ApplyServerControlledVelocityCycle` in `GameWindow.cs:3274` — but it's gated `if (IsPlayerGuid(serverGuid)) return;` for player remotes, exactly the case where the gap matters. - **The fix sketch is small (~10 lines):** un-gate `ApplyServerControlledVelocityCycle` for player remotes when `currentMotion` is a locomotion cycle. Add a `LastUMUpdateTime` grace window so fresh UMs win over UP-velocity refinement. - **Outbound from acdream is fine** — the matrix in #39 confirms retail observers see acdream's transitions correctly. The bug is purely in acdream's RECEIVE path. What's NOT yet confirmed (and is the primary goal of this investigation): - **Which exact retail function** does the cycle refinement when no UM arrives? Hypothesis: `CPhysicsObj::unpack_movement` → `MovementManager::unpack_movement` → some velocity-aware cycle picker. We need to identify the function name and what state it reads. - **What grace window** does retail use between UM and UP-derived refinement? Our hypothesis is ~500 ms but it may be different. - **Does retail use SideSpeed or SideCommand changes** the same way for strafe transitions? --- ## TTD-driven workflow The toolchain shipped in #44 (commit `e3e5bf5`). Bootstrap is one-time per machine — see CLAUDE.md "TTD recordings (offline replay)" or `tools/ttd-record.ps1` header. ### Recording scenario You need **two retail acclient.exe instances** running on the same local ACE — one as the recorded process, one as the actor. Acdream can be involved as a third client for cross-checking but is not strictly required for the recording itself. Setup: 1. Launch retail #1 on `testaccount` / `+Acdream` (or any character). 2. Launch retail #2 on a second account / character. It must be on the same landblock so retail #1 can see it. 3. Position retail #2 in retail #1's view, ~10 m away. 4. In an **elevated** PowerShell, attach TTD to retail #1 (the observer): ```powershell tools\ttd-record.ps1 -RingMaxMB 512 ``` 5. Wait for `Recording is now active`. Retail #1 will be ~10× slower — that's expected. Scenario (drive on retail #2, ~60 seconds total): ``` Phase 1 — Forward speed: Hold W (2 s) → +Shift → -Shift → +Shift → -Shift → release W (idle 1 s) Phase 2 — Backward speed: Hold S (2 s) → +Shift → -Shift → +Shift → -Shift → release S (idle 1 s) Phase 3 — Forward↔Back flip: Hold W (2 s) → release W → Hold S (2 s) → release S Phase 4 — Strafe-Left speed: Hold A (2 s) → +Shift → -Shift → +Shift → -Shift → release A Phase 5 — Strafe-Right speed: Hold D (2 s) → +Shift → -Shift → +Shift → -Shift → release D Phase 6 — Strafe L↔R flip: Hold A (2 s) → release A → Hold D (2 s) → release D Phase 7 — Forward↔Strafe: Hold W (2 s) → release W → Hold D (2 s) → release D ``` After phase 7, Ctrl+C the TTD recording. Trace lands at `~/.ttd/traces/acclientNN.run`. ### Query strategy The recording captured ~7 phases in ~60 sec. Each phase produced specific UM and/or UP traffic. Goal: for each phase identify what retail's receive code did to update the visible cycle. Start with a `.cdb` script under `tools/ttd-queries/locomotion.cdb`: ``` .echo === Top motion-receive entry points === dx -r2 @$cursession.TTD.Calls("acclient!CPhysicsObj::unpack_movement").Take(20) dx -r2 @$cursession.TTD.Calls("acclient!MovementManager::unpack_movement").Take(20) .echo === Cycle-update functions called === dx -r2 @$cursession.TTD.Calls("acclient!*set_animation*").GroupBy(c => c.Function).Select(g => new { Name = g.First().Function, Count = g.Count() }).OrderByDescending(x => x.Count) dx -r2 @$cursession.TTD.Calls("acclient!*SetCycle*").GroupBy(c => c.Function).Select(g => new { Name = g.First().Function, Count = g.Count() }).OrderByDescending(x => x.Count) dx -r2 @$cursession.TTD.Calls("acclient!*play_*").GroupBy(c => c.Function).Select(g => new { Name = g.First().Function, Count = g.Count() }).OrderByDescending(x => x.Count) .echo === Velocity / position-update handlers === dx -r2 @$cursession.TTD.Calls("acclient!*UpdatePosition*").GroupBy(c => c.Function).Select(g => new { Name = g.First().Function, Count = g.Count() }).OrderByDescending(x => x.Count) dx -r2 @$cursession.TTD.Calls("acclient!CPhysicsObj::set_velocity").Take(15) .echo === DONE === q ``` Run it: ```powershell tools\ttd-query.ps1 -Script tools\ttd-queries\locomotion.cdb ``` **Then iterate.** Use the function-name hits to guide the next query — inspect call args (`Take(N)` instead of `Count()`), navigate to specific TTD positions and dump struct fields with `dt acclient!ClassName `. The named-retail decomp at `docs/research/named-retail/acclient_2013_pseudo_c.txt` has the source for every retail function. Grep it by class::method to read what the function does, then use TTD to confirm what it does at runtime. ### Specifically what to look for For each phase in the recording, answer: 1. **Did a UM arrive?** Look for calls to the receive entry points around that phase's time window. UM dispatch should fire on direction-key changes (phases 3, 6, 7) but NOT on Shift-only toggles (phases 1, 2, 4, 5). 2. **What function refined the cycle when no UM arrived?** Hypothesis is something in the velocity-aware path. Find the symbol, read the decomp, document the conditions. 3. **What threshold/grace logic is in place?** Retail must have some way to prevent UP-velocity refinement from fighting a fresh UM. Usually a timestamp comparison. --- ## The fix The fix lives in `src/AcDream.App/Rendering/GameWindow.cs:3274` (`ApplyServerControlledVelocityCycle`). Current code returns early for player remotes. The fix is to: 1. **Un-gate the early return** when the current motion is a locomotion cycle. Locomotion cycles to detect: `0x44000007` (Run forward), `0x45000005` (Walk forward), backward equivalents, sidestep variants. Keep the gate when the motion is non-locomotion (e.g. emotes, attacks). 2. **Add a `LastUMUpdateTime` per remote.** Touch it in the UM handler path. In `ApplyServerControlledVelocityCycle`, skip refinement if `(now - LastUMUpdateTime) < graceMs`. Start with `graceMs = 500` and tune against the TTD findings. 3. **Use UP-derived velocity to pick the speed bucket.** Existing logic in `ServerControlledLocomotion.PlanFromVelocity` already does this — verify thresholds match retail's bucketing (TTD will tell you the exact boundaries retail uses). For direction-flip transitions (phases 3, 6, 7), the UM handler path should already work — the visible cycle should update from the UM directly. Confirm in-client that those transitions are clean BEFORE shipping the velocity-fallback fix; if they're broken too, that's a separate UM-handler bug that needs its own investigation. --- ## Acceptance criteria In acdream observing a retail-driven character: - **Phases 1, 2, 4, 5 (Shift toggles):** visible leg cycle switches Run↔Walk / Fast↔Slow within ~200 ms of the wire change. - **Phases 3, 6, 7 (direction flips):** visible cycle updates within ~100 ms (UM is direct-path, faster than UP). - **No regression** on already-working cases: - acdream-on-acdream (matrix row in #39) - retail observers viewing acdream (works today) - Idle ↔ Run transitions - Idle ↔ Walk transitions - Combat-mode locomotion (if testable) - **No spurious cycle thrashing during turning while running** — ObservedOmega-driven body rotation must not trigger velocity-bucket changes mid-cycle. --- ## Files to read first In this order: 1. `docs/ISSUES.md` — issue #39 (Run↔Walk specific) and #44 (TTD toolchain) 2. `docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md` — prior investigation, what's been tried, what works 3. `CLAUDE.md` — "Retail debugger toolchain" section (live cdb + "TTD recordings (offline replay)" subsection) 4. `memory/project_retail_debugger.md` — durable lessons + TTD addendum 5. `memory/project_retail_motion_outbound.md` — what retail's outbound motion path looks like (cdb live trace from 2026-05-01) Then: 6. `src/AcDream.App/Rendering/GameWindow.cs` — `OnLiveMotionUpdated` (~line 3203) and `ApplyServerControlledVelocityCycle` (~line 3274) 7. `src/AcDream.Core/Physics/ServerControlledLocomotion.cs` — speed bucket thresholds in `PlanFromVelocity` 8. `docs/research/named-retail/acclient_2013_pseudo_c.txt` — grep by the symbol names you discover from the TTD trace 9. `references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageUpdateMotion.cs` — what ACE actually sends for each transition (ground truth for wire content) --- ## Watchouts - **The named-retail leaf `MoveToStatePack::UnPack` returned 0 hits in TTD.** Don't query it directly; start at `CPhysicsObj::unpack_movement` or `MovementManager::unpack_movement` and walk down the call chain. Release-build inlining + virtual dispatch hide leaf decoders from TTD's static call counting (#44 lesson). - **Verify positioning IN-CLIENT before recording.** A trace where retail #1 can't see retail #2 is wasted bytes. Walk both characters to within visible range, confirm the actor is rendered on the observer's screen, THEN attach TTD. The 30-min validation experiment on 2026-05-06 wasted its main probe because of this — don't repeat. - **Outbound encoding quirk in our wire path** (CLAUDE.md "Outbound motion wire format"): acdream sends `WalkForward (0x05) + HoldKey.Run` for run, which ACE auto-upgrades to `RunForward (0x07)` when relaying to remote observers. So the inbound parser sees `fwd=0x07` for "remote is running." Don't get confused if outbound code shows 0x05 but inbound shows 0x07 — that's normal ACE behavior. - **TTD ring buffer with 512 MB max + ~60 sec recording** should fit the whole scenario without rollover. If you record longer, consider bumping to 1024 MB or chopping the scenario into multiple recordings. - **Don't kill the TTD process** with Stop-Process — it kills retail too. Use Ctrl+C in the TTD window or `TTD.exe -stop ` from another elevated shell. --- ## Definition of done This investigation is done when: 1. The TTD trace + queries identify the exact retail function(s) that refine the cycle from UP-derived velocity. Function name(s) + address(es) documented in `docs/research/2026-05-06-locomotion-cycle-transitions/findings.md`. 2. The threshold/grace logic retail uses is documented (timing values, conditions). 3. `ApplyServerControlledVelocityCycle` un-gating shipped in acdream with the corresponding test and visual verification on all 7 phases. 4. ISSUES.md #39 closed with the commit SHA. 5. Memory `project_retail_motion_outbound.md` (or a new note for the inbound side) gets the durable lessons appended. --- ## Time estimate - Recording: ~30 min (setup, two retail clients, scenario execution) - Initial query passes + decomp cross-reference: ~1–2 hours - Implementation + iteration: ~2–4 hours - Visual verification + commit: ~30 min Total: half a day to a full day depending on how clean retail's path is. If after the first TTD pass the retail receive code looks fundamentally different from what `ApplyServerControlledVelocityCycle` is doing, stop and reassess — the fix shape may need to change. Don't try to ram a mismatched approach through.