acdream/docs/research/2026-07-02-r3-motioninterp/r3-port-plan.md
Erik 8eff397801 docs(R3-W-1): CMotionInterp-completion research base — decomp extraction + ACE cross-ref + port work-list
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>
2026-07-02 20:56:16 +02:00

377 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).