acdream/docs/research/2026-07-02-inbound-motion-deviation-map.md
Erik fb3ee0544a docs(L.2g): inbound motion deviation map + campaign registration
/investigate deliverable for the inbound (remote-entity) animation+position
retail-parity effort. 10 deviations (DEV-1..10) mapped and adversarially
verified against the named retail decomp + ACE port + current code (9
confirmed, 1 refuted-and-corrected).

Headline: the #39-era UP-pace->cycle inference layer's premise ('wire goes
silent on Shift toggle') is refuted at both oracles — retail sends a fresh
MoveToState on HoldRun toggle while moving (0x006b37a8) and ACE rebroadcasts
every MoveToState unconditionally (GameActionMoveToState.cs:36); retail has
NO pace->animation adaptation anywhere (position error is absorbed solely by
the InterpolationManager chase, already ported verbatim in L.3).

Registers sub-lane L.2g in the roadmap: port the CMotionInterp inbound funnel
verbatim for all remote entity classes, slices S0-S6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 15:20:46 +02:00

283 lines
20 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.

# Inbound motion deviation map — remote-entity animation + position vs retail
Date: 2026-07-02
Mode: **/investigate report — no code changes made.** This doc + the four mapper
reports under `2026-07-02-inbound-motion-maps/` are the only writes (uncommitted).
Branch: `claude/vigorous-joliot-f0c3ad`. Follows the handoff
`2026-07-02-inbound-motion-verbatim-port-handoff.md`.
## Symptom (acceptance oracle — user's live observation, axiom)
Watching a remote player in acdream: walk↔run transitions WITHOUT stopping react
too slowly in the motion interpreter, throwing animation + position off afterward
and compounding through continued running/turning; plus general sliding + position
errors on stop.
## Headline findings
1. **The premise behind acdream's current walk↔run mechanism is refuted.** The
#39-era UP-velocity refinement layer (`ApplyPlayerLocomotionRefinement`,
0.2 s UM-grace + 510 Hz UP cadence + 5.5/4.5 m/s hysteresis) was built on the
2026-05-06 finding "the wire goes silent on Shift-toggle." Both oracles now
contradict that finding:
- **Retail sends.** `CommandInterpreter`'s HoldRun command handler
(`0x85000001`, pseudo-C `acclient_2013_pseudo_c.txt:699322-699328`, addr
0x006b37a8→0x006b3852) calls `SetHoldRun` then `SendMovementEvent()`
**whenever the player is not standing still** — i.e. a Shift toggle while W
is held emits a fresh MoveToState. (Same for HoldSidestep `0x85000002`.)
- **ACE relays.** `GameActionMoveToState.cs:36` calls
`BroadcastMovement(moveToState)` **unconditionally** for every inbound
MoveToState (`Player_Networking.cs:364-365``GameMessageUpdateMotion`
broadcast, with the WalkForward+HoldKey.Run→RunForward upgrade and
run-skill speed recompute).
So an observer **should** receive a UM with ForwardCommand flipping
Walk↔Run on every Shift toggle. Why the 2026-05-06 clean test saw only
Ready↔Run `[FWD_WIRE]` transitions is the one open empirical question —
settle it with probe S0 below before touching the refinement layer.
2. **Retail never adapts animation from observed pace.** Three independent decomp
dives + the ACE port cross-check all converge: the inbound position path
(0xF748/0xF619 → `SmartBox::HandleReceivedPosition``MoveOrTeleport`
`InterpolationManager` chase) has **zero writes into `CMotionInterp`** — it
only *reads* `get_adjusted_max_speed()` (×2.0) as the chase-speed cap. The
wire's velocity field is a **dead parameter** in `MoveOrTeleport`
(pseudo-C:284304 — passed, never dereferenced). Animation changes come only
from motion events (0xF74C → `MovementManager::unpack_movement`).
acdream's pace→cycle inference layer has **no retail equivalent** (DEV-2)
and is an **unregistered deviation**.
3. **acdream's instant DR-velocity snap on walk↔run is retail-correct; the
pose hard-cut is the real deviation.** Adversarial verification refuted the
initial "retail paces velocity through the link clip" claim: in
`GetObjectSequence`'s link-transition branch, `add_motion(new cycle)` is the
LAST synchronous call and `CSequence::set_velocity` is a hard overwrite —
the sequence velocity snaps to the new cycle's value the same tick
(pseudo-C:298437-298468, 300798-300814; ACE `MotionTable.cs:144-168`,
`Sequence.cs:127-130`). What acdream's Fix B loses is only the link **pose**
(plus its authored root-motion frames): retail plays the authored
walk↔run link clip to completion before the new cycle's animation starts.
4. **Retail's remote pipeline is one atomic funnel; acdream's is three loosely
coupled writes.** That structural split (DEV-1) is what lets every gap
(DEV-2/3/5/6) desync pose from velocity with nothing to reconcile them.
## The retail inbound model (what "verbatim" means here)
Two decoupled inbound paths, meeting only at the per-tick frame combiner:
**Motion events (0xF74C / 0xF619-embedded)**
`ACSmartBox::DispatchSmartBoxEvent` (pseudo-C:357117) →
`CPhysics::SetObjectMovement` (271370, staleness-gated on `update_times[8]`) →
`MovementManager::unpack_movement` (300563, **10-way jump table**: case 0 =
InterpretedMotionState, 6/7 = MoveTo, 8/9 = TurnToObject/TurnToHeading) →
`CMotionInterp::move_to_interpreted_state` (305936: flat `copy_movement_from`
overwrite of `interpreted_state`, caches `my_run_rate` from RunForward's
forward_speed) → `apply_interpreted_movement` (305713: per-slot
`DoInterpretedMotion`/`StopInterpretedMotion` dispatch in retail order — style,
forward, sidestep-or-stop, turn-or-stop, idle-stop enqueue) →
`CMotionTable::GetObjectSequence` (298636), which decides:
- **same substate + same speed sign** → fast path: `change_cycle_speed`
(playback rate × newSpeed/oldSpeed) + `subtract_motion`/`combine_motion`
velocity swap — same node keeps playing, **no restart** (acdream's SCFAST
fast-path is the equivalent);
- **substate change (walk↔run!)** → link path: `get_link` (double-hop through
the style default when no direct link / sign flip), `clear_physics` +
`remove_cyclic_anims` (keeps a mid-playback link, drops the old loop),
`add_motion(link)` + `add_motion(cycle)`, `re_modify` re-applies active
modifiers. Velocity = the **cycle's** (last overwrite, instant); pose = link
first, then cycle.
Bookkeeping: `MotionTableManager::add_to_queue`/`remove_redundant_links`/
`CheckForCompletedMotions` (290645-290854) + `CMotionInterp::pending_motions`/
`MotionDone` (305032/305238) reconcile completed commands at anim boundaries.
**Position events (0xF748/0xF619)**`SmartBox::HandleReceivedPosition`
(92896): for remotes, `MoveOrTeleport` (284304) only — staleness-gated
(`POSITION_TS`/`TELEPORT_TS`), >96 units from player → plain snap, else
`InterpolationManager::InterpolateTo` node queue (cap 20). Per tick,
`adjust_offset` (353071) moves the full remaining distance capped at
`2 × get_adjusted_max_speed()` (fallback 7.5 m/s), stall detection (<30%
progress per 5 frames fail counter >3 → hard snap). **acdream's
`InterpolationManager` is already a verbatim port of this (L.3).**
**Per tick** (`UpdateObjectInternal` 283611): `CPartArray::Update`
`CSequence::update` fills the frame delta from **the animation frames' baked
per-frame deltas** (anim-node path) or `velocity×dt` (`apply_physics` fallback);
`PositionManager::adjust_offset` **overwrites** it when a chase node is active
(last-writer-wins, NOT additive — matches acdream's `ComputeOffset` REPLACE
dichotomy); then the **full collision sweep** (`transition`) runs for remotes
too (acdream deliberately skips it for grounded player remotes — DEV-10).
**Stop** — one path for every stop: `CMotionTable::StopSequenceMotion` (298954)
re-enters `GetObjectSequence` targeting the style default with force=1:
`clear_physics` zeroes velocity/omega **at transition start**, the idle-link's
authored deceleration plays, `add_to_queue(READY)` + `RemoveMotion` bookkeeping.
Modifier stop = `subtract_motion` only. No pace-derived stop inference.
## Ranked deviation map
All 10 deviations were adversarially verified through two lenses (refute vs
current acdream code / refute vs retail decomp+ACE). 9 CONFIRMED, 1 REFUTED
(DEV-4's velocity half — corrected below). Full verify evidence:
`…/tasks/waqdgyk0m.output` (session temp) — the load-bearing citations were
additionally re-read first-hand in this session.
| # | Sev | Deviation | Symptom link |
|---|-----|-----------|--------------|
| DEV-1 | HIGH | Remote inbound bypasses the CMotionInterp funnel: bulk-copy + direct `SetCycle` instead of `move_to_interpreted_state`/`apply_interpreted_movement` | Structural root: pose + DR velocity are separate writes; every gap desyncs them with no reconciliation |
| DEV-2 | HIGH | Non-retail grace-window + hysteresis pace→cycle inference for player remotes (`UmGraceSeconds=0.2`, promote 5.5 / demote 4.5 m/s) | The direct "reacts too slowly": ≥0.2 s + UP cadence + hysteresis-crossing before the gait flips; backlog builds meanwhile. **Unregistered deviation**; premise refuted (headline 1) |
| DEV-3 | HIGH | Stop is three unwired mechanisms (UM-Ready overwrite / dead UP-velocity StopCompletely / landing); no `StopSequenceMotion` path; nothing zeroes body velocity or reconciles the queue | Direct cause of stop-slide: pose goes Ready while the body drains pre-stop waypoints at up to 2× maxSpeed |
| DEV-4 | MED (corrected) | Fix B drops the authored walk↔run link **pose** (hard visual cut + lost link root motion). ~~Velocity snap~~ — REFUTED: retail snaps velocity instantly too | Visual-only hard cut at each toggle; NOT a position-error source |
| DEV-5 | HIGH | Inbound movement types 8/9 (TurnToObject/TurnToHeading) dropped → misapplied as `SetCycle(Ready)` | Mid-locomotion turn command becomes a spurious full stop — error injected exactly during "compounding through running/turning" |
| DEV-6 | MED | Sequence-number staleness gate missing (movementSequence/serverControlSequence/isAutonomous parsed-then-discarded; retail gates on `update_times[8]`) | A reordered stale UM re-applies the old gait or un-stops a stop (extends register TS-26 to UM) |
| DEV-7 | MED | `pending_motions`/`pending_animations`/`MotionDone`/`remove_redundant_links` lifecycle absent | Enabler: missing `remove_redundant_links` is what made rapid toggles restart links forever, forcing Fix B; without MotionDone nothing clears per-transition bookkeeping |
| DEV-8 | MED | Remote DR velocity synthesized from hardcoded gait constants at SetCycle time vs retail's MotionData-accumulated CSequence velocity (+ baked per-frame anim deltas) | Any per-gait speed mismatch vs ACE's integration = constant-rate error that fills the interp backlog → discharges as slide/snap |
| DEV-9 | MED | Modifier layer missing: no `re_modify`, single-cycle selection, omega formula-seeded instead of combine/subtract | Turn contribution approximated independently of the motion table → heading-rate error while running+turning |
| DEV-10 | LOW | Bifurcated position-drive paths; grounded player remotes skip the per-tick collision sweep (retail sweeps every entity) | None for this symptom directly; parity + pass-through-geometry hazard |
### Key anchors per deviation
- **DEV-1** — retail: pseudo-C 305936 (`move_to_interpreted_state` 0x005289c0),
305713 (`apply_interpreted_movement` 0x00528600), 298636 (`GetObjectSequence`
0x00522860), 293301 (`copy_movement_from` 0x0051e750). acdream:
`GameWindow.cs:4590/4598` (bulk-copy), `:4769` (separate SetCycle), `:4517`
(comment-only funnel ref); D6.2 raw-input funnel is local-player-only
(`PlayerMovementController.cs:761-1014`). Nuance (verify correction): the
sidestep/turn axes DO route through `DoInterpretedMotion`/`StopInterpretedMotion`
(`GameWindow.cs:4794-4857`); the forward axis — the crux — is the bulk-copy.
- **DEV-2** — acdream: `GameWindow.cs:5095/5104/5110` (constants),
`:5112-5300` (both methods), `:5128-5134` (the unconfirmed-premise comment).
Retail: no equivalent exists (searched: InterpolationManager/PositionManager
ranges 352000-353400 — zero `CMotionInterp` writes; `MoveOrTeleport` velocity
param dead at 284304).
- **DEV-3** — retail: 298954 (`StopSequenceMotion` 0x00522fc0), 305635
(`StopInterpretedMotion` 0x00528470), 301194 (`clear_physics` 0x00524d50).
acdream: `GameWindow.cs:4364-4367` (UM-Ready), `:5711-5731` (dead UP-velocity
stop — ACE player UPs carry velocity=null per `:5700-5710`), `:5527-5553`
(landing = only `Interp.Clear` site); plus the acdream-invented 300 ms
stop-detection window (`GameWindow.cs:4862-4890` + TickAnimations) — also
retail-less, part of this cluster.
- **DEV-4** — retail: 298636 link branch; 298552 (`get_link`); 301777
(`append_animation`); velocity-overwrite refutation: 298437-298468
(`add_motion` 0x005224b0) + 300798-300814 (`set_velocity`/`set_omega`
overwrites), ACE `MotionTable.cs:144-168` + `Sequence.cs:127-130/221-230`.
acdream: `AnimationSequencer.cs:585-635` (Fix B), `:686-754` (velocity
synthesis — retail-equivalent in timing).
- **DEV-5** — retail: 300563/300707 (jump table cases 8/9 →
`MoveToManager::TurnToObject/TurnToHeading`). acdream:
`UpdateMotion.cs:136-238` (no case 8/9), `GameWindow.cs:4364-4367`
(null-command → Ready misfire). Confirmed live earlier: `mt=0x09` arrives,
acdream plays Ready (2026-06-26 audit D9).
- **DEV-6** — retail: 271370 (`SetObjectMovement` staleness gate), 357224
(`update_times[8]` compare). acdream: `UpdateMotion.cs:89-106`,
`UpdatePosition.cs:29-31/156` (parsed-then-discarded).
- **DEV-7** — retail: 305238 (`MotionDone` 0x00527ec0), 290854
(`MotionTableManager::add_to_queue` 0x0051bfe0), 290771
(`remove_redundant_links` 0x0051bf20), 290645 (`CheckForCompletedMotions`).
acdream: `AnimationSequencer.cs:401` (always-synchronous SetCycle),
`:1128-1129` (`AnimationDoneSentinel` exists, unconsumed by any motion
bookkeeping).
- **DEV-8** — retail: 298437-298492 (`add_motion`/`combine_motion`/
`subtract_motion`), 302402 (`CSequence::update` — anim-frame deltas are the
primary drive), 305160 (`get_state_velocity` constants 3.12/4.0 — the
*logical* values acdream synthesizes). acdream: `AnimationSequencer.cs:686-754`.
- **DEV-9** — retail: 298636 (`re_modify` at rebuild tail), 298954 (modifier
`subtract_motion` branch), 305713 (per-slot dispatch). acdream:
`GameWindow.cs:4648-4683` (single-cycle pick), `:4841-4847` (ObservedOmega
formula seed), `:10097-10106` (manual omega integration).
- **DEV-10** — retail: 280817/283611 (sweep for all entities). acdream:
`GameWindow.cs:9717-9722` (fork), `:9883-9890` (sweep skipped, player-remote
path), `:10156-10181` (sweep present, NPC path), `:5657` (NPC UP hard-snap).
## Recommended port campaign (for approval — no code written yet)
Ordered so each slice is independently landable, tests-first, minimal-surgery
(no sequencer rewrite; `SetCycle` internals stay). Every slice updates
`docs/architecture/retail-divergence-register.md` in the same commit.
- **S0 — the wire probe (no code).** Live capture, acdream as observer,
actor driven from retail: `ACDREAM_DUMP_MOTION=1 ACDREAM_REMOTE_VEL_DIAG=1`,
structured protocol with the walk↔run Shift toggle as the centerpiece;
optionally WireMCP on loopback :9000 for the raw S→C bytes. Expected per the
oracles: a UM with ForwardCommand flipping Walk↔Run on every toggle
(`[UM_RAW]` is the arbiter — the 2026-05-06 test read `[FWD_WIRE]`, which
only fires on interpreted-command *change* after acdream's own resolution).
Outcome decides DEV-2's disposition. *(Needs the user driving the retail
client — this is the one stop-and-wait step.)*
- **S1 — DEV-6 staleness gate.** ~30 isolated lines: capture the two u16s into
`UpdateMotion.Parsed`, retail wraparound-newer compare per guid, drop stale.
Unit-testable against the 0x7fff wraparound rule. Safe first commit.
- **S2 — DEV-1 funnel (the centerpiece), absorbing DEV-3 + DEV-9 dispatch.**
Port `move_to_interpreted_state` + `apply_interpreted_movement` into
`MotionInterpreter`, with `DoInterpretedMotion` routing to the existing
`AnimationSequencer.SetCycle` as the `GetObjectSequence` backend.
`OnLiveMotionUpdated`'s remote SubState branch collapses to one
`move_to_interpreted_state(ims)` call; stop flows through
`StopInterpretedMotion` (no `Interp.Clear()` band-aid — the queue's final
waypoints ARE the true stop position); sidestep/turn become per-slot retail
dispatch. Parity test first: replay a captured UM stream through old + new
paths, assert identical SetCycle sequence + InterpretedState trace; then a
stop-slide acceptance case.
- **S3 — DEV-7 bookkeeping.** `MotionTableManager` companion (pending_animations,
`add_to_queue`, `remove_redundant_links`, `CheckForCompletedMotions`) wiring
the existing `AnimationDoneSentinel`; `pending_motions`/`MotionDone` in
`MotionInterpreter` second. Additive, decomp-semantics unit tests first.
- **S4 — DEV-4 corrected: retire Fix B.** Restore the link-pose enqueue for
locomotion↔locomotion now that S3's `remove_redundant_links` removes Fix B's
original justification (link-restart spam). Velocity stays instant
(retail-correct). Acceptance: user visual side-by-side.
- **S5 — DEV-2 disposition per S0.** If UMs arrive: delete the refinement layer
(dead weight + latency + feedback loop) and let the S2 funnel drive. If the
wire is genuinely silent: keep a minimal layer as an explicitly registered
ACE-adaptation row (a retail client on the same wire would show the stale
gait too — matching retail display means NOT inferring), and file the gap
against ACE. Either way DEV-2's machinery stops being an unregistered
deviation.
- **S6 — remainder.** DEV-5 (parse types 8/9 → heading target; cross-check field
order vs Chorizite + holtburger first), DEV-8 (evidence first: dump Humanoid
MotionTable Walk/Run `MotionData.Velocity`+Flags from the dat, diff synthesized
magnitudes vs ACE's integration), DEV-10 deferred (fold-in after S2; NPC
hard-snap replacement needs explicit user approval per the reverted-campaign
precedent).
## What we've ruled out
- "acdream lacks any transition machinery" — false; `SetCycle` resolves authored
links (`GetLink`) and has the retail same-substate fast path (SCFAST ≈
`change_cycle_speed`).
- "Retail paces the walk↔run *velocity* change through the link clip" — refuted
(headline 3); instant velocity snap is retail-correct.
- "Retail refines the remote's cycle from UP-derived velocity" (#39's premise)
— refuted; no such mechanism exists in the decomp (two independent dives +
ACE cross-check).
- The `InterpolationManager` chase itself — already a verbatim port (L.3);
constants re-verified this session (2× adjusted max speed, 0.05 desired
distance, 30%/5-frame stall, fail>3 blip, 20-node cap, 96-unit gate upstream).
## What this is NOT
This is NOT a "port the whole CSequence/CPartArray stack" campaign — the
sequencer's node/link/queue mechanics are close enough that the funnel (DEV-1),
bookkeeping (DEV-7), and stop path (DEV-3) can be ported around it surgically;
and it is NOT a dead-reckoning-tuning problem — the chase is already retail,
it's the inputs (cycle + velocity source + stop signal) that deviate.
## Provenance
- Workflow `wf_b92a5670-283`: 4 read-only mappers (retail decomp / ACE port /
acdream code / prior-research claims — full reports preserved in
`2026-07-02-inbound-motion-maps/`) → synthesis → 2-lens adversarial verify
per deviation (25 agents, ~3.0 M tokens). The ACE-port mapper crashed on
structured output AFTER writing its report file; synthesis read all four
files. One verify lens (DEV-4 retail side) delivered the REFUTED verdict
incorporated above.
- Targeted agents: retail remote-anim adaptation dive (+2 children:
InterpolationManager decomp dump, ACE InterpolationManager cross-check).
- First-hand session reads: `OnLiveMotionUpdated` (GameWindow 4230-4930),
DR tick path (9700-9920), `AnimationSequencer.SetCycle` (340-790),
`ApplyServerControlledVelocityCycle`/`ApplyPlayerLocomotionRefinement`
(5077-5300), `ServerControlledLocomotion`, `PositionManager`,
`InterpolationManager`, `UpdatePosition` parser, ACE `Player_Tick.cs` /
`Player_Networking.cs` / `GameActionMoveToState.cs`, decomp
`SendMovementEvent`/HoldRun sites (699274-700312), 2026-06-04 deep-dive
divergence list, ISSUES #39, divergence register scan.
- Corrections applied to the raw workflow output: DEV-1 anchor fix
(`MotionInterpreter.cs:399/412/558` reference different retail functions —
the only funnel comment-refs are `GameWindow.cs:4517` +
`PlayerMovementController.cs:593`); DEV-4 rewritten per its refutation.