/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>
22 KiB
Claim map: inbound motion/animation research vs current code
Date: 2026-07-02. Repo (worktree): C:/Users/erikn/source/repos/acdream/.claude/worktrees/vigorous-joliot-f0c3ad
(branch claude/vigorous-joliot-f0c3ad, HEAD b60b9b4a).
Path note: docs/research/2026-06-04-animation-sequencer-deep-dive.md does NOT
exist in this worktree or in git history at all (it is an untracked file only
in the main checkout C:/Users/erikn/source/repos/acdream/docs/research/...).
It was never committed. Read directly from that absolute path for this audit.
The other two docs (2026-06-26-movement-animation-retail-parity-audit.md,
2026-07-01-d6-motion-interp-pseudocode.md) exist and are committed in this
worktree.
Doc 1: 2026-06-04-animation-sequencer-deep-dive.md (8 ranked divergences)
| # | Claim (doc anchor) | Current-code check | Status | Evidence (file:line) |
|---|---|---|---|---|
| 1 | Missing pending_motions/MotionDone chain (MotionInterp, HIGH). add_to_queue + CheckForCompletedMotions unimplemented. Affects multi-step action sequences, combo attacks, UpdateMotion Commands[] action chains. (doc anchor: "Divergences ranked" #1, L1242-1247) |
Grepped MotionInterpreter.cs/AnimationSequencer.cs for pending_motions/MotionDone/CheckForCompletedMotions/add_to_queue — only comment references (FUN_00510900 — CheckForCompletedMotions (animation flush, not simulated here), FUN_00528790(…) — add_to_queue). No queue data structure, no MotionDone event. |
STILL-TRUE | src/AcDream.Core/Physics/MotionInterpreter.cs:399,412,558 (comments only, no impl) |
| 2 | contact_allows_move level mismatch (MotionInterp, HIGH). acdream blocks Fallen/Dead/Crouch-range inside contact_allows_move itself; retail does this at DoMotion level with distinct error codes. (doc anchor: "Divergences ranked" #2, L1249-1253) |
Read contact_allows_move body: still explicitly checks fwd == MotionCommand.Fallen || fwd == MotionCommand.Dead and the crouch/sit/sleep range, returning false (blocked) directly from this function — exactly the divergence described. |
STILL-TRUE | src/AcDream.Core/Physics/MotionInterpreter.cs:1081-1107 (see lines 1092-1099 for the Fallen/Dead/Crouch block) |
| 3 | re_modify not implemented after cycle rebuild (CSequence, MEDIUM). Stacked Modifier motions (combat stance overlays) lose velocity/omega contribution on cycle switch. (doc anchor: "Divergences ranked" #3, L1255-1260) |
Grepped AnimationSequencer.cs for re_modify/ReModify/ModifierList/_activeModifiers — zero hits. |
STILL-TRUE | src/AcDream.Core/Physics/AnimationSequencer.cs (no match found for the term at all) |
| 4 | TransparentHook fires as instant snap vs smooth lerp (anim-hooks, MEDIUM). No FPHook time-interpolation equivalent. (doc anchor: "Divergences ranked" #4, L1262-1266) |
Not directly re-verified this session (out of scope for walk/run + stop-slide focus — this is a fade/translucency concern, not motion/position). | COULD-NOT-VERIFY (not checked; low relevance to task's (a)/(b)/(c) scope beyond "(c) inbound animation machinery generally") | n/a |
| 5 | direction_ default 0xFFFFFFFE vs AnimationHookDir.Both (anim-hooks, LOW). Dat-baked hooks with missing/zero direction may fire when retail would never fire them. (doc anchor: "Divergences ranked" #5, L1268-1272) |
Not re-verified this session — orthogonal to motion/position, low priority. | COULD-NOT-VERIFY | n/a |
| 6 | Legacy entity path has no hook dispatch (anim-hooks, LOW-MEDIUM), at old GameWindow.cs:8563-8573. (doc anchor: "Divergences ranked" #6, L1274-1277) |
Not re-verified — line numbers are 2026-06-04-vintage and GameWindow.cs has grown/shifted substantially (now 13,759 lines vs whatever it was then); the cited range is very likely stale. | STALE-ANCHOR (line numbers unverified/likely shifted; concept not re-checked) | n/a |
| 7 | enter_default_state not called on spawn (MotionInterp, LOW). No InitializeMotionTables/pending_motions Ready-node seeding at spawn. (doc anchor: "Divergences ranked" #7, L1279-1282) |
Grepped MotionInterpreter.cs for enter_default_state/EnterDefaultState/InitializeMotionTables — zero hits. |
STILL-TRUE | src/AcDream.Core/Physics/MotionInterpreter.cs (no match) |
| 8 | StandingLongJump path not called per-tick (MotionInterp, LOW). The standing-long-jump pre-jump pose sub-state exists as a flag but apply_interpreted_movement's per-tick branch (Ready + StopSideStep) is not wired. (doc anchor: "Divergences ranked" #8, L1284-1287) |
StandingLongJump field exists and is set/cleared in a few places (MotionInterpreter.cs:328,941,1119,1146) but no per-tick apply_interpreted_movement-style dispatch was found calling DoInterpretedMotion(Ready) + StopInterpretedMotion(SideStepRight) from the flag. Also confirmed D3 in doc 2 (MoveToState longjump bit) is now WIRED on the outbound wire side (standingLongjump param exists in MoveToState.Build), but that's the wire byte, not the per-tick animation branch this claim is about — those are different things. |
STILL-TRUE (the per-tick animation branch is still not called; note the wire-side standingLongjump param now exists as an input but nothing sets it to true from a live per-tick standing-longjump detection) |
src/AcDream.Core/Physics/MotionInterpreter.cs:328,941,1119,1146 (flag only, no per-tick apply path); cf. src/AcDream.Core.Net/Messages/MoveToState.cs:56-70 (wire param exists but is a pass-through, always false by default) |
| 9 (supporting) | Remote-entity path dispatches via direct SetCycle call from GameWindow.OnLiveMotionUpdated, NOT via move_to_interpreted_state (executive summary + divergence #5/#9 in the MotionInterp section, L303-317). |
Confirmed: move_to_interpreted_state/MoveToInterpretedState still does not exist anywhere except one comment reference. OnLiveMotionUpdated still directly manipulates ae.Sequencer (SetCycle) and does a raw bulk-copy into remoteMot.Motion.InterpretedState.ForwardCommand/ForwardSpeed — bypassing adjust_motion for remotes entirely. This is THE central mechanism relevant to the walk<->run-lag symptom: the D6-ported adjust_motion/apply_raw_movement/get_state_velocity triad is local-player-only (see doc-3 cross-check below). |
STILL-TRUE (confirmed, and directly load-bearing for the task's (a)/(c) focus) | src/AcDream.App/Rendering/GameWindow.cs:4517 (comment only ref to move_to_interpreted_state); GameWindow.cs:4590,4598 (direct bulk-copy into InterpretedState, bypassing adjust_motion); GameWindow.cs:4327-4492 (SetCycle-direct dispatch) |
| 10 (supporting) | acdream's remote velocity path bypasses apply_current_movement gating differences: acdream calls PhysicsObj.OnWalkable gate directly rather than the full retail contact/gravity flow (MotionInterp section divergence #3, L305). |
apply_current_movement (used by BOTH local and remote via rm.Motion.apply_current_movement calls) still gates on PhysicsObj.OnWalkable directly and calls get_state_velocity() — same shape as described. |
STILL-TRUE | src/AcDream.Core/Physics/MotionInterpreter.cs:890-910; called for remotes at GameWindow.cs:10042,10074 |
Doc 2: 2026-06-26-movement-animation-retail-parity-audit.md (D1-D12)
| # | Claim (doc anchor) | Current-code check | Status | Evidence (file:line) |
|---|---|---|---|---|
| D1 | MoveToState raw flags not retail — presence-based instead of default-difference packing; overs-sends default values. (doc L118-130) | MoveToState.cs doc header now explicitly says "default-difference flags dword + conditional fields + actions" and RawMotionState.Default exists with retail defaults (CurrentHoldKey=None, ForwardCommand=0x41000003, ForwardSpeed=1.0, etc. matching doc's retail defaults table L60-72). Shipped as part of L.2b (78e163a4, 2026-06-30). |
CLOSED-SINCE | src/AcDream.Core.Net/Messages/MoveToState.cs:22; src/AcDream.Core/Physics/RawMotionState.cs:44-58 |
| D2 | RawMotionState action-list and style packing incomplete — no full action-list packing, current-style scenarios. (doc L132-136) | RawMotionState now has Actions (IReadOnlyList) with a doc comment describing retail's num_actions bits 11-15 packing. Appears substantially addressed as part of L.2b, though full conformance vs the bitfield layout (11 one-bit flags + num_actions:5) was not independently re-verified byte-for-byte this session. |
CLOSED-SINCE (structure now present; exact bit-packing not re-verified) | src/AcDream.Core/Physics/RawMotionState.cs:57-63 |
| D3 | MoveToState longjump bit not modeled — only contact 0/1 passed, standing_longjump not wired as named state. (doc L138-147) |
MoveToState.Build now has a standingLongjump parameter and packs trailing = (byte)((standingLongjump ? 0x02 : 0) | (contact ? 0x01 : 0)) — matches doc's described retail byte layout. NOTE: the parameter is wired as an INPUT but this session did not verify any call site sets it to true from a live per-tick standing-longjump detection (see doc-1 item #8 above — the per-tick animation branch driving this flag is still missing). So the WIRE FORMAT is fixed; whether it's ever populated correctly is a separate, still-open question. |
CLOSED-SINCE (wire format only; upstream detection still absent) | src/AcDream.Core.Net/Messages/MoveToState.cs:56-70,100-101 |
| D4 | JumpAction packet layout retail-incompatible — missing full Position, extra objectGuid/spellId fields, wrong order. (doc L149-161) | JumpAction.cs doc header now cites Position::Pack (0x005a9640) and explicitly notes the fix: "the pre-slice code had it backwards (two spurious trailing zero u32s, no Position at all)" — confirms this was found and fixed as part of L.2b. |
CLOSED-SINCE | src/AcDream.Core.Net/Messages/JumpAction.cs:21-31 |
| D5 | Position heartbeat close but not fully proven — NotePositionSent stamps all three (time/position/contact-plane) on BOTH MTS and AP paths, but retail's SendMovementEvent (MTS) only stamps last_sent_position_time; only SendPositionEvent (AP) stamps all three. (doc L163-169) |
Confirmed STILL live and explicitly re-audited: a 2026-06-30 dated comment at the MTS send site says verbatim "this is a real, audit-confirmed divergence from retail... left UNCHANGED; reported to the lead engineer instead of fixed here." This is the most concrete, freshest confirmation in the whole audit — it was re-checked via Ghidra decompile-by-address and deliberately NOT fixed. | STILL-TRUE (explicitly re-confirmed and deliberately deferred, not a stale claim) | src/AcDream.App/Rendering/GameWindow.cs:8320-8339 |
| D6 | MotionInterpreter lacks canonical retail raw→interpreted normalization (adjust_motion, apply_run_to_command, apply_raw_movement not ported); SideStepLeft/WalkBackward produce zero velocity; jump lateral velocity hand-patched. (doc L171-182) |
This is exactly what Doc 3 (D6 pseudocode doc) documents as SHIPPED for the local player. Confirmed present: adjust_motion (MotionInterpreter.cs:741), apply_run_to_command (:805), apply_raw_movement (:848), get_state_velocity (:639) all exist and are called from PlayerMovementController.cs:909,974,1014. |
CLOSED-SINCE (for LOCAL player only — see D7 below, this does NOT extend to remotes) | src/AcDream.Core/Physics/MotionInterpreter.cs:639,741,805,848; src/AcDream.App/Input/PlayerMovementController.cs:880-1014 |
| D7 | Animation application split from retail motion flow — AnimationSequencer has useful pieces but MotionInterpreter.apply_current_movement is velocity-oriented and doesn't drive animation state through retail's order (style/forward/sidestep/turn/actions sequencing). (doc L184-188) |
THE central claim for the task's (a)/(c) focus. Confirmed still true and now MORE precisely characterized: for remotes, OnLiveMotionUpdated bulk-copies ForwardCommand/ForwardSpeed directly into InterpretedState (bypassing adjust_motion), AND separately calls ae.Sequencer.SetCycle(...) directly for the visual cycle — two parallel, only loosely-coupled paths (one drives position/velocity via apply_current_movement→get_state_velocity, the other drives the visual cycle via SetCycle). Neither goes through apply_interpreted_movement's full style/forward/sidestep/turn sequencing. Additionally there is a SEPARATE ApplyServerControlledVelocityCycle/ApplyPlayerLocomotionRefinement mechanism (UP-velocity-derived, with a 200ms UM grace window + 4.5/5.5 m/s hysteresis bucketing) that ALSO writes SetCycle for the local-player-observed-remotely case — this is a THIRD parallel path, not documented in either research doc, that is very likely a major contributor to "walk<->run reacts too slowly" (its grace window + hysteresis band means a walk<->run toggle needs velocity to cross a threshold via UpdatePosition samples before the visual cycle re-buckets, since a UM may not arrive for HoldKey-only toggles per the code's own comments). |
STILL-TRUE (confirmed and refined — root mechanism now identified precisely, more detailed than the original doc knew) | src/AcDream.App/Rendering/GameWindow.cs:4590,4598 (bulk-copy bypass); GameWindow.cs:5112-5176 (ApplyServerControlledVelocityCycle); GameWindow.cs:5178-5300+ (ApplyPlayerLocomotionRefinement, grace/hysteresis constants at :5095,5104,5110) |
| D8 | Force-walk and MoveTo are approximations — ServerControlledLocomotion, RemoteMoveToDriver, PlayerMovementController.BeginServerAutoWalk approximate steering/cycle selection vs retail's full MoveToManager queue (pre-turn, move, aux turn, final heading, sticky targeting, progress failure, MovementParameters::get_command). (doc L190-196) |
ServerControlledLocomotion.cs is still only 87 lines (a PlanMoveToStart/PlanFromVelocity helper, not a queued state machine). No pre-turn/aux-turn/sticky-target/progress-failure machinery found. |
STILL-TRUE | src/AcDream.Core/Physics/ServerControlledLocomotion.cs (87 lines total); src/AcDream.Core/Physics/RemoteMoveToDriver.cs (340 lines, still a simplified steering helper) |
| D9 | Inbound movement types 8 (TurnToObject) and 9 (TurnToHeading) dropped — UpdateMotion handles interpreted state and MoveTo 6/7 but not 8/9. (doc L198-207) |
Confirmed: UpdateMotion.TryParse only branches on movementType == 0 and movementType is 6 or 7; no 8/9 case anywhere. Grep for TurnToObject/TurnToHeading across UpdateMotion.cs and GameWindow.cs returns zero hits. The 2026-07-02 handoff doc explicitly reconfirms this live: "CONFIRMED live: mt=0x09 arrives and just SetCycle(Ready) instead of turning." |
STILL-TRUE (independently re-confirmed via grep + corroborated by a live capture cited in the handoff doc) | src/AcDream.Core.Net/Messages/UpdateMotion.cs:136-238 (no case 8/9) |
| D10 | Spawn-time movement state weaker than live — CreateObject detects movement types/flags but doesn't preserve full MoveTo target/origin/threshold data the way UpdateMotion does. (doc L209-213) |
Confirmed with exact detail: CreateObject.TryParseMoveToPayload (private, 3-out-param overload: movementParameters, speed, runRate) SKIPS Origin/distanceToObject/minDistance/failDistance/walkRunThreshold/desiredHeading via pos += advances with no output — so CreateObject's returned ServerMotionState.MoveToPath is always null. By contrast UpdateMotion.TryParseMoveToPayload (a DIFFERENT, more complete overload) DOES populate out CreateObject.MoveToPathData? path with Origin/thresholds. Two parsers of the same wire shape, one materially weaker. |
STILL-TRUE (confirmed with precise mechanism: two divergent parser overloads for the same MoveTo payload) | src/AcDream.Core.Net/Messages/CreateObject.cs:1194-1226 (weak 3-field overload, spawn path) vs src/AcDream.Core.Net/Messages/UpdateMotion.cs:255-331 (full path-populating overload, live-update path) |
| D11 | Sequence/autonomy data parsed then discarded — retail carries movement sequence, server-control sequence, autonomous state, motion flags, position sequence into movement application; acdream exposes only a subset. (doc L215-219) | Confirmed: UpdateMotion.TryParse still does a blind pos += 6 skip over movementSequence(u16) + serverControlSequence(u16) + isAutonomous(u8) + align — none of these three fields are captured into the Parsed record or ServerMotionState. (Note: WorldSession.cs DOES track _serverControlSequence/_instanceSequence/etc., but those are the OUTBOUND sequence counters sourced from UpdatePosition, not the INBOUND per-entity UpdateMotion fields this claim is about — different mechanism entirely, do not conflate.) _motionFlags (byte) is read into a local var and never used beyond that line either. |
STILL-TRUE (confirmed, and clarified that the WorldSession sequence counters some might assume closes this are unrelated) | src/AcDream.Core.Net/Messages/UpdateMotion.cs:89-106 (skip), :111 (_motionFlags read, unused past declaration) |
| D12 | Jump/falling/contact gates simplified — retail allows specific movement while falling/dead, has separate jump checks for posture/stamina/constraints/pending-motion/contact/leave-hit-ground reapplication; acdream blocks or simplifies several. (doc L221-225) | Not exhaustively re-verified line-by-line this session (large surface). Spot-checked contact_allows_move/motion_allows_jump-equivalent: the Fallen/Dead/Crouch block still lives inside contact_allows_move (see doc-1 item #2), which is itself evidence the gates remain simplified/misplaced relative to retail's DoMotion-level error codes. No pending_motions-driven jump eligibility check found (consistent with item #1). |
STILL-TRUE (partially re-verified via the contact_allows_move overlap with doc-1 #2; full jump/posture/stamina gate audit not redone) | src/AcDream.Core/Physics/MotionInterpreter.cs:1081-1107 (same evidence as doc-1 #2) |
Doc 3: 2026-07-01-d6-motion-interp-pseudocode.md — cross-check of its own claims
| Claim | Current-code check | Status |
|---|---|---|
"D6 ships adjust_motion/apply_run_to_command/apply_raw_movement/get_state_velocity in MotionInterpreter.cs, unifying LOCAL player velocity+turn+jump+wire onto one RawMotionState at forward_speed=1.0" |
Confirmed via git log (0f099bb6 D6.2a, d34721fa D6.2b) and via direct code read — all four functions exist and are wired into PlayerMovementController.cs only. |
STILL-TRUE (as scoped) |
| "Out of scope for D6: RawMotionState unification — retail uses ONE raw state for both the wire and the velocity pipeline; D6 keeps the L.2b wire construction separate... unifying is a later slice" (doc L153-156) | This scoping statement is itself accurate and still holds: the wire-side RawMotionState/MoveToState.Build path and the local velocity-side apply_raw_movement input are DIFFERENT RawMotionState construction sites (confirmed by design decision #1 in the doc itself, "ONE RawMotionState... is built from MovementInput" — but this unification was the D6.2 GOAL for local-only; there is still no unification with the REMOTE/inbound path at all). |
STILL-TRUE (scoping honored, and the doc's own "later slice" is exactly the gap this task's handoff doc targets) |
| "Out of scope for D6: turn ... acdream keeps direct-Yaw turn is replaced" / actually turn WAS ported per design decision #3 ("Turn ported to interpreted, feel unchanged... omega.Z = ±(π/2) × turn_speed... the fixed TurnRateFor direct-Yaw is replaced by the pipeline") — LOCAL only. | For remotes, RemoteMoveToDriver.TurnRateFor is still the mechanism (per doc's own "AP-9 stays" note) — confirmed no remote-side adjust_motion-sourced turn omega found; RemoteMoveToDriver.cs (340 lines) still exists with its own turn-rate logic, separate from MotionInterpreter. |
STILL-TRUE (remote turn is still the old direct-rate approximation; local turn was ported per D6 but that's a different code path) |
Synthesis: what this means for the walk<->run lag + stop-slide symptom (task focus a/b)
- D6 (the shipped port) does not touch the remote/inbound path at all. Every function it
ported (
adjust_motion,apply_run_to_command,apply_raw_movement,get_state_velocity) is called exclusively fromPlayerMovementController.cs(local player). Zero call sites fromGameWindow.cs's remote-entity code (OnLiveMotionUpdated,_remoteDeadReckon,ApplyServerControlledVelocityCycle) invoke these D6-ported functions. - The remote path bulk-copies wire commands directly into
InterpretedState(GameWindow.cs:4590,4598), bypassingadjust_motion— so backward/sidestep-left normalization and run-rate scaling never happen for remotes the retail-faithful way. This matches the retailcopy_movement_frombulk-copy semantics for THAT ONE STEP, but the surroundingpending_motions/queue/move_to_interpreted_statestate machine around it (doc-1 items #1, #9) is still entirely absent. - A newly-identified (not named in either doc) mechanism likely explains "reacts too
slowly":
ApplyPlayerLocomotionRefinement(GameWindow.cs:5178+) only kicks in after aUmGraceSeconds = 0.2(200ms) window since the last UM, and then applies hysteresis bands (PlayerRunPromoteSpeed = 5.5f,PlayerRunDemoteSpeed = 4.5f) before re-bucketing walk↔run from UpdatePosition-derived velocity — this is architecturally exactly the kind of mechanism that would produce a visible lag on a walk↔run toggle, especially combined with the code's own comment that retail may not broadcast a fresh MoveToState for HoldKey-only toggles (Shift while W held), forcing reliance on this slower velocity-inference path. - D9 (types 8/9 dropped) means any server-driven turn command mid-locomotion is silently
turned into a
SetCycle(Ready), which would visibly interrupt/desync locomotion — a plausible contributor to "compounding error... running around + turning." - D11 (sequence numbers discarded) means acdream has no way to detect/reject
out-of-order or duplicate
UpdateMotionpackets, which is a plausible contributor to the "position errors once the entity stops" (a late/duplicate packet could re-apply a stale state after a newer one, with no sequence check to catch it).
None of the above is a fix recommendation — this is a raw claim/verification map only, per the report-only investigation mode.