docs(D6): CMotionInterp motion-normalization pseudocode + unification design
Understand-phase output for D6: verbatim decomp + faithful pseudocode for adjust_motion (0x00528010), apply_run_to_command (0x00527be0), get_state_velocity (0x00527d50), apply_raw_movement (0x005287e0), all cross-checked against the retail decomp (ACE not checked out in this worktree; its constants match the decomp). Includes the acdream integration map (the hand-mirrored backward/strafe bypass D6 replaces), the retail-faithful behavior changes (strafe ~20% faster + 3.0 clamp), and the locked design decisions: full RawMotionState unification (wire+velocity+turn), forward_speed=1.0 on run verified via smoke echo, turn ported to interpreted omega (feel unchanged, AP-9 base rate stays). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f271a49e6a
commit
4ed278369b
1 changed files with 221 additions and 0 deletions
221
docs/research/2026-07-01-d6-motion-interp-pseudocode.md
Normal file
221
docs/research/2026-07-01-d6-motion-interp-pseudocode.md
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
# 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 (decomp-forced).** 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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue