S0 wire probe (live capture, retail actor via ACE) refuted the premise the #39 machinery was built on: walk<->run Shift toggles arrive as EXPLICIT UMs (0x0005 <-> 0x0007@runRate) because retail's default- difference packing baselines forward_command against Ready — W-held is always packed, and ACE re-emits it unconditionally with the holdKey upgrade (MovementData.cs:104-119). The frequent flags=0 autonomous UMs are genuine keys-released / heading-only states; retail applies them as full stops (InterpretedMotionState ctor 0x0051e8d0 defaults Ready + move_to_interpreted_state 0x005289c0 flat copy). Retail has NO pace->animation adaptation anywhere in its inbound pipeline (two decomp dives + ACE cross-check, deviation map DEV-2), and the refinement layer's 0.2s-grace re-promotion after legitimate Ready UMs was itself the observed Ready<->Run thrash / rubber-band component. Deleted: ApplyPlayerLocomotionRefinement, UmGraceSeconds, PlayerRunPromoteSpeed/PlayerRunDemoteSpeed, RemoteMotion.LastUMTime, the synth-player refinement call in OnLivePositionUpdated. Player-remote cycles are now UM-driven only, exactly like retail. NPC PlanFromVelocity path untouched (S6 unifies). S0 findings + S1 live validation (0 false UM_STALE drops across 280 UMs) recorded in docs/research/2026-07-02-inbound-motion-deviation-map.md. fix #39: closed — root-cause narrative corrected, machinery removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
22 KiB
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
- 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-Cacclient_2013_pseudo_c.txt:699322-699328, addr 0x006b37a8→0x006b3852) callsSetHoldRunthenSendMovementEvent()whenever the player is not standing still — i.e. a Shift toggle while W is held emits a fresh MoveToState. (Same for HoldSidestep0x85000002.) - ACE relays.
GameActionMoveToState.cs:36callsBroadcastMovement(moveToState)unconditionally for every inbound MoveToState (Player_Networking.cs:364-365→GameMessageUpdateMotionbroadcast, 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.
- Retail sends.
- 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→InterpolationManagerchase) has zero writes intoCMotionInterp— it only readsget_adjusted_max_speed()(×2.0) as the chase-speed cap. The wire's velocity field is a dead parameter inMoveOrTeleport(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. - 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 andCSequence::set_velocityis a hard overwrite — the sequence velocity snaps to the new cycle's value the same tick (pseudo-C:298437-298468, 300798-300814; ACEMotionTable.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. - 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_motionvelocity 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_modifyre-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). |
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_state0x005289c0), 305713 (apply_interpreted_movement0x00528600), 298636 (GetObjectSequence0x00522860), 293301 (copy_movement_from0x0051e750). 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 throughDoInterpretedMotion/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 — zeroCMotionInterpwrites;MoveOrTeleportvelocity param dead at 284304). - DEV-3 — retail: 298954 (
StopSequenceMotion0x00522fc0), 305635 (StopInterpretedMotion0x00528470), 301194 (clear_physics0x00524d50). 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 = onlyInterp.Clearsite); 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_motion0x005224b0) + 300798-300814 (set_velocity/set_omegaoverwrites), ACEMotionTable.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=0x09arrives, acdream plays Ready (2026-06-26 audit D9). - DEV-6 — retail: 271370 (
SetObjectMovementstaleness gate), 357224 (update_times[8]compare). acdream:UpdateMotion.cs:89-106,UpdatePosition.cs:29-31/156(parsed-then-discarded). - DEV-7 — retail: 305238 (
MotionDone0x00527ec0), 290854 (MotionTableManager::add_to_queue0x0051bfe0), 290771 (remove_redundant_links0x0051bf20), 290645 (CheckForCompletedMotions). acdream:AnimationSequencer.cs:401(always-synchronous SetCycle),:1128-1129(AnimationDoneSentinelexists, 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_velocityconstants 3.12/4.0 — the logical values acdream synthesizes). acdream:AnimationSequencer.cs:686-754. - DEV-9 — retail: 298636 (
re_modifyat rebuild tail), 298954 (modifiersubtract_motionbranch), 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_movementintoMotionInterpreter, withDoInterpretedMotionrouting to the existingAnimationSequencer.SetCycleas theGetObjectSequencebackend.OnLiveMotionUpdated's remote SubState branch collapses to onemove_to_interpreted_state(ims)call; stop flows throughStopInterpretedMotion(noInterp.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.
MotionTableManagercompanion (pending_animations,add_to_queue,remove_redundant_links,CheckForCompletedMotions) wiring the existingAnimationDoneSentinel;pending_motions/MotionDoneinMotionInterpretersecond. 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_linksremoves 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;
SetCycleresolves 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
InterpolationManagerchase 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.
S0 wire-probe RESULTS (2026-07-02, post-S1 live capture)
Observer acdream (+Acdream), actor retail-driven (guid 0x5000000B), structured
protocol incl. Shift toggles. 280 UMs captured (launch-s0-wireprobe.log).
- The wire is NOT silent — DEV-2's premise is dead. Walk↔run toggles
arrive explicitly:
fwd=0x0005 fwdSpd=null(=1.0)↔fwd=0x0007 fwdSpd=2.85. Root cause of the 2026-05-06 misreading: retail's default-difference packing baselinesforward_commandagainst Ready (RawMotionState.Default, our own D6.2b verbatim port) — W-held is non-default and always packed, so ACE (MovementData.cs:104-119) always re-emits it with the holdKey upgrade + run-skill speed. - flags=0 "empty" autonomous UMs are frequent (127/209) and LEGITIMATE
— they are genuine keys-released / heading-only MoveToState relays
(all-default raw state packs nothing; ACE
RawMotionState.Readleaves absent = Invalid;MovementDataemits nothing). Retail semantics (verified:InterpretedMotionStatector 0x0051e8d0 defaultsforward_command=Ready;MovementManager::unpack_movementcase 0 UnPacks into a default-constructed state;CMotionInterp::move_to_interpreted_state0x005289c0 does a FLATcopy_movement_from+apply_current_movement): an empty UM = a real full stop. acdream already maps it to Ready — but the DEV-2 refinement then re-promotes the cycle from UP pace after the 0.2 s grace, producing a Ready↔Run thrash against legitimate stop signals. That thrash is the observed flicker/rubber-band component. - S1 gate validated live: 0
[UM_STALE]drops across 280 UMs; movementSeq advances +1 per UM; byte-level layout confirmed (instanceSeq|movementSeq|serverControlSeq|isAutonomousexactly where the parser reads them). - Also confirmed in
unpack_movement(0x00524440) for the S2 port: the outer style u16 routes throughcommand_ids[]and applies viaDoMotionONLY when it differs from the current style; motionFlags carries sticky-object (0x100 →stick_to_object) and standing_longjump (0x200); the actions list is consumed under a 15-bitserver_action_stampwraparound compare (move_to_interpreted_state0x005289c0, lines 305953-305989).
Disposition: S5 = delete the player pace-inference layer (executed in the
same session; NPC PlanFromVelocity path untouched — its unification is S6).
Provenance
- Workflow
wf_b92a5670-283: 4 read-only mappers (retail decomp / ACE port / acdream code / prior-research claims — full reports preserved in2026-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,UpdatePositionparser, ACEPlayer_Tick.cs/Player_Networking.cs/GameActionMoveToState.cs, decompSendMovementEvent/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/558reference different retail functions — the only funnel comment-refs areGameWindow.cs:4517+PlayerMovementController.cs:593); DEV-4 rewritten per its refutation.