Workflow-produced R3 research (3 docs, 3,061 lines):
- r3-motioninterp-decomp.md: verbatim pseudo-C + anchors for the full R3
scope — pending_motions lifecycle (add_to_queue 0x00527b80, MotionDone
0x00527ec0), DoMotion 0x00528d20 + PerformMovement 0x00528e80, the whole
jump family, HitGround 0x00528ac0 / LeaveGround 0x00528b00 (stale
0x00529710 doc-comment corrected), enter_default_state, MovementManager
relay surface, struct anchors, constants inventory. Negative results
(IsAnimating / HandleUpdateTarget / CMotionInterp::HandleEnterWorld NOT
in retail) explicitly recorded.
- r3-ace-motioninterp.md: ACE MotionInterp/MovementManager/MotionNode map
with flagged ACE-isms (jump_is_allowed L747 NPE typo, Falling-vs-Fallen
boundary discrepancy).
- r3-port-plan.md: 10 pinned ambiguities (headline: motion_allows_jump
0x48 polarity INVERTED in the BN annotation — ranges are a BLOCKLIST;
apply_current_movement dispatch gate IsThePlayer vs IsCreature), 19
itemized gaps J1-J19, keep-list, 7-commit sequence W0-W7 ending in the
local-player unification, exact IMotionDoneSink wiring spec vs R2 §4.
Precondition paragraph updated at vaulting: Q2 committed 98f58db9.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
377 lines
44 KiB
Markdown
377 lines
44 KiB
Markdown
# R3 port work-list — CMotionInterp completion + local-player unification
|
||
|
||
Inputs: `r3-motioninterp-decomp.md` (verbatim retail extraction, same dir),
|
||
`r3-ace-motioninterp.md` (ACE cross-ref, same dir), plan of record
|
||
`docs/plans/2026-07-02-retail-motion-animation-rewrite.md` (stage R3), R2 boundary
|
||
contract `docs/research/2026-07-02-r2-motiontable/r2-port-plan.md` §4, current code
|
||
`src/AcDream.Core/Physics/MotionInterpreter.cs` (1426 lines, post-D6/S2a),
|
||
`src/AcDream.App/Input/PlayerMovementController.cs` (1521 lines),
|
||
`src/AcDream.Core/Physics/InterpretedMotionFunnel.cs`, GameWindow K-fix18 sites.
|
||
|
||
**Precondition / state at R3 start (updated at vaulting, 2026-07-02):** R1 complete
|
||
(`a987cad1` P6). R2 is IN FLIGHT: Q0 (`dc54a3e4`) + Q1 MotionState (`2345da30`) +
|
||
Q2 `Motion/CMotionTable.cs` (`98f58db9`, 44 tests) committed;
|
||
Q3 (MotionTableManager + `IMotionDoneSink`) in flight, Q4 (adapter cutover +
|
||
queue-drain wiring) and Q5 (RemoteMotionSink deletion) NOT started.
|
||
`RemoteMotionSink.cs` still exists (217 lines). **R3 commits W2+ hard-depend on
|
||
R2 Q3/Q4/Q5 being shipped** (the `IMotionDoneSink` seam, the
|
||
sentinel→`AnimationDone` drain, and funnel-direct `PerformMovement` dispatch are
|
||
R2 deliverables R3 consumes). W0/W1 are independent and can start while R2
|
||
finishes.
|
||
|
||
---
|
||
|
||
## 0. DECOMP AMBIGUITIES TO PIN before porting (the W0 pseudocode commit)
|
||
|
||
| # | Ambiguity | Evidence each way | Pin method |
|
||
|---|---|---|---|
|
||
| A1 | **`motion_allows_jump` 0x48 polarity is INVERTED in the BN extraction's "cleaned" note.** r3-motioninterp-decomp §3a annotates `0x48` = "jump allowed" and the range set as a whitelist. But the CALLER `jump_is_allowed` (0x005282b0, §3h) does `eax_7 = motion_allows_jump(fwd); if (eax_7 != 0) return eax_7;` — nonzero is returned AS THE ERROR, so **0x48 = "YouCantJumpFromThisPosition"-class BLOCK and the ranges are the blocklist** (matches ACE MotionInterp.cs:770-779: five blocked ranges + one exact id, return None otherwise). Under the corrected polarity the retail blocklist is: `[0x1000006f,0x10000078]`, `[0x10000128,0x10000131]`, `0x40000008 (Fallen)`, `[0x40000016,0x40000018]`, `[0x4000001e,0x40000039]`, `[0x41000012,0x41000014] (Crouch..Sleeping)`; everything else (incl. **0x40000015 Falling → 0 = pass**) falls through. ACE instead blocks exactly `Falling` and never `Fallen` — one of the two mis-transcribed a constant. The same wrong "0x48 = allowed" annotation propagates into the decomp doc's §10 constants inventory. | decomp §3a/§3h vs ACE L770-779; acdream MotionCommand.cs: Fallen=0x40000008, Falling=0x40000015 | Re-read raw pseudo-C @305 line 304908 for the exact comparison chain; map every boundary constant through the dat MotionCommand table (does 0x40000015 really pass?); decisive cdb: bp `acclient!CMotionInterp::motion_allows_jump` log arg2+eax while user jumps during walk/run/mid-fall/crouch. **Port the pinned blocklist as literal uint ranges, NOT enum-ordinal ranges** (ACE's enum-order dependence is the flagged fragility). |
|
||
| A2 | **`jump_is_allowed` pending-head peek gate.** Retail (§3h full body): peek `pending_motions.head_` whenever non-empty; if `head.jump_error_code != 0` return it. ACE (L756): `PendingMotions.Count > 1 && First.JumpErrorCode != 0` — the `Count > 1` has no retail counterpart in this decomp. | decomp §3h (no count check) vs ACE L756 | Retail decomp is full-body and unambiguous → port retail (head peek whenever queue non-empty). Note in pseudocode why ACE differs (their head = currently-playing heuristic). ACE's L747 `WeenieObj == null && !WeenieObj.IsCreature()` NPE-typo is CONFIRMED wrong vs retail's `weenie != 0 && IsCreature()==0` gate — do not copy. |
|
||
| A3 | **`apply_current_movement` / `SetWeenieObject` / `SetPhysicsObject` dual-dispatch gate: `IsThePlayer` or `IsCreature`?** The decomp extraction (§4c) states ReportExhaustion gates on `IsThePlayer() && movement_is_autonomous()` and claims the identical pattern in apply_current_movement/SetWeenieObject/SetPhysicsObject. ACE apply_current_movement (L430-438) gates on `IsCreature`. These are NOT the same predicate: a remote player is a creature but not the player. Client-side, `IsThePlayer` is the coherent reading (raw_state is only meaningful for the locally-controlled object); ACE is a server where its own physics objects are all "the player". Getting this wrong sends remotes down `apply_raw_movement` and corrupts their interpreted state from an empty raw state. | decomp §4c text vs ACE L430/L497 | Re-read raw pseudo-C at 0x00528870 (line 305838, apply_current_movement), 0x00528920 (SetWeenieObject), 0x00528970 (SetPhysicsObject) — the vtable slot called distinguishes `IsThePlayer` from `IsCreature`. If still ambiguous, cdb bp with `this` dump on a remote-visible session. Default: **IsThePlayer-based for apply_current_movement/ReportExhaustion** (decomp's explicit ReportExhaustion body shows IsThePlayer; §4c says the others are identical). |
|
||
| A4 | **MovementParameters bit numbering.** DoMotion reads byte-1 bits: `&8` → SetHoldKey (=0x800 overall), `&0x20` → raw mirror (=0x2000); acclient.h's union comment says "bit0x8=SetHoldKey, bit0x20=Raw mirror, bit0x40=Interpreted mirror" (byte-relative). Sign bit = interrupt. Ctor default = `(bitfield & 0xfffdee0f) | 0x1ee0f`. | decomp §0/§2 vs acclient.h:31453 comment | Derive the absolute bit positions from the ctor default 0x1ee0f + ACE MovementParameters property names (CancelMoveTo/SetHoldKey/ModifyRawState/ModifyInterpretedState/DisableJumpDuringLink/…) which ACE mapped from the same bitfield. Document the full flag table in the pseudocode doc; the C# port uses named bool properties + a `ToBitfield()` only if the wire ever needs it. |
|
||
| A5 | **`get_jump_v_z` no-weenie fallback + near-zero path.** BN garbles the `extent < 0.0002` path (x87 flag noise) and reads the no-weenie return as "clamped extent"; ACE: `< EPSILON → 0.0f`, no-weenie → `10.0f`, InqJumpVelocity false → `0.0f`. Current acdream uses ACE's shape but with epsilon **0.001** (wrong — retail literal is `0.000199999995f`). | decomp §3c vs ACE L634-652 vs MotionInterpreter.cs:278 | Textual pin at 0x00527aa0 raw. Low stakes (creatures always have a weenie); fix the epsilon regardless. |
|
||
| A6 | **`get_leave_ground_velocity` zero-fallback matrix direction.** BN reads `m_fl2gv` (local→global) applied to the velocity; ACE does `GlobalToLocalVec(Velocity)`. Since the result feeds `set_local_velocity` (a LOCAL vector) and `m_velocityVector` is global, global→local is the only coherent reading — BN row/column confusion. Current `Quaternion.Inverse(Orientation)` transform (MotionInterpreter.cs:1024) IS global→local: keep. Epsilon must become 0.0002 (currently 0.001 via JumpExtentEpsilon). | decomp §3d vs ACE L654-663 vs current :1011-1029 | Adjudicate textually in pseudocode; conformance test: walk-off-ledge with nonzero world velocity preserves momentum direction. |
|
||
| A7 | **`MovementManager::MotionDone` uninit-`edx` relay + dead `arg2` in `CMotionInterp::MotionDone`.** Decompiler artifact (adjudicated in the decomp doc §6d). Port as `MotionDone(uint motion, bool success)` passing the completed node's motion id; the interp body reads NEITHER param in this build — **pop the HEAD unconditionally, never match by motion id**. Keep the params for R5 signature parity. | decomp §1c/§6d | Documented adjudication only. |
|
||
| A8 | **`enter_default_state` does NOT clear `pending_motions`.** Retail (§4d): resets raw+interp state, InitializeMotionTables, then APPENDS the `{0, 0x41000003, 0}` sentinel to whatever queue exists — no drain. ACE resets `PendingMotions = new LinkedList` with the author's own `// ??`. Port retail (append, no clear); note the MovementManager lazy-create path calls enter_default_state TWICE on first touch (retail double-call, §6g) — genuine retail, do not "fix", but R3's direct-bind construction calls it once explicitly. | decomp §4d/§6g vs ACE L604-616 | Documented adjudication; test asserts pre-existing nodes survive an enter_default_state. |
|
||
| A9 | **`StopCompletely` jump-snapshot quirk**: `motion_allows_jump` is evaluated on the OLD forward_command, then the state is overwritten and the OLD result is stashed in the queued Ready node. Port verbatim including the quirk (ACE L301-327 agrees). | decomp §5a + ACE | Textual pin only; golden asserts the queued node's error code reflects the pre-stop command. |
|
||
| A10 | **Error-code renumber vs current acdream `WeenieError`.** Retail numeric usage: `8`=no physics obj, `0x24`=not grounded (jump_is_allowed airborne), `0x3f/0x40/0x41`=combat-stance crouch/sit/sleep rejects, `0x42`=combat-stance chat-emote-bit reject, `0x45`=action depth ≥6, `0x47`=constrained OR stamina OR bad MovementStruct.type, `0x48`=motion/position blocks jump, `0x49`=CanJump refused. acdream's current enum (MotionInterpreter.cs:113-127) has the NAMES shuffled (0x24 named GeneralMovementFailure but used where retail uses 0x48-class; jump_is_allowed returns 0x48 for airborne where retail returns 0x24). | decomp §10 vs MotionInterpreter.cs:113-127, :1064 | W1 fixes the enum to retail's numeric semantics (add 0x3f/0x40/0x41/0x42/0x45); W3/W5 use them. Codes are local-only (never on the wire) — safe rename. |
|
||
|
||
**W0 cdb capture (one session serves all of R3):** bp `CMotionInterp::MotionDone` /
|
||
`add_to_queue` / `jump` / `jump_is_allowed` / `jump_charge_is_allowed` / `charge_jump`
|
||
/ `motion_allows_jump` / `HitGround` / `LeaveGround` / `StopCompletely` / `set_hold_run`
|
||
/ `SetHoldKey` / `enter_default_state` with arg+ret logging (pattern:
|
||
tools/cdb/l2g-observer.cdb). User protocol: idle / walk / run / shift-toggle mid-walk /
|
||
tap-jump / full-charge jump / running jump / standing long-jump (charge while idle,
|
||
then W mid-charge — the StandingLongJump suppression) / jump attempt while crouched
|
||
(blocked codes) / jump attempt in combat stance (0x3f-0x42 family) / walk off a ledge
|
||
(LeaveGround without jump — the momentum fallback) / land / emote-while-running
|
||
(action-class node through MotionDone) / 7 rapid emotes (0x45 depth cap) / logout
|
||
(HandleExitWorld drain).
|
||
|
||
---
|
||
|
||
## 1. ITEMIZED GAPS — current vs retail (R3 scope)
|
||
|
||
Severity: **BLOCKER** = R3's conformance harness meaningless without it; **HIGH** =
|
||
visible behavior wrongness / blocks R4-R6; **MED** = edge-visible; **LOW** = textual.
|
||
|
||
| # | Retail behavior acdream lacks/diverges on | Retail anchor | Current-code anchor | Severity |
|
||
|---|---|---|---|---|
|
||
| J1 | **No `pending_motions` queue** — no MotionNode, no `add_to_queue` (append `{context_id, motion, jump_error_code}`), no `MotionDone` (head action-class 0x10000000 → unstick + Interpreted+Raw `RemoveAction`, then unconditional head pop), no `motions_pending`, no `HandleExitWorld` drain loop. The R2 `IMotionDoneSink` signal dead-ends in a diagnostic recorder (per r2-port-plan Q4). | add_to_queue 0x00527b80 @305032; MotionDone 0x00527ec0 @305238; motions_pending 0x00527fe0; HandleExitWorld 0x00527f30; ACE MotionInterp.cs:210-234/388-392/160-173 | `MotionInterpreter.cs:1394` ("Idle add_to_queue(Ready) bookkeeping lands with S3") + `:1417` ("add_to_queue … S3 bookkeeping") — both TODO comments; no queue field exists | **BLOCKER** |
|
||
| J2 | **No action FIFO on the states** — retail `RawMotionState`/`InterpretedMotionState` each carry an actions list (`ApplyMotion`/`RemoveMotion`/`AddAction`/`RemoveAction`/`GetNumActions`); MotionDone's action-class pop and DoMotion's ≥6 depth cap operate on it. acdream's `InterpretedMotionState` (MotionInterpreter.cs:185-210) + `LegacyRawMotionState` (:148-179) are flat 6/7-field structs; the outbound `RawMotionState` packs an always-empty action list (register TS-24). | acclient.h RawMotionState/InterpretedMotionState; DoMotion @306159 GetNumActions≥6; MotionDone RemoveAction pair | `MotionInterpreter.cs:148-210`; `Core/Physics/RawMotionState.cs` (packer type, no actions) | **BLOCKER** |
|
||
| J3 | **DoMotion is a 12-line approximation.** Retail 0x00528d20: interrupt on sign bit; `SetHoldKey` on bit-SetHoldKey BEFORE `adjust_motion`; fresh local `MovementParameters` re-default (caller's distance/heading fields discarded for the interpreted call); combat-stance gate (`current_style != 0x8000003d` rejects 0x41000012/13/14 → 0x3f/0x40/0x41 and `motion & 0x2000000` → 0x42 — tested on the ORIGINAL id); action-depth cap → 0x45; delegate `DoInterpretedMotion(adjusted, localParams)`; mirror `RawState.ApplyMotion(ORIGINAL id)` on the mirror bit. Current: writes RawState.Forward\* then calls the legacy DoInterpretedMotion — no params, no gates, no mirror discipline. | 0x00528d20 @306159; ACE L112-158 | `MotionInterpreter.cs:448-462` | **BLOCKER** |
|
||
| J4 | **TWO parallel DoInterpretedMotion implementations, both partial.** The legacy overload (:473-491) hand-applies state + apply_current_movement; the S2a funnel `DispatchInterpretedMotion` (:1408-1425) has the verbatim contact gate + sink dispatch but no StandingLongJump special-case (Walk/Run/SideStepRight while charging → state-only, NO dispatch, NO queue), no `Dead → RemoveLinkAnimations`, no jump_error_code compute (double `motion_allows_jump` check: incoming motion, then ForwardCommand if non-action) + `add_to_queue`, no `ModifyInterpretedState` param, no `CurCell==null → RemoveLinkAnimations` tail. Same story for StopInterpretedMotion (:527-553 vs the funnel's bare `sink.StopMotion`): missing post-stop `add_to_queue(ctx, Ready, None)` + raw `RemoveMotion` mirror. They must MERGE into one verbatim pair. | DoInterpretedMotion 0x00528360 (S2a base) + queue callers @305607/305657/305775; ACE L51-110/329-365 | `MotionInterpreter.cs:473-491`, `:498-553`, `:1408-1425`, `:1358-1396` | **BLOCKER** |
|
||
| J5 | **Jump gate family absent**: no `motion_allows_jump` (0x005279e0 literal-range blocklist, A1), no `jump_charge_is_allowed` (0x00527a50: CanJump → 0x49; Fallen/Crouch..Sleeping → 0x48), no `charge_jump` (0x005281c0: arms `standing_longjump` only when on-ground + fwd==Ready + no sidestep/turn). `jump_is_allowed` (:1053-1071) is a 15-line approximation missing the creature/state-0x400 entry shape, IsFullyConstrained → 0x47, the pending-head jump_error_code peek (A2), the charge→motion→stamina chain, and `JumpStaminaCost` (not on IWeenieObject). Wrong airborne code (0x48, retail 0x24 — A10). | §3a/3b/3e/3h; ACE L729-779 | `MotionInterpreter.cs:1053-1071`; IWeenieObject :239-256 (no JumpStaminaCost/IsThePlayer) | **BLOCKER** |
|
||
| J6 | **StandingLongJump armed in the WRONG function** — the S2a-flagged misattribution: acdream arms it as a side effect of `contact_allows_move` (:1139-1148, explicitly marked "PRE-EXISTING acdream side effect (not part of 0x00528240)"); retail arms it ONLY in `charge_jump`. Consequence today: every grounded idle contact check flips the flag, so the funnel's StandingLongJump branch (:1371-1375) can fire without any jump charge. | charge_jump 0x005281c0 @305448 | `MotionInterpreter.cs:1139-1148` | **HIGH** |
|
||
| J7 | **jump() missing `interrupt_current_movement`** (retail always cancels the in-flight transition first; ACE `cancel_moveto`). And the CONTROLLER manually calls `LeaveGround()` + pre-captures `get_jump_v_z` at key-release (PlayerMovementController.cs:996-1028) — retail's LeaveGround fires from the physics layer detecting ground departure (`set_on_walkable(false)` → transition → MovementManager::LeaveGround). Walk-off-a-ledge never calls LeaveGround at all today (the get_leave_ground_velocity momentum fallback is dead code). | jump 0x00528780 @305792; LeaveGround 0x00528b00 (NOT the doc-comment's 0x00529710) | `MotionInterpreter.cs:943-958`; PlayerMovementController.cs:996-1028, :1226-1257 (transition block calls HitGround but never LeaveGround) | **HIGH** |
|
||
| J8 | **HitGround/LeaveGround bodies incomplete**: both missing the creature gate (`weenie==null OR IsCreature`) and `RemoveLinkAnimations`; LeaveGround missing the state-0x400 gate, `apply_current_movement(0,0)` re-sync, and the autonomous flag on `set_local_velocity(v, 1)`. **This is the retail mechanism K-fix18 fakes**: leaving ground strips pending link animations, so Falling engages instantly — no `skipTransitionLink` needed. | HitGround 0x00528ac0 @305996; LeaveGround 0x00528b00 @306022 | `MotionInterpreter.cs:1166-1198`; K-fix18 sites: AnimationSequencer.cs:305/313/390/401/433, GameWindow.cs:4817-4831, :10194-10224 | **HIGH** |
|
||
| J9 | **StopCompletely missing** interrupt, the A9 jump snapshot, `add_to_queue(0, Ready, jumpErr)`, and cell-null `RemoveLinkAnimations`; extra divergence: resets sidestep/turn SPEEDS to 1.0 (retail writes only fwd cmd/speed + zeroes sidestep/turn COMMANDS). `set_velocity(Zero)` stands in for `StopCompletely_Internal` (acceptable, keep + note). | 0x00527e40 @305208 | `MotionInterpreter.cs:577-602` | **HIGH** |
|
||
| J10 | **No `enter_default_state` / `initted`**: no state reset + InitializeMotionTables + Ready-sentinel enqueue (A8) + `initted=1` + unconditional LeaveGround; nothing gates `apply_current_movement`/`ReportExhaustion` on initted. | 0x00528c80 @306124 | constructors at `MotionInterpreter.cs:386-398` only | **BLOCKER** |
|
||
| J11 | **`apply_current_movement` is a direct-velocity approximation**, not the retail dual dispatch (`initted` gate → IsThePlayer/autonomous (A3) → `apply_raw_movement(cancelMoveTo, allowJump)` else `apply_interpreted_movement(...)`). The grounded `set_local_velocity(get_state_velocity())` write is an acdream adaptation of root-motion-driven velocity — survives R3 (register row), retires in R6. Also: no `(cancelMoveTo, allowJump)` param plumbing anywhere — `DisableJumpDuringLink = !allowJump` feeds the jump_error_code path (ACE L538/L232). | 0x00528870 @305838 (A3 pin); ACE L430-438 | `MotionInterpreter.cs:905-925`; funnel `ApplyInterpretedMovement` :1358 (no params) | **HIGH** |
|
||
| J12 | **No `ReportExhaustion`** (initted gate + IsThePlayer/autonomous dual dispatch, both `(0,0)` args); no `movement_is_autonomous` flag on PhysicsBody; no `IsThePlayer` on IWeenieObject. Retail relay chain: MovementManager::ReportExhaustion → CMotionInterp (+ MoveToManager, R4). The dead ACE `NoticeHandler.RecvNotice_PrevSpellSelection` breadcrumb is NOT R3 scope (spell-UI notice) — file as a note, don't port. | 0x005288d0 @305861 | nothing | MED-HIGH (needed for stamina-exhaustion demotes; wire caller lands with server stamina events) |
|
||
| J13 | **No `SetHoldKey`/`set_hold_run`** — retail's Shift edge handling (XOR toggle guard, None-only-meaningful-from-Run, both re-apply movement). acdream fakes it by rebuilding the whole RawMotionState every frame (J15). `HoldKey_None=1 / HoldKey_Run=2` encoding already matches acdream's HoldKey enum usage. | set_hold_run 0x00528b70 @306053; SetHoldKey 0x00528bb0 @306072 | `MotionInterpreter.cs:340` (bare CurrentHoldKey field, written by apply_raw_movement only) | **HIGH** |
|
||
| J14 | **No `CMotionInterp::PerformMovement` 5-way dispatch** with `CheckForCompletedMotions` after EVERY op (the synchronous zero-tick drain — retail calls it at 306234-306262 after each of DoMotion/DoInterpretedMotion/StopMotion/StopInterpretedMotion/StopCompletely). Current PerformMovement (:416-430) dispatches to the approximations and has a comment explicitly skipping the flush. Needs a seam to the entity's MotionTableManager (R2-Q3). | 0x00528e80 @306221; CPhysicsObj::CheckForCompletedMotions 0x0050fe30 | `MotionInterpreter.cs:400-430` | **BLOCKER** |
|
||
| J15 | **Local player motion state machine lives in PlayerMovementController, level-triggered.** Per-frame RawMotionState rebuild from held keys (:887-910) instead of retail's edge-driven DoMotion/StopMotion + set_hold_run through PerformMovement; jump charge/fire block calls `_motion.jump` + manual LeaveGround (:986-1028); outbound wire commands re-derived from `input` (:1264-1328) instead of read from the raw state; `LocalAnimationCommand`/`LocalAnimationSpeed` synthesized (:1284-1296, :1418-1456) instead of the interpreted state driving the sequencer through the same funnel path remotes use. This is the plan-of-record's "motion half of PlayerMovementController" — R3's unification target. | DoMotion/StopMotion callers = CommandInterpreter edges; set_hold_run call sites §5c (0x0058b303 / 0x006b33ca) | PlayerMovementController.cs:887-910, :986-1028, :1264-1359, :1418-1456; GameWindow.cs:10062-10234 (UpdatePlayerAnimation fallback chain + SetCycle drive) | **BLOCKER** for the stage's named deliverable |
|
||
| J16 | **Epsilons + codes**: `JumpExtentEpsilon = 0.001` used in get_jump_v_z AND get_leave_ground_velocity — retail is `0.000199999995f` in both (§10); WeenieError names shuffled vs retail numerics (A10). | §3c/§3d/§10 | `MotionInterpreter.cs:278`, :978, :1020; :113-127 | MED |
|
||
| J17 | **`is_standing_still` absent** (on-ground + Ready + no sidestep/turn) — trivial; needed by callers (input layer, R7 cadence) and by the decomp's documented substitute set for the not-in-retail `IsAnimating`/`IsMovingOrAnimating`. **Do NOT invent a combined IsAnimating helper** — retail has none (decomp §7f negative result); ACE's `PhysicsObj.IsAnimating` flag is an ACE-ism (retail queries `motions_pending`). | 0x00527fa0 @305309 | nothing | LOW |
|
||
| J18 | **`adjust_motion` creature guard unwired** (register TS-34): `IWeenieObject.IsCreature()` exists (default true) but adjust_motion ignores it (comment ":758 Creature guard unwired"). One-line fix; retires TS-34. | adjust_motion @305343 gate | `MotionInterpreter.cs:756-758` | LOW |
|
||
| J19 | **VectorUpdate remote-jump path bypasses the interp** (GameWindow.cs:4782-4840): sets body velocity/airborne + forces the Falling cycle via `SetCycle(..., skipTransitionLink:true)` (K-fix18 site 1). Retail: the remote's physics leaves ground → LeaveGround → RemoveLinkAnimations + apply_current_movement → contact-blocked funnel dispatches Falling. After J8, this becomes `rm.Motion.LeaveGround()` (+ the funnel's existing Falling dispatch). | SmartBox::DoVectorUpdate 0x004521C0 → set_velocity/set_omega; LeaveGround chain | GameWindow.cs:4802-4833 | **HIGH** |
|
||
|
||
---
|
||
|
||
## 2. KEEP LIST — already matching retail (do not re-port)
|
||
|
||
| Behavior | Retail anchor | acdream anchor |
|
||
|---|---|---|
|
||
| Inbound funnel dispatch order: `MoveToInterpretedState` (flat copy + 15-bit stamp gate + local-echo skip) / `ApplyInterpretedMovement` axis order (style → fwd-or-Falling → sidestep(-stop) → turn(-stop, early return)) + 183-case live-trace suite | 0x005289c0 / 0x00528600 (S2a, `7b0cbbda`) | `MotionInterpreter.cs:1312-1396` — W5 merges the Dispatch backend into verbatim DoInterpretedMotion WITHOUT touching dispatch order; suite is the parity bar |
|
||
| `contact_allows_move` verbatim body (turns/Falling/Dead always-allowed, non-creature bypass, gravity bypass, Contact+OnWalkable) | 0x00528240 (S2a rewrite) | `MotionInterpreter.cs:1096-1151` — MINUS the :1139-1148 StandingLongJump side effect (J6 deletes it) |
|
||
| `adjust_motion` / `apply_run_to_command` / `apply_raw_movement` / `get_state_velocity` incl. the RunForward-early-return, sign-gated promotion, ±3.0 sidestep clamp, max-speed clamp | D6/D6.2a (`0f099bb6`) | `MotionInterpreter.cs:654-888` — untouched except J18's one-line guard |
|
||
| `GetMaxSpeed` ×4.0 (UN-2 byte-verified vs the BN x87 dropout) | 0x00527cb0 | `MotionInterpreter.cs:1235-1245` |
|
||
| `GetCycleVelocity` Option-B forward-velocity source (MotionData.Velocity over the constant) | adaptation, r03 §1.3 | `MotionInterpreter.cs:382`, :668-683 — keep; verify register row exists, retire when R6 root motion drives the body |
|
||
| `StopCompletely_Internal` stand-in = `PhysicsObj.set_velocity(Zero)` | 0x0050f5a0 is CPhysicsObj-side | `MotionInterpreter.cs:599` — keep + register note (full port is physics-layer scope) |
|
||
| Jump charge accumulation UI timing (2.0 extent/s, AP-24) + extent→height formula | GetPowerBarLevel 0x0056ADE0 illegible | PlayerMovementController.cs:167-178 — charge STAYS controller-side (retail's charge lives at the SmartBox/input boundary too, §3e caller 0x0056afac); AP-24 row survives |
|
||
| Physics tick gate (30 Hz MinQuantum), Resolve/bounce/landing detection, AP diff cadence, PortalSpace, SetPosition/SnapToCell, NotePositionSent | L.5/L.2c/B.6 anchors in-file | PlayerMovementController.cs sections 4-5, 8 — R3 does not touch; R6 owns tick order |
|
||
| Auto-walk (B.6) block incl. `DoMotion(Ready)`/`DoMotion(WalkForward)` drive | MovementManager case 6 stand-in | PlayerMovementController.cs:254-766 — survives R3 verbatim (calls route through the new DoMotion transparently); REPLACED in R4 by MoveToManager |
|
||
| Outbound wire packers (RawMotionState::Pack, MoveToStatePack, JumpPack) + dual command catalogs | L.2b/D6.2b/L.1b | untouched; W6 changes only WHERE the packed values are read from (raw state instead of re-derivation) |
|
||
| `MotionSequenceGate` (S1), `InterpolationManager` (L.3), R1 CSequence core, R2 CMotionTable/MotionState/MotionTableManager | plan "absorbed" + R2 | untouched — R3 sits between MovementManager(-to-be) and the R2 queue |
|
||
| `0x41000003` Ready sentinel / `0x10000000` action-class bit / `0x8000003d` NonCombat conventions | §10 | already the file-wide constants |
|
||
|
||
---
|
||
|
||
## 3. COMMIT SEQUENCE — dependency-sorted, each ONE commit, tests-first
|
||
|
||
New code target: extend `src/AcDream.Core/Physics/MotionInterpreter.cs` in place (it IS
|
||
the CMotionInterp port; plan rule 4's `Motion/` dir holds the R2 table/queue classes it
|
||
talks to). Tests: `tests/AcDream.Core.Tests/Physics/` (existing MotionInterpreter
|
||
suites + `Motion/` for new fixtures). Every commit: build+test green, register rows
|
||
added/retired in-commit.
|
||
|
||
**W0 — pseudocode + ambiguity pinning (docs only).**
|
||
`docs/research/2026-07-0x-motioninterp-pseudocode.md` from r3-motioninterp-decomp.md,
|
||
resolving A1-A10 (§0) — the A1 polarity fix and A3 dispatch-gate pin are load-bearing;
|
||
correct the decomp doc's §3a/§10 annotations in the same commit. Run the ONE cdb
|
||
session (§0 protocol) — feeds W2/W3/W5/W6 goldens.
|
||
Fixture source: **cdb** (live retail). Deps: none (parallel with R2 completion).
|
||
|
||
**W1 — retail state completion: action FIFO + MovementParameters + WeenieError renumber.** (closes J2, J16-codes/A10)
|
||
`InterpretedMotionState`/`RawMotionState` gain the actions list
|
||
(`AddAction`/`RemoveAction`/`GetNumActions`/`ApplyMotion`/`RemoveMotion` per retail
|
||
member set; the outbound packer's empty-list TS-24 comment updates to "populated by
|
||
CMotionInterp"); fold `LegacyRawMotionState` into the full-field
|
||
`AcDream.Core.Physics.RawMotionState` (one raw-state type; delete the legacy struct);
|
||
`MovementParameters` verbatim class (ctor defaults: distance_to_object=0.6,
|
||
fail_distance=FLT_MAX, speed=1, walk_run_threshhold=15, hold_key=Invalid, bitfield
|
||
0x1ee0f per A4 pin; named bool properties); `MotionNode {ContextId, Motion,
|
||
JumpErrorCode}`; WeenieError renumbered to retail (add 0x3f/40/41/42/45, fix
|
||
0x24/0x47/0x48 semantics).
|
||
Tests first: FIFO discipline; params defaults vs ctor decomp; existing suites green
|
||
under the raw-state fold.
|
||
Fixture source: **synthetic**. Deps: W0.
|
||
|
||
**W2 — pending_motions lifecycle + the MotionDone consumer (R2 seam lands).** (closes J1, J17; §4 below is this commit's wiring spec)
|
||
`MotionInterpreter` gains `PendingMotions` (LinkedList<MotionNode> — register note
|
||
reuses AD-34 wording), `add_to_queue` 0x00527b80, `motions_pending` 0x00527fe0,
|
||
`MotionDone(uint motion, bool success)` 0x00527ec0 (head-peek action-class → unstick
|
||
seam (no-op Action? callback until R5 StickyManager — register row) +
|
||
`InterpretedState.RemoveAction()` + `RawState.RemoveAction()`; unconditional head pop;
|
||
NEVER match by motion id per A7), `HandleExitWorld` 0x00527f30 (loop),
|
||
`is_standing_still` 0x00527fa0, and `motion_allows_jump` 0x005279e0 (pure fn, pinned
|
||
per A1 — needed here because every add_to_queue caller computes jump_error_code).
|
||
Wire the queue's PRODUCERS into the funnel: `DispatchInterpretedMotion` success path
|
||
computes jump_error_code (double-check per ACE L231-239 shape pinned at W0) +
|
||
`add_to_queue(ctx, motion, err)`; `ApplyInterpretedMovement`'s turn-stop adds
|
||
`add_to_queue(ctx, Ready, None)` (@305775); funnel StopMotion path adds the post-stop
|
||
Ready node (@305657). GameWindow: replace R2-Q4's diagnostic `IMotionDoneSink` binding
|
||
with the real per-entity bind (§4); teleport/despawn/exit paths call BOTH
|
||
`manager.HandleExitWorld()` (R2) and `interp.HandleExitWorld()` (retail has both
|
||
layers' drains).
|
||
Tests first: queue tables (enqueue per successful dispatch incl. Ready stops; pop
|
||
order; action-class pops from BOTH states + fires unstick; exit drain); S2a 183-case
|
||
suite green (dispatch unchanged, queue side effects asserted additively); end-to-end
|
||
chain test: MotionTableManager.AnimationDone → sink → interp pops (§4 diagram).
|
||
Fixture source: **synthetic + W0 cdb goldens** (add_to_queue arg conformance).
|
||
Deps: W1 + **R2 Q3/Q4 shipped**.
|
||
|
||
**W3 — jump family verbatim.** (closes J5, J6, J7-interp-side, J16-epsilons)
|
||
`jump_charge_is_allowed` 0x00527a50; `charge_jump` 0x005281c0 (arms StandingLongJump;
|
||
**DELETE the contact_allows_move side effect :1139-1148** — J6); `get_jump_v_z`
|
||
(epsilon → 0.000199999995f; A5 fallback pinned); `get_leave_ground_velocity` (same
|
||
epsilon; A6 direction confirmed); `jump_is_allowed` 0x005282b0 full chain (creature/
|
||
state-0x400 entry shape → 0x24; `IsFullyConstrained` seam on PhysicsBody (stub false +
|
||
register row); pending-head peek per A2; charge → motion_allows_jump(fwd) →
|
||
`JumpStaminaCost` (new IWeenieObject member; PlayerWeenie implements — real gating
|
||
stays TS-5-deferred, row survives)); `jump` 0x00528780 (+`interrupt_current_movement`
|
||
seam — no-op Action? until R4 cancel_moveto exists; register row). IWeenieObject gains
|
||
`IsThePlayer()` (default false; PlayerWeenie true) for W4's A3 dispatch.
|
||
Tests first: per-error-code gate tables from W0 goldens; StandingLongJump arming
|
||
matrix (grounded+idle only, cleared on failed jump); head-peek short-circuit;
|
||
blocked-motion jump attempts (crouch range per pinned A1 table).
|
||
Fixture source: **W0 cdb goldens + synthetic**. Deps: W2.
|
||
|
||
**W4 — ground transitions + lifecycle: HitGround/LeaveGround/enter_default_state/ReportExhaustion/hold keys; K-fix18 DELETED.** (closes J8, J10, J11-shape, J12, J13, J19; J18 one-liner rides along)
|
||
`HitGround` 0x00528ac0 verbatim (creature gate + Gravity + `RemoveLinkAnimations` seam
|
||
+ `apply_current_movement(false, true)`); `LeaveGround` 0x00528b00 verbatim (velocity
|
||
+ autonomous flag arg on set_local_velocity + resets + RemoveLinkAnimations +
|
||
apply_current_movement); `RemoveLinkAnimations` seam = `Action?` on MotionInterpreter,
|
||
App binds to the entity's AnimationSequencer (CSequence.RemoveAllLinkAnimations per
|
||
CPhysicsObj::RemoveLinkAnimations 0x0050fe20 → CPartArray → sequence);
|
||
`enter_default_state` 0x00528c80 (A8: append sentinel, no clear; `Initted` field;
|
||
LeaveGround tail); `Initted` gates apply_current_movement + ReportExhaustion;
|
||
`apply_current_movement` becomes the verbatim dual dispatch per A3 (the direct
|
||
grounded-velocity write MOVES to the controller-side call site unchanged — register
|
||
row: velocity-from-state adaptation until R6 root motion); `ReportExhaustion`
|
||
0x005288d0 (A3 gate; `movement_is_autonomous` flag added to PhysicsBody, set true at
|
||
the local-player chokepoint, false on DR-applied updates); `SetHoldKey` 0x00528bb0 +
|
||
`set_hold_run` 0x00528b70; `SetPhysicsObject`/`SetWeenieObject` re-apply pattern.
|
||
**PlayerMovementController transition wiring:** grounded→airborne edge (section 5,
|
||
:1245-1257) now calls `_motion.LeaveGround()`; the jump-fire block stops calling
|
||
LeaveGround manually — `jump()` sets on_walkable(false) and the SAME frame's
|
||
transition detection fires LeaveGround (ordering test); JumpAction wire velocity read
|
||
from the LeaveGround-computed vector (get_leave_ground_velocity at fire time —
|
||
byte-identical values, source unified).
|
||
**K-fix18 DELETION (both sites + the parameter):** GameWindow :4817-4833 remote
|
||
VectorUpdate → `rm.Motion.LeaveGround()` replaces the forced SetCycle (J19); GameWindow
|
||
:10194-10224 skipLink computation + arg deleted; `AnimationSequencer.SetCycle`'s
|
||
`skipTransitionLink` parameter + its post-dispatch link-clear (whatever shape R2-Q4
|
||
left it in) deleted; K-fix18 register row (r2-port-plan keep-list says "register row
|
||
kept → R3") retired — if no row exists, the sweep note records it was comment-only.
|
||
Tests first: HitGround/LeaveGround call RemoveLinkAnimations + re-apply (seam mock);
|
||
ledge walk-off momentum fallback (A6); jump-fire → LeaveGround same-tick ordering;
|
||
enter_default_state seeds queue + initted + LeaveGround; ReportExhaustion dispatch
|
||
truth table (A3 pin); hold-key toggle tables (set_hold_run XOR guard; SetHoldKey
|
||
None-only-from-Run). Visual acceptance carried to W6's pass: jump engages Falling
|
||
instantly WITHOUT K-fix18.
|
||
Fixture source: **synthetic + W0 cdb goldens (LeaveGround/HitGround arg traces)**.
|
||
Deps: W3 (+ R2-Q4 committed for the sequencer seam shape).
|
||
|
||
**W5 — DoMotion/StopMotion/StopCompletely verbatim + the ONE DoInterpretedMotion + PerformMovement flush.** (closes J3, J4, J9, J14)
|
||
`DoMotion` 0x00528d20 (interrupt bit; SetHoldKey bit BEFORE adjust_motion; params
|
||
re-default; combat-stance gate on ORIGINAL id → 0x3f/0x40/0x41/0x42; depth cap 0x45;
|
||
raw mirror ORIGINAL id); `StopMotion` 0x00528530 (mirror shape, no gates, raw
|
||
RemoveMotion ORIGINAL id); `StopCompletely` 0x00527e40 (A9 verbatim incl. quirk +
|
||
exact-fields-only reset + add_to_queue + cell-null RemoveLinkAnimations); **merge**
|
||
legacy `DoInterpretedMotion`/`StopInterpretedMotion` overloads with the funnel
|
||
Dispatch backend into ONE verbatim `DoInterpretedMotion(uint, MovementParameters)` /
|
||
`StopInterpretedMotion(uint, MovementParameters)` (StandingLongJump state-only branch;
|
||
Dead → RemoveLinkAnimations; jump_error_code + add_to_queue (moves here from W2's
|
||
interim funnel site); ModifyInterpretedState; CurCell-null tail — CurCell proxy =
|
||
`PhysicsObj.CellId == 0`); **DELETE** `ApplyMotionToInterpretedState` (:1253-1282) +
|
||
the legacy overloads; `PerformMovement` gains the per-op
|
||
`CheckForCompletedMotions` seam call (bound to the entity's MotionTableManager
|
||
UseTime/CheckForCompletedMotions — R2-Q3 object) after every dispatch.
|
||
Tests first: DoMotion gate tables (each combat-stance code, depth cap, mirror
|
||
discipline: raw gets ORIGINAL id, interpreted gets adjusted) from W0 goldens; S2a
|
||
183-case suite green under the merged backend (assertions unchanged — dispatch order
|
||
is funnel-owned); zero-tick flush test (stop-with-no-link completes same call).
|
||
Fixture source: **W0 cdb goldens + suite**. Deps: W2+W4 (+ R2-Q5 so the funnel's sink
|
||
IS PerformMovement dispatch — otherwise the merge has two consumers).
|
||
|
||
**W6 — LOCAL PLAYER UNIFICATION (the GameWindow + controller commit; do NOT fan out — feedback_dont_parallelize_coupled_plan_slices).** (closes J15)
|
||
PlayerMovementController sheds the motion state machine; edge-driven retail input:
|
||
- **DIES in the controller:** per-frame RawMotionState rebuild + `apply_raw_movement`
|
||
call (:878-910); the manual LeaveGround + get_jump_v_z pre-capture (done in W4);
|
||
`localAnimCmd`/`LocalAnimationSpeed` synthesis (:1283-1296, :1340-1359 change
|
||
detection on localAnimCmd, :1412-1456 K-fix5 block + auto-walk anim overrides —
|
||
the sequencer is now driven by the interp's own dispatch, not by
|
||
UpdatePlayerAnimation); `MovementResult.LocalAnimationCommand/LocalAnimationSpeed`
|
||
fields (and GameWindow consumers).
|
||
- **MOVES to MotionInterpreter calls (edge-driven):** input edges → `PerformMovement`:
|
||
W/S press → `DoMotion(WalkForward/WalkBackward, params)`; release →
|
||
`StopMotion(same)`; strafe/turn keys likewise; Shift edge → `set_hold_run(run,
|
||
interrupt:true)` (retail 0x006b33ca shape); jump release → `jump(extent)` (charge
|
||
accumulation STAYS controller-side, AP-24). The controller keeps a tiny
|
||
prev-key-state edge detector — that IS retail's CommandInterpreter altitude.
|
||
- **STAYS in the controller (input/camera/physics/wire):** Yaw + mouse turn
|
||
(:938-946); keyboard-turn Yaw integration reading
|
||
`_motion.InterpretedState.TurnCommand/TurnSpeed` (:932-937, unchanged); grounded
|
||
velocity write from `get_state_velocity` (:966-976 — the R6-deferred adaptation);
|
||
physics integrate/Resolve/bounce/landing (sections 4-5); heartbeat/AP (section 8);
|
||
auto-walk (B.6, until R4); SetPosition/PortalSpace (the :825 `DoMotion(Ready)`
|
||
arrival-idle becomes `StopCompletely()` — retail's teleport idle is a full stop;
|
||
note in commit); outbound section 6 now READS `_motion.RawState` (commands + per-axis
|
||
hold keys) instead of re-deriving from `input` — wire bytes identical (golden-byte
|
||
packer tests prove it).
|
||
- **GameWindow call sites that change:** `UpdatePlayerAnimation(result)` (:10062-10234)
|
||
DELETED — the player's AnimatedEntity sequencer/manager is bound to the player's
|
||
MotionInterpreter via the same funnel/PerformMovement path remotes use (R2-Q5
|
||
sink shape); the airborne-Falling override falls out of contact_allows_move +
|
||
apply_current_movement (retail mechanism, already live in the funnel); the #45
|
||
sidestep animSpeed factor + ACDREAM_ANIM_SPEED_SCALE knob move into (or are
|
||
re-verified against) the CMotionTable-driven speedMod path — expected-diff
|
||
annotations required; :4344 `ApplyServerRunRate` retargets to `my_run_rate` +
|
||
`apply_current_movement` (same effect, no InterpretedState poke); :7953-7971
|
||
JumpAction build reads the W4-unified launch vector; :7975 call deleted with
|
||
UpdatePlayerAnimation.
|
||
Tests first: FULL suite green; pre-cutover recorded local-player SetCycle/dispatch
|
||
traces (captured BEFORE this commit under the W0 protocol) replayed → same cycle
|
||
identities + speeds, expected-diffs documented (Falling engage now via
|
||
link-strip; sidestep factor source); outbound golden-byte parity (MoveToState/AP/Jump
|
||
byte-identical for the same input script); live smoke ACDREAM_DUMP_MOTION +
|
||
observer view; **ONE user visual pass** (walk/run/toggle/strafe/turn/jump instant
|
||
Falling/landing/emote-mid-run — the stage acceptance).
|
||
Fixture source: **pre-cutover recorded traces + golden-byte + suite**. Deps: W5.
|
||
|
||
**W7 — register sweep + roadmap + digest (docs/cleanup only).**
|
||
Retired rows: MotionDone-unconsumed (R2's), K-fix18, TS-34 (adjust_motion guard),
|
||
plus the S2a StandingLongJump-misattribution note. Surviving rows (verify/add):
|
||
TS-5 (CanJump always-true → now ALSO JumpStaminaCost stub), AP-24 (charge rate),
|
||
TS-24 updated (actions now populated locally — packing next), TS-21/TS-23/AP-30
|
||
(re-anchor line numbers after controller edits), NEW rows: unstick_from_object no-op
|
||
seam (→R5 StickyManager), interrupt_current_movement/cancel_moveto no-op (→R4),
|
||
grounded velocity-from-state write (→R6 root motion), managed LinkedList<MotionNode>
|
||
vs LList (AD-34 wording), A5/A6 pinned-reading notes if either resolves against ACE.
|
||
Roadmap stage table (R3 shipped); milestones "currently working toward" check; memory
|
||
digest note (animation-sequencer deep-dive: pending_motions gap CLOSED end-to-end).
|
||
Deps: W6.
|
||
|
||
Parallelization: W0∥W1 only. W2→W3→W4→W5→W6 are sequential-coupled (queue → gates
|
||
using queue → transitions using gates → dispatchers using all → cutover). W4 and W6
|
||
both touch GameWindow — single-agent each.
|
||
|
||
---
|
||
|
||
## 4. IMotionDoneSink → MotionInterpreter.MotionDone — exact wiring vs R2's seam
|
||
|
||
R2's contract (r2-port-plan §4) delivers: `CSequence` G5 gate → `AnimationDoneSentinel`
|
||
→ GameWindow anim tick drain (:9876-9890 area) → `manager.AnimationDone(true)` per
|
||
sentinel + `manager.UseTime()` once per tick → `MotionTableManager` countdown pop →
|
||
action-class head pops the MANAGER's own `MotionState.RemoveActionHead()` (R2-OWNED)
|
||
→ `IMotionDoneSink.MotionDone(uint motion, bool success)` — bound in R2-Q4 to a
|
||
diagnostic recorder.
|
||
|
||
R3-W2 replaces the recorder:
|
||
|
||
```
|
||
MotionTableManager (per entity) [R2-Q3, exists]
|
||
└─ IMotionDoneSink.MotionDone(motion, success) [R2 seam — signature UNCHANGED]
|
||
└─ R3: the entity's MotionInterpreter.MotionDone(motion, success)
|
||
[stands in for retail CPhysicsObj::MotionDone 0x0050fdb0 →
|
||
MovementManager::MotionDone 0x005242d0 → CMotionInterp::MotionDone
|
||
0x00527ec0 — both intermediates are null-guarded relays with ZERO
|
||
logic; R5 inserts MovementManager between manager and interp
|
||
WITHOUT changing this behavior]
|
||
body: head-peek pending_motions;
|
||
head.Motion & 0x10000000 → unstick seam (no-op → R5)
|
||
+ InterpretedState.RemoveAction()
|
||
+ RawState.RemoveAction();
|
||
pop head UNCONDITIONALLY (A7: motion id + success are IGNORED —
|
||
positional protocol, never match-by-id, never branch on success)
|
||
```
|
||
|
||
Binding points (App, per entity, at creation):
|
||
- **Remote:** where `RemoteMotion` is constructed (each remote already owns a
|
||
`MotionInterpreter` — `remoteMot.Motion`, GameWindow.cs:4646): the same construction
|
||
that gives the entity its MotionTableManager (R2-Q4/Q5) passes
|
||
`sink: remoteMot.Motion` (MotionInterpreter implements IMotionDoneSink directly).
|
||
- **Local player:** `PlayerMovementController` exposes its `MotionInterpreter`
|
||
(new internal property `Motion`); GameWindow binds the player entity's manager to it
|
||
when the player's AnimatedEntity/sequencer is created (same site as
|
||
`AttachCycleVelocityAccessor`, :384).
|
||
|
||
Two structural rules carried from R2 §4 (decomp-confirmed in r3 extraction §7b):
|
||
1. **Two queues, never merged**: `MotionTableManager.pending_animations` (CPartArray
|
||
side, duration-in-ticks) vs `CMotionInterp.pending_motions` (movement side,
|
||
waiting-for-callback). THREE action trackers stay in lockstep via the 0x10000000
|
||
bit: manager's MotionState action FIFO (popped in R2's AnimationDone),
|
||
InterpretedState.actions + RawState.actions (popped in R3's MotionDone).
|
||
2. **Tick placement provisional until R6**: the sentinel drain + UseTime stay at the
|
||
R2-documented GameWindow drain point (G6 seam). R3 adds only the SYNCHRONOUS flush:
|
||
`CMotionInterp.PerformMovement` calls the manager's CheckForCompletedMotions after
|
||
every op (W5), which fires MotionDone same-call for zero-tick nodes.
|
||
|
||
Success-flag adjudication (closes r2-plan §4's warning): retail
|
||
`CMotionInterp::MotionDone` never reads the flag in this build — R3's jump/HitGround
|
||
logic does NOT key off it after all. The flag's only real consumers are
|
||
server/weenie-side (`OnMotionDone`) and the R2 manager's own countdown semantics.
|
||
Preserve pass-through (true from Hook_AnimDone, false from world drains, hardcoded
|
||
true from CheckForCompletedMotions) for R5 signature parity; document as inert at the
|
||
interp.
|
||
|
||
Enter/exit world: exit paths call `manager.HandleExitWorld()` (drains
|
||
pending_animations, each firing sink.MotionDone(motion, false) → interp pops in step)
|
||
THEN `interp.HandleExitWorld()` (drains any remainder — retail runs both layers'
|
||
drains via CPartArray::HandleExitWorld and MovementManager::HandleExitWorld
|
||
independently). Enter-world: manager-side only (`remove_all_link_animations` + drain);
|
||
`MovementManager::HandleEnterWorld`'s interp relay is the mis-symbolicated tailcall
|
||
(decomp §6e) — CMotionInterp has NO HandleEnterWorld; do not invent one (negative
|
||
result, decomp §6e/§1d).
|
||
|
||
---
|
||
|
||
## 5. NEGATIVE RESULTS / DO-NOT-INVENT (binding on subagents)
|
||
|
||
- `CPhysicsObj::IsAnimating` / `IsMovingOrAnimating` — **not in this PDB** (decomp
|
||
§7f). ACE's `PhysicsObj.IsAnimating` bool is an ACE-ism. acdream queries
|
||
`motions_pending()` / `is_standing_still()` / `CPartArray.HasAnims` individually.
|
||
- `MovementManager::HandleUpdateTarget` — not in this PDB (§6j); likely
|
||
MoveToManager-internal (R4). Do not stub.
|
||
- `CMotionInterp::HandleEnterWorld` — does not exist (§6e); enter-world work is
|
||
manager/CPartArray-side only.
|
||
- `MakeMoveToManager` / MoveToManager internals — R4. R3 leaves the B.6 auto-walk +
|
||
`interrupt_current_movement`/`cancel_moveto` as registered no-op seams.
|
||
- `NoticeHandler::RecvNotice_PrevSpellSelection` (HandleEnterWorld/ReportExhaustion
|
||
relays) — retail spell-UI notice ACE never ported; out of R3 scope; file a research
|
||
note only (no ACE reference exists — decomp-direct if ever needed).
|
||
- `LeaveGround` address is **0x00528b00** — the 0x00529710 in MotionInterpreter.cs's
|
||
doc comment (:1153, :1159) is a stale Ghidra-chunk guess; W4 fixes the citations
|
||
(several header comments cite old FUN_ addresses — sweep them to named symbols).
|