# D6 — CMotionInterp motion-normalization port: pseudocode + integration map Date: 2026-07-01 Phase: **L.1 / L.2b follow-up (D6)** — port retail's raw→interpreted motion normalization so local velocity for backward/strafe-left comes from the retail source instead of hand-mirrored controller code. Standard: **decomp-verbatim.** Retail decomp is the oracle; ACE `MotionInterp.cs` is the secondary oracle and was confirmed to match retail **byte-for-byte** on every constant and branch (understand-phase workflow, 2026-07-01). Oracle anchors (all in `docs/research/named-retail/acclient_2013_pseudo_c.txt`): - `CMotionInterp::adjust_motion` — `0x00528010`, lines 305343-305400 - `CMotionInterp::apply_run_to_command` — `0x00527be0`, lines 305062-305123 - `CMotionInterp::get_state_velocity` — `0x00527d50`, lines 305160-305204 - `CMotionInterp::apply_raw_movement` — `0x005287e0`, lines 305817-305834 Secondary oracle: `references/ACE/Source/ACE.Server/Physics/Animation/MotionInterp.cs` (constants L26-32; `adjust_motion` L394-428; `apply_run_to_command` L525-562; `get_state_velocity` L678-700; `get_leave_ground_velocity` L654-663; `apply_raw_movement` L506-523). ## Constants (retail = ACE, verified) | name | value | retail anchor | |---|---|---| | BackwardsFactor | `0.649999976` | `007c8910`; WalkBackwards speed `*= -BackwardsFactor` | | WalkAnimSpeed | `3.11999989` | `007c891c`; forward `v.Y = WalkAnimSpeed * fwdSpeed` | | RunAnimSpeed | `4.0` | RunForward `v.Y`; also `maxSpeed = RunAnimSpeed * rate` | | SidestepAnimSpeed | `1.25` | sidestep `v.X`; adjust_motion divisor | | SidestepFactor | `0.5` | adjust_motion sidestep scale | | RunTurnFactor | `1.5` | apply_run_to_command TurnRight | | MaxSidestepAnimRate | `3.0` | apply_run_to_command SideStepRight clamp | MotionCommand hex: `0x45000005`=WalkForward, `0x45000006`=WalkBackwards, `0x44000007`=RunForward, `0x6500000d`=TurnRight, `0x6500000e`=TurnLeft, `0x6500000f`=SideStepRight, `0x65000010`=SideStepLeft. ## The four functions (faithful pseudocode) ### apply_raw_movement (orchestrator) — 0x005287e0 ``` if physics_obj == null: return // copy 7 raw fields into interpreted interp.current_style = raw.current_style interp.forward_command = raw.forward_command ; interp.forward_speed = raw.forward_speed interp.sidestep_command = raw.sidestep_command; interp.sidestep_speed = raw.sidestep_speed interp.turn_command = raw.turn_command ; interp.turn_speed = raw.turn_speed // normalize EACH channel with ITS OWN per-channel hold key adjust_motion(ref interp.forward_command, ref interp.forward_speed, raw.forward_holdkey) adjust_motion(ref interp.sidestep_command, ref interp.sidestep_speed, raw.sidestep_holdkey) adjust_motion(ref interp.turn_command, ref interp.turn_speed, raw.turn_holdkey) apply_interpreted_movement(arg2, arg3) // acdream: apply velocity via get_state_velocity ``` ### adjust_motion(ref cmd, ref speed, holdKey) — 0x00528010 ``` if weenie != null and !weenie.IsCreature(): return // non-creature: no adjust switch cmd: RunForward: return // already normalized — NO holdkey path WalkBackwards: cmd = WalkForward; speed *= -BackwardsFactor // -0.649999976 TurnLeft: cmd = TurnRight; speed *= -1 SideStepLeft: cmd = SideStepRight; speed *= -1 // after remap, sidestep anim-rate scale: if cmd == SideStepRight: speed *= SidestepFactor * (WalkAnimSpeed / SidestepAnimSpeed) // (3.12/1.25)*0.5 ≈ 1.24799995 // hold-key (runs for everything EXCEPT the RunForward early-return): if holdKey == Invalid: holdKey = raw.current_holdkey if holdKey == Run: apply_run_to_command(ref cmd, ref speed) ``` GOTCHA: RunForward early-returns and does NOT run the hold-key path. The sidestep negate happens BEFORE the ×1.248 scale (so SideStepLeft = -1.248*speed). ### apply_run_to_command(ref cmd, ref speed) — 0x00527be0 ``` speedMod = 1.0 if weenie != null: speedMod = weenie.InqRunRate(out r) ? r : MyRunRate switch cmd: WalkForward: if speed > 0: cmd = RunForward // promote ONLY when moving forward (sign gate) speed *= speedMod // UNCONDITIONAL — applies to backward too (negative speed) TurnRight: speed *= RunTurnFactor // *1.5 SideStepRight: speed *= speedMod if abs(speed) > MaxSidestepAnimRate: // clamp to ±3.0 (ACE resolves the {test ah,0x5} polarity) speed = speed > 0 ? MaxSidestepAnimRate : -MaxSidestepAnimRate ``` GOTCHA: WalkForward `speed *= speedMod` is unconditional — so **backward DOES get run-scaled** (resolves the UN-5 "backward cites nothing" doubt in acdream's favor). The walk→run promotion is sign-gated: backward keeps a negative speed so it stays WalkForward (not promoted). ### get_state_velocity() -> local Vector3 — 0x00527d50 ``` v = (0,0,0) if interp.sidestep_command == SideStepRight: v.X = SidestepAnimSpeed * interp.sidestep_speed // 1.25*s if interp.forward_command == WalkForward: v.Y = WalkAnimSpeed * interp.forward_speed // 3.12*s else if interp.forward_command == RunForward: v.Y = RunAnimSpeed * interp.forward_speed // 4.0*s v.Z = 0 rate = MyRunRate; if weenie != null: weenie.InqRunRate(ref rate) maxSpeed = RunAnimSpeed * rate // 4.0 * rate if v.Length() > maxSpeed: v = normalize(v) * maxSpeed // PORT ACE'S FORM — decomp operand // names are wrong (x87 register aliasing) ``` GOTCHA: backward/strafe-left produce velocity here ONLY because adjust_motion already converted them to WalkForward/SideStepRight with negated speed. Z (jump) is added by the caller `get_leave_ground_velocity`, AFTER this clamp. ## acdream current state (what D6 replaces) Two divergent velocity paths (verified in source): - **PATH A** — `MotionInterpreter.get_state_velocity` (`MotionInterpreter.cs:599-648`) is faithful but only handles WalkForward/RunForward/SideStepRight; returns 0 for WalkBackward + SideStepLeft because `adjust_motion` is **not ported** (the InterpretedState holds the un-normalized command). - **PATH B** — the controller bypass (`PlayerMovementController.cs`): grounded velocity block builds body-local velocity by hand — forward `localY=stateVel.Y` (`:986`), backward `localY=-(WalkAnimSpeed*0.65*runMul)` (`:988`), strafe `localX=±SidestepAnimSpeed*runMul` (`:994/:996`); `runMul = InqRunRate if Run else 1.0` (`:981-983`). The **same formulas are re-copied in the jump block** (`:1072`, `:1076-1079`) because LeaveGround→get_state_velocity would zero backward/strafe. This duplication is register rows **TS-22** + **UN-5**. Integration seams: - Input enters at `PlayerMovementController.Update(dt, MovementInput)` (`:838`); section 2 (`:911-1000`) maps input → `_motion.DoMotion`/`DoInterpretedMotion` + the hand-built velocity. - Velocity is consumed by `_body.set_local_velocity` (`:998`, jump `:1090`) → physics integration (`:1096-1130`) → `ResolveWithTransition` (`:1136-1160`). - The **outbound wire** (section 6, `:1329-1399` → MovementResult → GameWindow `:8264-8388`) is a SEPARATE construction (the L.2b RawMotionState); it sends the RAW commands (WalkForward+HoldKey.Run, backward/strafe @1.0) and ACE/the observer runs adjust_motion. D6 must NOT change these wire bytes. ## Behavior changes D6 introduces (retail-faithful; flag for smoke test) 1. **Strafe ~20% faster.** Current `1.25×runRate`; retail `1.25 × 1.248 (adjust_motion sidestep scale) × runRate = 1.56×runRate`, then clamped via `SideStepSpeed ≤ 3.0` (so v.X ≤ 3.75). acdream is currently missing the 1.248 scale + the 3.0 clamp. 2. **Sidestep ±3.0 clamp becomes active** at high run rates. 3. **Backward unchanged** for typical run rates (already retail-faithful: `3.12×0.65×runRate`), now sourced from the real pipeline. 4. **Backward/strafe-left no longer zero** out of `get_state_velocity` (the TS-22 bug class is retired at the source, not hand-patched). ## Out of scope for D6 (follow-ups) - **Turn**: `get_state_velocity` does not produce turn velocity (turn is angular). acdream drives Yaw directly (`RemoteMoveToDriver.TurnRateFor`, ~90°/135° per sec). D6 keeps direct-Yaw turn; routing turn through interpreted angular velocity is a separate slice. `adjust_motion` on the turn channel is still ported (for completeness + the wire), but it does not change turn feel. - **RawMotionState unification**: retail uses ONE raw state for both the wire and the velocity pipeline. D6 keeps the L.2b wire construction separate and builds/normalizes a velocity-side raw state; unifying the two is a later slice. - `apply_current_movement` full sequence (style/forward/sidestep/turn/action ordering, `CMotionTable::GetObjectSequence`) — the understand-phase extractor for this one hit the structured-output cap; re-extract when the animation sequencing slice needs it. Not required for the velocity port. ## Design decisions (locked with the user 2026-07-01) 1. **Full unification.** ONE `AcDream.Core.Physics.RawMotionState` (the L.2b type) is built from `MovementInput` and drives BOTH the outbound wire packing AND the local velocity/turn pipeline (`apply_raw_movement` → `get_state_velocity` + turn omega). The separate GameWindow wire-construction is retired; `MovementResult` carries the raw state, GameWindow packs it. 2. **`forward_speed = 1.0` on run — CONFIRMED viable (echo-test 2026-07-01).** The echo-test settled it: acdream sends `forward_speed=1.0`, ACE broadcasts back `RunForward @ runRate` (not `1.0`), and a retail observer saw +Acdream run at full pace — so **ACE recomputes the broadcast run speed from run skill** (the `MovementData.cs`-citing "ACE relays" assumption was wrong). The wire now sends the retail-faithful raw `1.0` (D6.2b). Original reasoning below stands: The raw `forward_speed` must be `1.0` when running, because `apply_run_to_command` multiplies by run-rate — carrying `runRate` in the raw state would double-scale to `runRate²`. So the wire now sends `forward_speed=1.0` (omitted by default-difference) + `HoldKey.Run`, NOT the current `runRate`. This is the retail-faithful encoding (observer/server derives the run speed). **Acceptance = the smoke echo:** send `1.0`, confirm the `UM` echo still shows the player at `~runRate` (⇒ ACE recomputes from run skill ⇒ correct). If the echo shows `~1.0`, ACE relays literally and the wire half reverts. The L.2b golden tests update to the faithful encoding (forward_speed omitted on run). 3. **Turn ported to interpreted, feel unchanged.** Local turn is driven from the interpreted `turn_speed` (`adjust_motion`: TurnLeft→TurnRight `×-1`, `×1.5` run) via `omega.Z = ±(π/2) × turn_speed` — the SAME formula the remote path already uses (`GameWindow.cs:4845`). Numerically identical to today's `TurnRateFor` for the local player (both `π/2 × RunTurnFactor`), so no turn-feel change; the fixed `TurnRateFor` direct-Yaw is replaced by the pipeline. **AP-9 stays** (the `π/2` base rate is still an approximation of the per-creature TurnSpeed; wiring that is a separate follow-up). 4. **References caveat.** Only `references/WorldBuilder` is checked out in this worktree; ACE/holtburger are not. The retail decomp (`docs/research/named-retail/`) is the sole in-repo oracle and was verified directly for all four functions. ACE values quoted here are training-memory cross-checks that happen to match the decomp — do not cite ACE file:lines. **Retires / touches:** register **TS-22** deleted (adjust_motion ported; get_state_velocity no longer returns 0 for backward/strafe-left). **UN-5** resolved (run-scaling backward IS retail — `apply_run_to_command` unconditional `*speedMod`). **AP-9** stays (turn base rate). A wire-encoding change row is added if the echo test confirms `forward_speed=1.0`. **Implementation slices:** - **D6.1 (delegated, bounded):** port `adjust_motion` + `apply_run_to_command` into `MotionInterpreter` + conformance tests, tests-first, against this doc. - **D6.2 (lead, delicate):** `apply_raw_movement` orchestrator + build the unified `RawMotionState` in `PlayerMovementController` from `MovementInput`, route grounded + jump velocity through `get_state_velocity`, drive turn omega from interpreted `turn_speed`, thread the raw state through `MovementResult` to the GameWindow packer, delete the hand-mirrors (`:988/:994/:996` + jump `:1072/:1076-1079`) and the `TurnRateFor` direct-Yaw, update the L.2b golden tests. Then smoke (echo + strafe/backward/turn/jump feel). ## Conformance-test targets - `adjust_motion`: each of the 7 command transforms + the sidestep 1.248 scale + the backward -0.65 + the negate-before-scale order + the RunForward no-op + the non-creature early return + per-channel holdKey inheritance. - `apply_run_to_command`: WalkForward promote-when-speed>0 + unconditional `*speedMod`; TurnRight ×1.5; SideStepRight ×speedMod + ±3.0 clamp (test the boundary at runRate that pushes |speed| past 3.0). - `get_state_velocity`: backward + strafe-left now non-zero (the TS-22 fix); the maxSpeed normalize clamp; strafe = 1.56×runRate clamped to ≤3.75. - `apply_raw_movement`: 3-channel copy + per-channel adjust + the run-promotion of a running-forward raw state.