# Retail motion outbound trace — findings **Date:** 2026-05-01 **Tool:** cdb 10.0.28000.1839 attached non-invasively to live retail acclient.exe v11.4186 (Sept 2013 EoR build, GUID `9e847e2f-777c-4bd9-886c-22256bb87f32`). **Symbols:** `refs/acclient.pdb` — 18,366 named functions, 5,371 named struct/class types. **Server:** local ACE on `127.0.0.1:9000`. Character: `+Acdream`, spawned in Holtburg. ## TL;DR - **WASD movement does NOT go through `ACCmdInterp::SendDoMovementEvent`.** Across two captures totalling ~5 minutes of running, jumping, and turning, that breakpoint fired **zero** times. `SendDoMovementEvent` is apparently slash-command-only (`/run`, `/walk`, `/sneak`, etc.). The per-frame movement send path is `CommandInterpreter::SendMovementEvent` (which we couldn't trace stably — see "Open" below). - **Jump goes through `CM_Movement::Event_Jump`.** It fires **per-frame while the spacebar is held** (charge phase), not just once per jump. 44 hits in ~30 sec of jumping. Same `JumpPack` stack pointer every time — function probably packs every frame but conditionally sends. - **AutoPos heartbeat fires often during sustained motion** (~5 Hz at rest after gating, much higher under motion). It's gated by `transient_state & 1 && transient_state & 2 && Position::IsValid` so **does not fire when the player is standing still**. - **`set_heading` is for player rotation only, not camera mouse-look.** Camera pan does not fire it; turn keys / strafe / character autoface do. - **`set_state`** XOR mask shows which physics-state bits change on each call. Distinct values observed: 11 different new-state bitmasks across two captures (see "set_state bitmask atlas" below). ## What we ran | Run | Duration | Activity | Hits | |-----|----------|----------|------| | v1 | ~60 sec | Run forward + turn | 127 | | v2 | ~3 min | Run + jump + turn (holding W, repeated jumps) | 247 | Both with the same six-breakpoint set: `SendDoMovementEvent`, `SendStopMovementEvent`, `set_state`, `set_heading`, `Event_Jump`, `Event_AutonomousPosition`. Logs at [motion_trace_v1_walk_and_turn_2026-05-01.log](../../../.claude/worktrees/jovial-blackburn-773942/motion_trace_v1_walk_and_turn_2026-05-01.log) and [motion_trace_v2_run_jump_turn_2026-05-01.log](../../../.claude/worktrees/jovial-blackburn-773942/motion_trace_v2_run_jump_turn_2026-05-01.log). A v3 attempt added `CommandInterpreter::SendMovementEvent` (the per-frame MoveToState gateway). The cdb attach + breakpoint setup with that 7th breakpoint added enough overhead that retail's network thread starved; ACE timed out the session and retail crashed within seconds of attach. That bp is too high-frequency to instrument with a `printf` in the action — needs a separate, minimal-overhead trace (counter only, no print). ## Hit distribution (v2) | BP | Hits | Notes | |----|------|-------| | `set_state` | 159 | All 11 distinct new-masks; clusters around motion transitions | | `Event_Jump` | 44 | Bursts during sustained spacebar-hold | | `Event_AutonomousPosition` (printed) | 38 | Throttle 1/10 → ~370 actual hits | | `set_heading` (printed) | 6 | Throttle 1/10 → ~60 actual; only fires on player rotate | | `SendDoMovementEvent` | 0 | **Confirmed unused for keyboard motion** | | `SendStopMovementEvent` | 0 | **Confirmed unused** | ## set_state bitmask atlas Captured `new` masks (current state was `0x00400c08` baseline, sometimes `0x00410c08`). Distinct values across both runs: | `new` | bits set | Likely meaning (cross-checked vs `PHYSICS_STATE` enum candidates) | |-------|----------|--------------------------------------------------------------------| | `0x00000408` | 3, 10 | Base "in world, animating" | | `0x00000410` | 4, 10 | Walk / sidestep variant | | `0x00000414` | 2, 4, 10 | Walking + something | | `0x00000418` | 3, 4, 10 | Walking + alt | | `0x00000c0c` | 2, 3, 10, 11 | Multi-state combo | | `0x00000c14` | 2, 4, 10, 11 | Combo near jump | | `0x00010008` | 3, 16 | Bit 16 likely "scripted/cinematic"? | | `0x00020414` | 2, 4, 10, 17 | Bit 17 likely something high | | `0x00200418` | 3, 4, 10, 21 | Bit 21 likely "frozen/inert" | | `0x00600418` | 3, 4, 10, 21, 22 | Bits 21+22 — common during motion | | `0x0060041c` | 2, 3, 4, 10, 21, 22 | Same + bit 2 | **Action item:** decode the `PhysicsState` enum bits from `docs/research/named-retail/acclient.h` and label this table authoritatively. The `cur` value `0x00400c08` (bits 3, 10, 11, 22) should be the "neutral, in-world, alive" base. ## set_heading angle samples Angles captured (4-byte float bit pattern, decoded): | Hex | Float (degrees) | |-----|-----------------| | `0x00000000` | 0.0 (north) | | `0x41c8af72` | 25.09 | | `0x43070000` | 135.0 | | `0x43083c22` | 136.23 | | `0x430d7596` | 141.46 | | `0x43340000` | 180.0 | | `0x433a1741` | 186.09 | | `0x43a9d6c7` | 339.68 | Confirms heading is **degrees, float, 0–360 wrap**. Matches the acdream-side encoding in the outbound builders. ## Event_Jump pattern 44 hits in v2 — way more than user keypresses. Pattern: ``` [70..92] burst of 9 hits (one charged jump session) [97..138] burst of 11 hits (another charged jump) [251..408] continuous hits (multiple chained jumps) ``` Same `JumpPack` pointer (`001af5f8`) on every hit — stack-allocated local in the caller. Consistent with `Event_Jump` being called **per-frame while the jump button is held to charge**, with the function itself deciding whether to actually send the pack on the wire. We did not break inside the function so we don't have the send/no-send ratio. **Action item:** trace one jump in isolation — set a bp on `Event_Jump` AND on the BSTREAM-write deeper in the function. Capture how many of the per-frame Event_Jump calls actually emit a 0xF61B JumpAction packet. acdream's [JumpAction.cs](../../../src/AcDream.Core.Net/Messages/JumpAction.cs) sends one JumpAction per spacebar release — if retail sends one per charge-frame, our outbound is wrong (or vice-versa). ## AutoPos heartbeat behaviour `CM_Movement::Event_AutonomousPosition` is called from `CommandInterpreter::SendPositionEvent` (006b4770), which is **guarded** by: ```c if (this->smartbox != 0 && this->player != 0 && (transient_state & 1) != 0 && (transient_state & 2) != 0 && Position::IsValid(&player->m_position)) ``` `transient_state` bits 0 and 1 are required — this is why AutoPos **doesn't fire when the player is fully at rest**. During sustained motion it fires ~5 Hz at the throttle interval we saw (1 print every 10 hits = ~1 line/sec → ~50 hits in 5 sec). **Action item:** acdream's outbound heartbeat in [GameWindow.cs](../../../src/AcDream.App/Rendering/GameWindow.cs) is a 200ms periodic pulse. Verify whether retail's cadence matches our fixed 200ms or is event-driven (e.g. fires per-frame while moving and not at all at rest). If event-driven, we may be sending stale heartbeats while standing still that retail would suppress. ## Confirmed correct vs acdream-side | Behaviour | Retail observed | acdream | Match | |-----------|-----------------|---------|-------| | Heading encoding | float degrees 0–360 | float degrees 0–360 in MoveToState | ✓ | | Heartbeat at rest | does not send | sends every 200ms | **MISMATCH** — ours sends extras | | Slash-command motion path | `SendDoMovementEvent` (unused for WASD) | n/a (acdream doesn't send slash motions) | ✓ | | Jump per-frame charge | `Event_Jump` fires every frame while spacebar held | acdream sends 1 JumpAction on release | **POSSIBLE MISMATCH** — needs deeper trace | ## Open / what we still don't have 1. **The actual 0xF61C MoveToState send path** — retail's per-frame movement message. Our breakpoint on `CommandInterpreter::SendMovementEvent` (0x006B4680) was the right entry but adding it to the trace caused retail to crash (cumulative bp-action overhead). Needs a SEPARATE focused trace with just that bp + a counter (no `printf`). 2. **The exact wire bytes of MoveToState / AutonomousPosition / Jump.** Was planned for the v3 wire-trace; not run. Would require `RawMotionState::Pack` and `AutonomousPositionPack::Pack` breakpoints with `dt` struct dump + `db` byte dump. Same overhead concerns; do one bp at a time. 3. **Sequence-counter behaviour.** Our trace captured nothing about how retail bumps the four `Instance/ServerControl/Teleport/ForcePosition` sequence counters across messages. Needs taps inside `MoveToStatePack` / `AutonomousPositionPack` constructors. 4. **The `PhysicsState` enum decode.** Names map for the 0x408 / 0x418 / 0x600418 / 0x10008 etc. bitmasks lives in `docs/research/named-retail/acclient.h`. Pull the `PHYSICS_STATE` enum and label this trace's set_state column authoritatively. 5. **DoMov / StopMov context.** Confirmed unused for WASD, but `SendDoMovementEvent(0x85000001, 0x3f800000, 0)` was found in the pseudo-C as a real call site — find what triggers it (slash command? autorun toggle?) and document so our IS_SLASH_COMMAND test paths know what to expect. ## Toolchain notes - **`qd` is not allowed inside breakpoint command actions** in cdb. Empirically silently ignored (no parse error). Quoting the docs: "you cannot use commands that wait for input or end the debugger session in breakpoint command lists." `qd`, `q`, `qq` all fall under this. **Use `.detach` (a meta-command) instead** when you need a bp-driven detach. - **`cdb -pd` does NOT survive `Stop-Process -Force`.** `-pd` makes the *graceful* exit not terminate the debuggee. `TerminateProcess` (which is what `Stop-Process -Force` does) bypasses cleanup and the OS treats debugger termination as debuggee termination. Need either `qd`/`.detach` from inside, or send `CTRL_BREAK_EVENT` to cdb (via P/Invoke) and feed `qd` to its stdin. - **Per-frame breakpoints with `printf` actions are too expensive.** Adding `CommandInterpreter::SendMovementEvent` (called every motion tick, ~30 Hz) with a `printf` action lagged retail enough that the ACE session timed out within a few seconds. **For per-frame breakpoints, use counter-only actions** (`r $tN = @$tN + 1; gc`) — no `.printf`, no `poi` reads. ## Files - [motion_discovery.cdb](../../../.claude/worktrees/jovial-blackburn-773942/motion_discovery.cdb) — symbol resolution + prologue dump (one-shot, kept for reference) - [motion_trace.cdb](../../../.claude/worktrees/jovial-blackburn-773942/motion_trace.cdb) — current motion-trace script (v4, with `.detach`) - [run_motion_trace.ps1](../../../.claude/worktrees/jovial-blackburn-773942/run_motion_trace.ps1) — launcher with timer-based fallback - v1 log: walk + turn, no jump - v2 log: run + jump + turn ## Next session 1. Decode `PHYSICS_STATE` enum from `acclient.h`. 30 minutes. 2. Single-bp focused trace of `Event_Jump` with `db` byte dump on the pack, to determine send-rate vs charge-frame-rate. 3. Single-bp focused trace of `CommandInterpreter::SendMovementEvent` (counter only) to confirm per-frame fire and measure rate. 4. Wire-bytes trace using `RawMotionState::Pack` + post-`Pack` byte dump for one each of: idle, run-forward, run+turn, jump.