Imports the two 2026-06-26 movement/animation retail-parity research docs (audit D1-D12 + ACE-vs-2013 command-catalog gap) and adds the design spec for the first implementation slice: dual command catalog (AceModern runtime default + full-extraction Retail2013 conformance) + outbound wire parity (RawMotionState default-difference packing D1, contact|standingLongjump trailing byte D3, retail JumpPack layout D4). Decomp-verbatim, tests-first; D6 motion-interpreter input-state construction explicitly deferred. Spec: docs/superpowers/specs/2026-06-30-movement-wire-parity-design.md Phases: L.2b (movement wire/contact authority) + L.1b (command router). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
17 KiB
Movement and Animation Retail Parity Audit
Date: 2026-06-26
Verdict on approach
The approach is good if it stays decomp-first and test-first. For this area, "verbatim" needs to mean:
- Outbound wire bytes match retail packers for the same input state.
- Incoming movement packets are parsed into the same semantic state retail would unpack.
- Local, remote player, monster, NPC, and force-walk paths flow through retail-equivalent motion state transitions before animation selection.
- Any behavior that cannot be proven from retail decomp, Ghidra, or retail captures is marked unresolved instead of tuned by feel.
The approach is not good if it begins as a broad rewrite or animation polish pass. The current code has several comments and tests that already encode approximations. Those need to become failing parity tests first, then implementation slices.
Sources checked
Primary oracle:
docs/research/named-retail/acclient_2013_pseudo_c.txtdocs/research/named-retail/acclient.hdocs/research/named-retail/acclient.c
Secondary oracle:
docs/research/acclient_decompiled.c- Ghidra HTTP bridge verified on
http://127.0.0.1:8081;methods?limit=3responded and decompile-by-address worked during the audit. Codex tool discovery did not expose a first-class Ghidra MCP namespace in this thread, so direct HTTP is the usable path here.
Current implementation surfaces:
src/AcDream.Core.Net/Messages/MoveToState.cssrc/AcDream.Core.Net/Messages/AutonomousPosition.cssrc/AcDream.Core.Net/Messages/JumpAction.cssrc/AcDream.Core.Net/Messages/UpdateMotion.cssrc/AcDream.Core.Net/Messages/UpdatePosition.cssrc/AcDream.Core.Net/Messages/CreateObject.cssrc/AcDream.Core.Net/WorldSession.cssrc/AcDream.Core/Physics/MotionInterpreter.cssrc/AcDream.Core/Physics/AnimationSequencer.cssrc/AcDream.Core/Physics/ServerControlledLocomotion.cssrc/AcDream.Core/Physics/RemoteMoveToDriver.cssrc/AcDream.App/Input/PlayerMovementController.cssrc/AcDream.App/Rendering/GameWindow.cs
Retail source map
Outbound movement to ACE
CommandInterpreter::ShouldSendPositionEventat0x006b45e0, pseudo line around700233.CommandInterpreter::SendMovementEventat0x006b4680, pseudo line around700274.CommandInterpreter::SendPositionEventat0x006b4770, pseudo line around700316.CM_Movement::Event_AutonomousPositionat0x006af790, sub-opcode0xf753.CM_Movement::Event_Jumpat0x006afa70, sub-opcode0xf61b.CM_Movement::Event_MoveToStateat0x006afc00, sub-opcode0xf61c.MoveToStatePack::Packat0x005168f0, pseudo line around284694.AutonomousPositionPack::Packat0x00516af0, pseudo line around284795.JumpPack::Packat0x00516d10, pseudo line around284915.RawMotionState::Packat0x0051ed10, pseudo line around293761.
Retail RawMotionState defaults:
current_holdkey = HoldKey.Nonecurrent_style = 0x8000003dforward_command = 0x41000003forward_holdkey = HoldKey.Invalidforward_speed = 1.0sidestep_command = 0sidestep_holdkey = HoldKey.Invalidsidestep_speed = 1.0turn_command = 0turn_holdkey = HoldKey.Invalidturn_speed = 1.0
Retail RawMotionState::Pack compares against these defaults. It does not set a field bit merely because the caller supplied a value. For example, forward_speed == 1.0f is omitted.
Incoming movement and force movement
MovementManager::PerformMovementat0x005240d0, pseudo line around300194, routes movement types1-5toCMotionInterpand6-9toMoveToManager.MovementManager::unpack_movementat0x00524440, pseudo line around300563, unpacks interpreted motion plusMoveToObject,MoveToPosition,TurnToObject, andTurnToHeading.MovementParameters::UnPackNetat0x0052ac70, pseudo line around308118.MovementParameters::get_commandat0x0052aa00, pseudo line around307946.MoveToManager::_DoMotion,MoveToPosition, and related queue logic are around pseudo lines306351,307187, and307521.
Retail movement types from acclient.h:
0: interpreted motion in inbound packet context1: raw motion2: interpreted motion3: stop raw motion4: stop interpreted motion5: stop completely6: move to object7: move to position8: turn to object9: turn to heading
Animation and motion interpretation
CMotionInterp::apply_run_to_commandat0x00527be0, pseudo line around305062.CMotionInterp::get_state_velocityat0x00527d50.CMotionInterp::adjust_motionat0x00528010, pseudo line around305343.CMotionInterp::apply_raw_movementat0x005287e0.CMotionInterp::DoInterpretedMotionaround pseudo line305575.CMotionInterp::apply_current_movementaround pseudo line305713.CMotionTable::GetObjectSequencearound pseudo line298636.CSequenceappend/advance/update logic around pseudo lines301622,301777,301839, and302425.
Key retail normalization:
SideStepLeftbecomesSideStepRightwith negative speed.SideStepRightspeed becomes(3.11999989 / 1.25) * 0.5 * speed.TurnLeftbecomesTurnRightwith negative speed.WalkBackwardbecomesWalkForwardwith-0.649999976 * speed.- Hold-key
Runupgrades forward walk to run, multiplies turn speed by1.5, and multiplies sidestep by run rate with an absolute clamp of3.0.
Confirmed divergences
D1: MoveToState raw flags are not retail
Current MoveToState.Build sets raw-motion flags from nullable/presence inputs. Retail sets flags by comparing the complete RawMotionState against default values. This means the current client can over-send:
CurrentHoldKey = NoneForwardSpeed = 1.0SidestepSpeed = 1.0TurnSpeed = 1.0- likely per-axis hold keys when they are default
Invalid
Existing MoveToStateTests.Build_WalkForward_IncludesForwardCommandInFlags currently asserts the non-retail behavior by expecting a ForwardSpeed flag for speed 1.0.
Impact: outbound movement bytes can differ from retail for normal running, walking, sidestepping, and turning.
D2: RawMotionState action-list and style packing are incomplete
Retail bitfield layout uses 11 one-bit flags plus num_actions : 5 at bits 11-15. Current comments imply the command list length is broader than retail. Current builder does not cover full action-list packing or current-style scenarios.
Impact: action/modifier movement states cannot be proven retail-equivalent.
D3: MoveToState longjump bit is not modeled
Retail MoveToStatePack::Pack writes one trailing byte:
- bit
0x01: contact - bit
0x02:standing_longjump
Current GameWindow passes only contact 0/1 into a generic byte. The source of standing_longjump is not wired as a named state.
Impact: standing longjump movement-state updates can differ from retail.
D4: JumpAction packet layout is retail-incompatible
Retail JumpPack packs:
float extentVector3 velocity- full
Position - four
u16update timestamps - alignment
Current JumpAction.Build packs extent and velocity, then timestamps, then extra objectGuid and spellId, and does not pack Position.
Impact: local jump movement update is a high-confidence wire mismatch.
D5: Position heartbeat is close but not fully proven
Retail ShouldSendPositionEvent gates autonomous position updates by active state, autonomy level, player smartbox, 1.0 second interval, cell/frame change, and contact-plane change. Current PlayerMovementController has a similar cadence.
Open proof point: retail SendMovementEvent visibly stamps last_sent_position_time; retail SendPositionEvent stamps time, position, and contact plane. Current GameWindow calls NotePositionSent after both MTS and AP, stamping all three.
Impact: the client may suppress or reorder autonomous position sends differently after movement-state sends.
D6: MotionInterpreter lacks canonical retail raw-to-interpreted normalization
Retail CMotionInterp::apply_raw_movement copies raw state, calls adjust_motion for forward/sidestep/turn, applies run-hold behavior, then applies interpreted movement. Current MotionInterpreter.DoMotion stores commands directly and applies velocity directly.
Specific visible risks:
SideStepLeftmay produce no velocity because velocity code only recognizesSideStepRight.- backward and sidestep speeds are patched elsewhere instead of normalized at the retail source point.
- turn run-speed scaling is not faithfully represented.
- local jump lateral velocity relies on comments that say this is temporary until
adjust_motionis ported.
Impact: running, slow walking, slow side walking, backward movement, turning, and jump lateral motion are not retail-proven.
D7: Animation application is split away from retail motion flow
Retail movement application sequences style, forward or falling, sidestep start/stop, turn start/stop, and actions through CMotionInterp and CMotionTable::GetObjectSequence. Current AnimationSequencer implements useful pieces, but MotionInterpreter.apply_current_movement is mostly velocity-oriented and does not drive animation state through the same retail order.
Impact: animations can look plausible while being state-machine divergent.
D8: Force-walk and MoveTo are approximations
Retail MoveToManager is a queued command system over CMotionInterp, with pre-turn, move, aux turn, final heading, sticky targeting, progress failure, and MovementParameters::get_command walk/run/hold-key selection.
Current ServerControlledLocomotion, RemoteMoveToDriver, and PlayerMovementController.BeginServerAutoWalk approximate visible steering and cycle selection.
Impact: ACE force walking and monster/NPC MoveTo behavior are not retail-equivalent.
D9: Inbound movement types 8 and 9 are dropped
Retail inbound movement supports:
8:TurnToObject9:TurnToHeading
Current UpdateMotion handles interpreted state and MoveTo types 6/7, but not 8/9.
Impact: incoming server turn commands for characters, monsters, and NPCs are ignored.
D10: Spawn-time movement state is weaker than live movement state
CreateObject can detect movement types and some flags, but it does not preserve full MoveTo target/origin/threshold data the way live UpdateMotion does.
Impact: monsters/NPCs spawned while already moving cannot be driven retail-equivalently until a later movement packet arrives.
D11: Sequence/autonomy data is parsed then discarded
Retail carries movement sequence, server-control sequence, autonomous state, motion flags, and position sequence into movement application. Current events expose only a subset.
Impact: ordering, stale update rejection, and force-control transitions cannot match retail.
D12: Jump/falling/contact gates are simplified
Retail allows specific movement while falling/dead and has separate jump checks for posture, stamina, constraints, pending motion, contact, and leave/hit-ground reapplication. Current code blocks or simplifies several of these paths.
Impact: airborne movement, falling animation, dead/fallen movement commands, and jump eligibility are not retail-proven.
Priority plan
Phase 0: Lock the oracle
- Create small retail-reference helpers/tests that encode
RawMotionState::Pack,MoveToStatePack::Pack,AutonomousPositionPack::Pack,JumpPack::Pack,MovementParameters::UnPackNet, andMovementParameters::get_command. - Every helper must cite the decomp function and address in the test name or comments.
- Any unclear branch gets a TODO with address and blocked status, not an approximation.
Exit criteria: failing tests describe current divergences without changing runtime behavior yet.
Phase 1: Fix outbound wire parity
- Change
MoveToStatepacking from presence-based to retail default-difference packing. - Add explicit raw-state model with defaults, current style, action list count, and per-axis hold keys.
- Add explicit
contactandstandingLongjumpparameters for MTS trailing byte. - Replace
JumpActionwith retailJumpPack: extent, velocity, full position, four timestamps, align; remove extra object/spell fields unless another retail opcode proves they belong elsewhere. - Verify timestamp order: instance
[8], server-control[5], teleport[4], force-position[6]. - Audit whether MTS should stamp only last-send time or also last-send position/contact plane.
Exit criteria: golden byte tests pass for walk, run, sidestep, turn, slow walk/toggle-run, jump, contact, longjump, and AP heartbeat cases.
Phase 2: Port retail raw/interpreted motion core
- Introduce retail-shaped
RawMotionStateandInterpretedMotionStatein core physics. - Port
adjust_motion,apply_run_to_command,get_state_velocity, andapply_raw_movement. - Route local player input through this path instead of compensating in
PlayerMovementController. - Preserve run rate and hold-key semantics, including toggle-run slow-walk behavior.
Exit criteria: movement state tests prove backward, sidestep left/right, run turn, run sidestep clamp, forward run, and slow-walk semantics match retail math.
Phase 3: Reconnect animation to retail movement application
- Port enough of
CMotionInterp::apply_current_movementto sequence style, forward/falling, sidestep stop/start, turn stop/start, and actions in retail order. - Keep existing
AnimationSequencerwhere it already matches retail, but move normalization out of sequencer-only code and into motion interpretation. - Add parity tests for reverse playback, link fallback, modifier state, action sequencing, and root-motion composition using real retail motion table data.
Exit criteria: local and remote animation selection flows from the same interpreted state that drives velocity.
Phase 4: Port inbound movement and force-walk behavior
- Extend
UpdateMotionto parse and surface types8and9. - Preserve sequence/autonomy/motion flags through
WorldSessionevents. - Preserve full spawn-time MoveTo state in
CreateObject. - Replace approximate server-driven locomotion with a retail-shaped
MoveToManagerqueue overCMotionInterp. - Port
MovementParameters::get_command, including walk/run threshold,can_run, force-walk, hold-key application, fail distance, sticky/target following, and final heading.
Exit criteria: incoming interpreted motion, MoveToObject, MoveToPosition, TurnToObject, TurnToHeading, and ACE force-walk packets animate and move through retail-equivalent state transitions.
Phase 5: Integration captures and regression guard
- Capture retail-equivalent fixtures for normal run, slow walk, sidestep, slow sidestep, backward, turn, jump, forced walk, NPC MoveTo, monster chase/flee, and remote player interpolation.
- Assert wire bytes for outbound messages.
- Assert parsed semantic state for inbound messages.
- Assert animation command sequence and velocity for local and remote entities.
- Run full test suite plus a targeted in-app smoke pass.
Exit criteria: no movement or animation path is accepted without a retail-addressed test.
Initial test inventory to add or replace
MoveToStategolden tests: default raw state, walk forward speed1.0, run forward, backward, sidestep right/left, turn right/left, current style, action count, contact/longjump byte.JumpActiongolden tests: retailJumpPacklayout with full position and no object/spell fields.AutonomousPositiontests: timestamp order and contact byte.PlayerMovementControllertests: forward walk/run, slow walk/toggle-run, backward, sidestep, turn-only run, jump release, server auto-walk suppression.ShouldSendPositionEventtests: pre-interval cell change, pre-interval contact-plane change, interval frame change, idle no-send, airborne AP suppression.UpdateMotiontests: interpreted, MoveToObject, MoveToPosition, TurnToObject, TurnToHeading.CreateObjecttests: spawn-time MoveTo preservation.WorldSessiontests: sequence/autonomy propagation.MotionInterpretertests:adjust_motion,apply_run_to_command,get_state_velocity, contact gates, falling fallback, jump eligibility.AnimationSequencertests: retail sequence order, stop/start transitions, reverse playback, link fallback, modifier combine/subtract behavior.MoveToManagertests: pre-turn, move, aux turn, final heading, target following, fail distance, force-walk walk/run choice.
Implementation rule
No code path in this system should remain justified by "feels like retail", "ACE-compatible", or "close enough". If the decomp is unclear, use Ghidra or captures to resolve it. If it still cannot be resolved, leave the behavior behind a clearly named unresolved test or blocked TODO rather than guessing.