/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>
283 lines
20 KiB
Markdown
283 lines
20 KiB
Markdown
# 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 + 5–10 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.
|