# R3 — ACE port map: MotionInterp + MovementManager + MotionDone chain Sources (full-file reads, all line numbers are 1-indexed in the ACE source tree): - `references/ACE/Source/ACE.Server/Physics/Animation/MotionInterp.cs` (836 lines, whole file read) - `references/ACE/Source/ACE.Server/Physics/Managers/MovementManager.cs` (216 lines, whole file read) - `references/ACE/Source/ACE.Server/Physics/Animation/MotionNode.cs` (21 lines, whole file read) - `references/ACE/Source/ACE.Server/Physics/Managers/MotionTableManager.cs` (252 lines, whole file read — AnimationDone/CheckForCompletedMotions, the actual MotionDone producer) - `references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs` (targeted reads: L80-109 IsAnimating/IsMovingOrAnimating, L290-301 CheckForCompletedMotions, L890-903 MotionDone, L4235-4247 ShowPendingMotions/motions_pending) - `references/ACE/Source/ACE.Server/Physics/PartArray.cs` (L60-77 CheckForCompletedMotions passthrough) No named-retail cross-reference was performed in this pass (ACE-side mapping only, per R3 scope). ACE class is `MotionInterp` (note: **not** `CMotionInterp` — ACE dropped the `C` Hungarian prefix project-wide, this is a naming-convention divergence not a functional one). --- ## 1. Class fields (MotionInterp.cs L14-32) ```csharp public bool Initted; public WeenieObject WeenieObj; public PhysicsObj PhysicsObj; public RawMotionState RawState; public InterpretedMotionState InterpretedState; public float CurrentSpeedFactor; public bool StandingLongJump; public float JumpExtent; public int ServerActionStamp; public float MyRunRate; public LinkedList PendingMotions; public const float BackwardsFactor = 6.4999998e-1f; // 0.65 public const float MaxSidestepAnimRate = 3.0f; public const float RunAnimSpeed = 4.0f; public const float RunTurnFactor = 1.5f; public const float SidestepAnimSpeed = 1.25f; public const float SidestepFactor = 0.5f; public const float WalkAnimSpeed = 3.1199999f; // 3.12 ``` Notes: - `PendingMotions` is `LinkedList`, not an array/ring buffer — ACE reimplemented retail's pending-motion queue as a doubly-linked list. `MotionNode` (see §2) carries `ContextID`, `Motion`, `JumpErrorCode` — this is the FIFO queue of in-flight interpreted motions waiting on animation completion. - `CurrentSpeedFactor` is declared but the only read site in this file is `get_adjusted_max_speed()` (L629) — no write site anywhere in MotionInterp.cs. Likely set externally (WeenieObj or PhysicsObj) — not visible in this file. - `JumpExtent` doubles as both "how hard is the current jump charging" (set in `jump()`, read in `charge_jump()`/`get_jump_v_z()`) and as a guard the WeenieObj layer checks via `CanJump(JumpExtent)`. --- ## 2. MotionNode (queue element) — MotionNode.cs, whole file ```csharp public class MotionNode { public int ContextID; public uint Motion; public WeenieError JumpErrorCode; public MotionNode() { } public MotionNode(int contextID, uint motion, WeenieError jumpErrorCode) { ContextID = contextID; Motion = motion; JumpErrorCode = jumpErrorCode; } } ``` Trivial DTO. `JumpErrorCode` is the pre-computed "would a jump be legal right now, if the player pressed jump while this pending motion is still in flight" — computed once at `add_to_queue` time (see `motion_allows_jump` call sites in `DoInterpretedMotion`/`StopCompletely`), then consumed lazily by `jump_is_allowed()` (§7) via `PendingMotions.First.Value.JumpErrorCode` when `PendingMotions.Count > 1`. --- ## 3. add_to_queue / MotionDone / RemoveMotion (queue lifecycle) ### add_to_queue (L388-392) ```csharp public void add_to_queue(int contextID, uint motion, WeenieError jumpErrorCode) { PendingMotions.AddLast(new MotionNode(contextID, motion, jumpErrorCode)); PhysicsObj.IsAnimating = true; } ``` Simple append + set the `IsAnimating` flag unconditionally true (even if this is the first entry). Called from: - `DoInterpretedMotion` (L85) — normal interpreted-motion success path, `jump_error_code` precomputed at L73-83. - `StopCompletely` (L321) — with hardcoded `MotionCommand.Ready` and a `jump` precomputed at L307. - `StopInterpretedMotion` (L348) — with hardcoded `MotionCommand.Ready` and `WeenieError.None`. - `apply_interpreted_movement` (L495) — the turn-stop fallback branch, hardcoded `MotionCommand.Ready` / `WeenieError.None`. ### MotionDone (L210-234) — the dequeue-on-animation-complete handler ```csharp public void MotionDone(bool success) { if (PhysicsObj == null) return; var motionData = PendingMotions.First; // null or != last in list? if (motionData != null) { var pendingMotion = motionData.Value; if ((pendingMotion.Motion & (uint)CommandMask.Action) != 0) { PhysicsObj.unstick_from_object(); InterpretedState.RemoveAction(); RawState.RemoveAction(); } motionData = PendingMotions.First; if (motionData != null) { PendingMotions.Remove(motionData); PhysicsObj.IsAnimating = PendingMotions.Count > 0; } } } ``` Note the ACE dev's own inline comment `// null or != last in list?` — flags this as a spot they weren't fully certain about vs. retail (a "did I get this right" breadcrumb). Two things happen if the head node exists: 1. If the head's Motion has the `Action` bit set (`CommandMask.Action`), it's treated as an "action" motion (e.g. attack/emote) that stuck the object to something — `unstick_from_object()` + pop the action off both InterpretedState and RawState action stacks. 2. **Re-fetches `PendingMotions.First` a second time** (redundant re-read — `motionData` unchanged since nothing between the two reads mutates the list) before removing it and recomputing `IsAnimating` as `Count > 0` (as opposed to `add_to_queue`'s unconditional `true`). `success` parameter is accepted but **never read** in this method body — dead parameter as far as MotionInterp.MotionDone goes. (It IS read/propagated further down in `MotionTableManager.AnimationDone`/`CheckForCompletedMotions`, see §8 — but `MotionInterp.MotionDone` itself ignores it entirely.) ### RemoveMotion Not defined in MotionInterp.cs — it's called on `InterpretedState`/`RawState` (`InterpretedState.RemoveMotion(motion)`, `RawState.RemoveMotion(motion)`), i.e. lives in `InterpretedMotionState`/`RawMotionState`/`MotionState` classes, NOT in MotionInterp. Call sites inside this file: L340, L351, L358, L498. Out of scope for this file-bounded pass (not in MotionInterp.cs or MovementManager.cs). --- ## 4. DoMotion / StopMotion / StopCompletely (top-level motion API) ### DoMotion (L112-158) — raw-command entry point ```csharp public WeenieError DoMotion(uint motion, MovementParameters movementParams) { if (PhysicsObj == null) return WeenieError.NoPhysicsObject; var currentParams = new MovementParameters(); currentParams.CopySome(movementParams); var currentMotion = motion; if (movementParams.CancelMoveTo) PhysicsObj.cancel_moveto(); if (movementParams.SetHoldKey) SetHoldKey(movementParams.HoldKeyToApply, movementParams.CancelMoveTo); adjust_motion(ref currentMotion, ref currentParams.Speed, movementParams.HoldKeyToApply); if (InterpretedState.CurrentStyle != (uint)MotionCommand.NonCombat) { switch (motion) { case (uint)MotionCommand.Crouch: return WeenieError.CantCrouchInCombat; case (uint)MotionCommand.Sitting: return WeenieError.CantSitInCombat; case (uint)MotionCommand.Sleeping: return WeenieError.CantLieDownInCombat; } if ((motion & (uint)CommandMask.ChatEmote) != 0) return WeenieError.CantChatEmoteInCombat; } if ((motion & (uint)CommandMask.Action) != 0) { if (InterpretedState.GetNumActions() >= 6) return WeenieError.TooManyActions; } var result = DoInterpretedMotion(currentMotion, currentParams); if (result == WeenieError.None && movementParams.ModifyRawState) RawState.ApplyMotion(motion, movementParams); return result; } ``` Key points: - `switch (motion)` (combat-blocked motions) tests the **original** `motion` parameter, not `currentMotion` (post-`adjust_motion` mutation) — combat-block checks happen on the raw pre-adjustment command. - Hardcoded action cap of 6 concurrent actions (`GetNumActions() >= 6`). - `RawState.ApplyMotion` uses the **original** `motion`, not `currentMotion` — raw state records what was actually requested, interpreted state (via `DoInterpretedMotion`) gets the post-`adjust_motion` (walk→run promoted, backward-inverted, etc.) version. ### StopMotion (L367-386) Mirror of DoMotion but for the "stop" side: `cancel_moveto` gate, `adjust_motion`, delegates to `StopInterpretedMotion`, then conditionally calls `RawState.RemoveMotion(motion)` (original motion, not adjusted) on success. ### StopCompletely (L301-327) ```csharp public WeenieError StopCompletely() { if (PhysicsObj == null) return WeenieError.NoPhysicsObject; PhysicsObj.cancel_moveto(); var jump = motion_allows_jump(InterpretedState.ForwardCommand); RawState.ForwardCommand = (uint)MotionCommand.Ready; RawState.ForwardSpeed = 1.0f; RawState.SideStepCommand = 0; RawState.TurnCommand = 0; InterpretedState.ForwardCommand = (uint)MotionCommand.Ready; InterpretedState.ForwardSpeed = 1.0f; InterpretedState.SideStepCommand = 0; InterpretedState.TurnCommand = 0; PhysicsObj.StopCompletely_Internal(); add_to_queue(0, (uint)MotionCommand.Ready, jump); if (PhysicsObj.CurCell == null) PhysicsObj.RemoveLinkAnimations(); return WeenieError.None; } ``` Hard-resets both Raw and Interpreted state (Forward/SideStep/Turn) to Ready/1.0/0/0 directly by field assignment — bypasses the normal `ApplyMotion`/`RemoveMotion` mutators entirely. `jump` (the eligibility precomputed BEFORE the reset, off the OLD `InterpretedState.ForwardCommand`) is stashed into the queued `Ready` node's `JumpErrorCode`. Delegates the physics-side reset to `PhysicsObj.StopCompletely_Internal()` (not in this file). --- ## 5. DoInterpretedMotion / StopInterpretedMotion (interpreted-command layer) ### DoInterpretedMotion (L51-110) ```csharp public WeenieError DoInterpretedMotion(uint motion, MovementParameters movementParams) { if (PhysicsObj == null) return WeenieError.NoPhysicsObject; var result = WeenieError.None; if (contact_allows_move(motion)) { if (StandingLongJump && (motion == WalkForward || motion == RunForward || motion == SideStepRight)) { if (movementParams.ModifyInterpretedState) InterpretedState.ApplyMotion(motion, movementParams); } else { if (motion == (uint)MotionCommand.Dead) PhysicsObj.RemoveLinkAnimations(); result = PhysicsObj.DoInterpretedMotion(motion, movementParams); if (result == WeenieError.None) { var jump_error_code = WeenieError.None; if (movementParams.DisableJumpDuringLink) jump_error_code = WeenieError.YouCantJumpFromThisPosition; else { jump_error_code = motion_allows_jump(motion); if (jump_error_code == WeenieError.None && (motion & (uint)CommandMask.Action) == 0) jump_error_code = motion_allows_jump(InterpretedState.ForwardCommand); } add_to_queue(movementParams.ContextID, motion, jump_error_code); if (movementParams.ModifyInterpretedState) InterpretedState.ApplyMotion(motion, movementParams); } } } else { if ((motion & (uint)CommandMask.Action) != 0) result = WeenieError.YouCantJumpWhileInTheAir; else { if (movementParams.ModifyInterpretedState) InterpretedState.ApplyMotion(motion, movementParams); result = WeenieError.None; } } if (PhysicsObj.CurCell == null) PhysicsObj.RemoveLinkAnimations(); return result; } ``` Control-flow shape: 1. Gate on `contact_allows_move(motion)` (§7) — governs whether the object is grounded enough to accept this motion at all. 2. **StandingLongJump special-case**: if mid-standing-long-jump AND the incoming motion is one of {WalkForward, RunForward, SideStepRight}, the motion is applied to InterpretedState ONLY (no `PhysicsObj.DoInterpretedMotion` call, no queue entry) — the animation itself is suppressed while charging/airborne from a standing jump, but the *intent* state still updates so movement resumes correctly on landing. 3. Otherwise: `Dead` motion clears link animations first; then delegates the actual animation dispatch to `PhysicsObj.DoInterpretedMotion` (physics/animation-table layer, out of file scope); on success, computes the jump-error-code for THIS pending motion (double motion_allows_jump check: first against the incoming `motion` itself, then — only if the incoming motion isn't itself an Action AND passed — against the current `ForwardCommand`), queues it, and conditionally applies to InterpretedState. 4. If contact does NOT allow movement: Action-class motions fail outright with `YouCantJumpWhileInTheAir`; everything else silently updates InterpretedState (if requested) and returns success — i.e., non-action motions (turning, etc.) are allowed to update intent state even while airborne, just not animate/queue. 5. Unconditional tail: if `CurCell == null` (off the cell grid — e.g. despawned/uninitialized), strip link animations. ### StopInterpretedMotion (L329-365) Structural mirror of DoInterpretedMotion for the "stop" direction: same `contact_allows_move` gate, same StandingLongJump special-case (calls `InterpretedState.RemoveMotion` instead of `ApplyMotion`), same delegate-to-PhysicsObj pattern (`PhysicsObj.StopInterpretedMotion`), same add_to_queue-with-Ready-on-success pattern, same CurCell==null tail cleanup. The "contact disallows" else-branch here has NO action-vs-non-action split (StopInterpretedMotion always just conditionally calls `RemoveMotion` and returns `WeenieError.None`) — asymmetric with DoInterpretedMotion's stricter "action motions error out while airborne" rule. --- ## 6. HitGround / LeaveGround (contact transition hooks) ### HitGround (L175-185) ```csharp public void HitGround() { if (PhysicsObj == null) return; if (WeenieObj != null && !WeenieObj.IsCreature()) return; if (!PhysicsObj.State.HasFlag(PhysicsState.Gravity)) return; PhysicsObj.RemoveLinkAnimations(); apply_current_movement(false, true); } ``` Guarded to creature-only (or WeenieObj==null, i.e. non-weenie-backed physics objects) AND gravity-affected objects. Strips link animations and re-applies current movement intent with `cancelMoveTo=false, allowJump=true`. ### LeaveGround (L192-208) ```csharp public void LeaveGround() { if (PhysicsObj == null) return; if (WeenieObj != null && !WeenieObj.IsCreature()) return; if (!PhysicsObj.State.HasFlag(PhysicsState.Gravity)) return; var velocity = get_leave_ground_velocity(); PhysicsObj.set_local_velocity(velocity, true); StandingLongJump = false; JumpExtent = 0; PhysicsObj.RemoveLinkAnimations(); apply_current_movement(false, true); } ``` Same guard pattern as HitGround. Computes leave-ground velocity (§9), pushes it into physics as a LOCAL velocity (`set_local_velocity(velocity, true)` — second arg likely "isLocal"/"autonomous", not resolved in this file), clears the jump-charge state (`StandingLongJump=false`, `JumpExtent=0`), strips link anims, reapplies movement. **Called from `enter_default_state()` (L615)** as the terminal step of default-state setup — i.e. every newly-initialized MotionInterp starts as if it just left the ground. MovementManager wrappers (MovementManager.cs): - `HitGround()` (L66-73): calls `MotionInterpreter.HitGround()` THEN `MoveToManager.HitGround()` — both interpreters get the hit-ground signal, MotionInterp first. - `LeaveGround()` (L104-110): calls `MotionInterpreter.LeaveGround()` only; has a dead commented-out line `// NoticeHandler::RecvNotice_PrevSpellSection` (retail-symbol trace left as a comment — not ported to ACE — a network-notice hook ACE apparently didn't implement here). --- ## 7. Jump family ### jump (L710-727) ```csharp public WeenieError jump(float extent, int adjustStamina) { if (PhysicsObj == null) return WeenieError.NoPhysicsObject; PhysicsObj.cancel_moveto(); var result = jump_is_allowed(extent, adjustStamina); if (result == WeenieError.None) { JumpExtent = extent; PhysicsObj.set_on_walkable(false); } else StandingLongJump = false; return result; } ``` Note: on success it sets `JumpExtent` and clears the walkable flag but does NOT itself call `LeaveGround()` or apply velocity — that must happen elsewhere (likely triggered by the physics tick detecting `on_walkable==false` + `JumpExtent>0`, out of file scope). On failure it clears `StandingLongJump` (defensive — aborts an in-progress standing-long-jump charge if the actual jump call fails). ### jump_is_allowed (L742-768) ```csharp public WeenieError jump_is_allowed(float extent, int staminaCost) { if (PhysicsObj == null) return WeenieError.NoPhysicsObject; if (WeenieObj == null && !WeenieObj.IsCreature() || !PhysicsObj.State.HasFlag(PhysicsState.Gravity) || PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact | TransientStateFlags.OnWalkable)) { if (PhysicsObj.IsFullyConstrained()) return WeenieError.GeneralMovementFailure; if (PendingMotions.Count > 1 && PendingMotions.First.Value.JumpErrorCode != 0) return PendingMotions.First.Value.JumpErrorCode; var jumpError = jump_charge_is_allowed(); if (jumpError == WeenieError.None) { jumpError = motion_allows_jump(InterpretedState.ForwardCommand); if (jumpError == WeenieError.None && WeenieObj != null && WeenieObj.JumpStaminaCost(extent, staminaCost) == 0) jumpError = WeenieError.GeneralMovementFailure; } return jumpError; } return WeenieError.YouCantJumpWhileInTheAir; } ``` **LIKELY BUG (ACE-side, flag for cross-check against named-retail):** `WeenieObj == null && !WeenieObj.IsCreature()` — if `WeenieObj` actually is null, `!WeenieObj.IsCreature()` would NPE; C#'s `&&` short-circuits left-to-right so `WeenieObj == null` being true still evaluates the right side... wait, no: `&&` short-circuits so if `WeenieObj == null` is `true`, C# does NOT evaluate `!WeenieObj.IsCreature()` — correct, no NPE. But semantically this condition is almost certainly meant to be `WeenieObj != null && !WeenieObj.IsCreature()` (matching the guard pattern used identically in HitGround/LeaveGround/apply_current_movement/adjust_motion — all of which use `WeenieObj != null && !WeenieObj.IsCreature()`). As written, `WeenieObj == null && !WeenieObj.IsCreature()` can **never be true** (if WeenieObj==null is true, the whole condition short-circuits false because `&&` needs the null-check false... actually WeenieObj==null must be TRUE for this branch, and if it's true the right operand isn't even evaluated, so `WeenieObj==null && [anything]` reduces to just needing `WeenieObj==null` — no, `A && B` requires BOTH true; if A is true B is still checked. If A is true (WeenieObj IS null) and B accesses `WeenieObj.IsCreature()` on a null WeenieObj, this WOULD NPE.** This is a genuine apparent typo vs. the established pattern elsewhere in the same file (should be `WeenieObj != null && !WeenieObj.IsCreature()`) — **flag as a divergence risk to verify against `docs/research/named-retail/` before porting this exact condition to acdream.** - Entry condition: allowed to even check jump-legality if EITHER not-a-creature-weenie, OR not gravity-affected, OR (already in Contact+OnWalkable state). - `IsFullyConstrained()` → GeneralMovementFailure. - **Queue-lookahead check**: if there's more than one pending motion AND the head's precomputed `JumpErrorCode != 0`, return that cached error immediately (short-circuit — reuses the jump-eligibility computed back when that motion was queued, rather than recomputing). - Otherwise chains `jump_charge_is_allowed()` → `motion_allows_jump(ForwardCommand)` → `WeenieObj.JumpStaminaCost(extent, staminaCost) == 0` (stamina check, external to this file) → `GeneralMovementFailure` if stamina call returns 0. - If the outer gate fails, returns `YouCantJumpWhileInTheAir` (airborne + gravity + not a special-cased non-creature). ### jump_charge_is_allowed (L729-740) ```csharp public WeenieError jump_charge_is_allowed() { if (WeenieObj != null && !WeenieObj.CanJump(JumpExtent)) return WeenieError.CantJumpLoadedDown; var forward = InterpretedState.ForwardCommand; if (forward == (uint)MotionCommand.Fallen || forward >= (uint)MotionCommand.Crouch && forward <= (uint)MotionCommand.Sleeping) return WeenieError.YouCantJumpFromThisPosition; return WeenieError.None; } ``` `WeenieObj.CanJump(JumpExtent)` is presumably an encumbrance/burden check (name: "loaded down"). Blocks jump-charging while `Fallen` or in the [Crouch..Sleeping] MotionCommand range (a contiguous enum-range test — same idiom used throughout this file for "in one of these seated/prone states"). ### charge_jump (L564-582) ```csharp public int charge_jump() { if (WeenieObj != null && !WeenieObj.CanJump(JumpExtent)) return 0x49; var forward = InterpretedState.ForwardCommand; if (forward == (uint)MotionCommand.Falling || forward >= (uint)MotionCommand.Crouch && forward < (uint)MotionCommand.Sleeping) return 0x48; else { if (PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact | TransientStateFlags.OnWalkable) && forward == (uint)MotionCommand.Ready && InterpretedState.SideStepCommand == 0 && InterpretedState.TurnCommand == 0) { StandingLongJump = true; } } return 0; } ``` Returns raw `int` error codes (`0x49`, `0x48`, `0`) rather than `WeenieError` enum — a leftover-looking retail-style raw-hresult-ish return convention (likely these hex values ARE `WeenieError` underlying ints but the method signature wasn't converted — worth checking `0x49`/`0x48` against the `WeenieError` enum values for `CantJumpLoadedDown`/`YouCantJumpFromThisPosition` equivalents). **Note the range-check here uses `Falling` (not `Fallen` as in `jump_charge_is_allowed`) and `< Sleeping` (exclusive) vs `jump_charge_is_allowed`'s `<= Sleeping` (inclusive)** — these two "similar" gating functions have subtly different boundary conditions; flag for retail cross-check, this looks like it could be an ACE transcription inconsistency OR an intentional retail distinction between "starting to charge a jump" vs "already mid-charge-and-issuing-the-actual-jump." The actual `StandingLongJump = true` side-effect only fires when: grounded+walkable, current forward command is exactly `Ready` (standing still), AND no sidestep/turn in progress — i.e. this is the "player pressed-and-held jump while standing still" detector that arms the long-jump-charge special path used throughout DoInterpretedMotion/StopInterpretedMotion/apply_interpreted_movement. ### get_jump_v_z (L634-652) ```csharp public float get_jump_v_z() { if (JumpExtent < PhysicsGlobals.EPSILON) return 0.0f; var extent = JumpExtent; if (extent > 1.0f) extent = 1.0f; if (WeenieObj == null) return 10.0f; float vz = extent; if (WeenieObj.InqJumpVelocity(extent, out vz)) return vz; return 0.0f; } ``` Clamps `JumpExtent` to [something-above-EPSILON, 1.0], delegates the actual extent→velocity curve to `WeenieObj.InqJumpVelocity` (out of file scope — presumably reads jump skill). Fallback of `10.0f` if there's no WeenieObj at all (non-weenie physics object jumping at max power, e.g. test/editor objects). `float vz = extent;` is a dead initializer — immediately overwritten by the `out vz` param if `InqJumpVelocity` returns true, else the method returns `0.0f` on the false path (never returns the dead-initialized `extent` value). ### get_leave_ground_velocity (L654-663) ```csharp public Vector3 get_leave_ground_velocity() { var velocity = get_state_velocity(); velocity.Z = get_jump_v_z(); if (Vec.IsZero(velocity)) velocity = PhysicsObj.Position.GlobalToLocalVec(PhysicsObj.Velocity); return velocity; } ``` Composes horizontal velocity from current interpreted-state motion (§9 `get_state_velocity`) with vertical velocity from the jump charge, UNLESS the composed vector is exactly zero — in which case it falls back to converting the physics object's actual (global) velocity into local space. This fallback matters for e.g. falling-off-a-ledge (no jump input, no WASD, but the object still has downward/lateral velocity from having walked off an edge) vs. a genuine standing-still jump. --- ## 8. HitGround/MotionDone chain — full call graph (cross-file, MotionTableManager.cs) The actual "animation finished playing" signal originates in `MotionTableManager` (a SEPARATE class from `MotionInterp`, owned by `PartArray`, NOT by `MovementManager`): ``` MotionTableManager.AnimationDone(bool success) [L28-61] MotionTableManager.CheckForCompletedMotions() [L63-85] -> PhysicsObj.MotionDone(motionID, success) [PhysicsObj.cs L899-903] -> MovementManager.MotionDone(motion, success) [MovementManager.cs L118-122] -> MotionInterpreter.MotionDone(success) [MotionInterp.cs L210-234] -> PhysicsObj.WeenieObj.OnMotionDone(motionID, success) [parallel notify, weenie-layer] ``` Both `AnimationDone` and `CheckForCompletedMotions` in MotionTableManager independently walk `PendingAnimations` (a `LinkedList`, the animation-table-layer's OWN pending queue — distinct from `MotionInterp.PendingMotions`) and call `PhysicsObj.MotionDone` once per completed entry, followed immediately by `WeenieObj.OnMotionDone` — i.e. **every completed motion fires two independent listeners**: the MotionInterp queue-pop (state-machine bookkeeping) and the WeenieObject notification (game-logic reaction, e.g. quest triggers, sound cues — out of scope here). `CheckForCompletedMotions()` reachability: ``` MotionInterp.PerformMovement(mvs) [L260] -- called after EVERY movement dispatch (Do/Stop/StopCompletely) -> PhysicsObj.CheckForCompletedMotions() [PhysicsObj.cs L296-300] -> PartArray.CheckForCompletedMotions() [PartArray.cs L72-76] -> MotionTableManager.CheckForCompletedMotions() [L63-85] ``` So every `MovementManager.PerformMovement` → `MotionInterp.PerformMovement` call (§10) ends with an immediate synchronous drain of any already-finished animations at the head of `PendingAnimations` — this is how a 0-frame-length animation (e.g. an instant `Ready` transition) gets its `MotionDone` fired same-tick rather than waiting for the next animation-tick callback. `MotionTableManager.UseTime()` (L158-161) also calls `CheckForCompletedMotions()` — this is the periodic (per-tick, presumably driven by `MovementManager.UseTime()` → `MoveToManager.UseTime()`... **NOTE:** `MovementManager.UseTime()` (MovementManager.cs L176-179) only forwards to `MoveToManager.UseTime()`, NOT to `MotionInterpreter`/`MotionTableManager` — so `MotionTableManager.UseTime()`'s caller is NOT in this file pair; it must be driven directly by `PartArray`/`Sequence`'s own per-tick update, bypassing MovementManager entirely). `AnimationDone` (L28-61) is the OTHER path into the same `PhysicsObj.MotionDone` call — driven by whatever animation-tick/callback system invokes it directly (not visible in these two files; likely `Sequence`/`AFrame` playback completion). Its loop differs from `CheckForCompletedMotions` in that it decrements a running `AnimationCounter` against each node's `NumAnims` rather than checking `NumAnims != 0` directly — i.e. `AnimationDone` is the incremental "one more anim-frame-group finished" tick, while `CheckForCompletedMotions` is the "resync/drain everything already at zero" batch pass. --- ## 9. apply_raw / apply_interpreted / apply_current_movement (state → animation dispatch) ### apply_current_movement (L430-438) — dispatcher ```csharp public void apply_current_movement(bool cancelMoveTo, bool allowJump) { if (PhysicsObj == null || !Initted) return; if (WeenieObj != null && !WeenieObj.IsCreature() || !PhysicsObj.movement_is_autonomous()) apply_interpreted_movement(cancelMoveTo, allowJump); else apply_raw_movement(cancelMoveTo, allowJump); } ``` Routes to raw-vs-interpreted based on: non-creature-weenie OR not-autonomous-movement → interpreted path; creature AND autonomous movement → raw path. (`movement_is_autonomous()` presumably distinguishes server-driven/scripted motion from player-input-driven motion — out of file scope.) Requires `Initted == true` (set at the tail of `enter_default_state`, §11) — calls before init are no-ops. ### apply_raw_movement (L506-523) ```csharp public void apply_raw_movement(bool cancelMoveTo, bool allowJump) { if (PhysicsObj == null) return; InterpretedState.CurrentStyle = RawState.CurrentStyle; InterpretedState.ForwardCommand = RawState.ForwardCommand; InterpretedState.ForwardSpeed = RawState.ForwardSpeed; InterpretedState.SideStepCommand = RawState.SideStepCommand; InterpretedState.SideStepSpeed = RawState.SideStepSpeed; InterpretedState.TurnCommand = RawState.TurnCommand; InterpretedState.TurnSpeed = RawState.TurnSpeed; adjust_motion(ref InterpretedState.ForwardCommand, ref InterpretedState.ForwardSpeed, RawState.ForwardHoldKey); adjust_motion(ref InterpretedState.SideStepCommand, ref InterpretedState.SideStepSpeed, RawState.SideStepHoldKey); adjust_motion(ref InterpretedState.TurnCommand, ref InterpretedState.TurnSpeed, RawState.TurnHoldKey); apply_interpreted_movement(cancelMoveTo, allowJump); } ``` Copies all 7 raw-state fields verbatim into interpreted-state, THEN runs `adjust_motion` (§12) independently over each of the three command/speed pairs (Forward, SideStep, Turn) with their respective per-axis hold-keys, THEN falls through to `apply_interpreted_movement`. This is the "translate the low-level input intent into the higher-level interpreted/animation intent" step — walk→run promotion, backward-inversion, sidestep animation-rate scaling all happen here per-axis. ### apply_interpreted_movement (L440-504) ```csharp public void apply_interpreted_movement(bool cancelMoveTo, bool allowJump) { if (PhysicsObj == null) return; var movementParams = new MovementParameters(); movementParams.SetHoldKey = false; movementParams.ModifyInterpretedState = false; movementParams.CancelMoveTo = cancelMoveTo; movementParams.DisableJumpDuringLink = !allowJump; if (InterpretedState.ForwardCommand == (uint)MotionCommand.RunForward) MyRunRate = InterpretedState.ForwardSpeed; DoInterpretedMotion(InterpretedState.CurrentStyle, movementParams); if (contact_allows_move(InterpretedState.ForwardCommand)) { if (!StandingLongJump) { movementParams.Speed = InterpretedState.ForwardSpeed; DoInterpretedMotion(InterpretedState.ForwardCommand, movementParams); if (InterpretedState.SideStepCommand != 0) { movementParams.Speed = InterpretedState.SideStepSpeed; DoInterpretedMotion(InterpretedState.SideStepCommand, movementParams); } else StopInterpretedMotion((uint)MotionCommand.SideStepRight, movementParams); } else { movementParams.Speed = 1.0f; DoInterpretedMotion((uint)MotionCommand.Ready, movementParams); StopInterpretedMotion((uint)MotionCommand.SideStepRight, movementParams); } } else { movementParams.Speed = 1.0f; DoInterpretedMotion((uint)MotionCommand.Falling, movementParams); } if (InterpretedState.TurnCommand != 0) { movementParams.Speed = InterpretedState.TurnSpeed; DoInterpretedMotion(InterpretedState.TurnCommand, movementParams); } else { var result = PhysicsObj.StopInterpretedMotion((uint)MotionCommand.TurnRight, movementParams); if (result == WeenieError.None) { add_to_queue(movementParams.ContextID, (uint)MotionCommand.Ready, WeenieError.None); if (movementParams.ModifyInterpretedState) InterpretedState.RemoveMotion((uint)MotionCommand.TurnRight); } if (PhysicsObj.CurCell == null) PhysicsObj.RemoveLinkAnimations(); } } ``` This is the per-axis re-dispatch of the CURRENT interpreted state as fresh `DoInterpretedMotion`/`StopInterpretedMotion` calls (used both after raw→interpreted translation, and directly whenever contact/gravity transitions need to re-trigger animation — e.g. HitGround/LeaveGround/SetPhysicsObject/SetWeenieObject/SetHoldKey/set_hold_run all funnel here via `apply_current_movement`). Sequence: 1. `CurrentStyle` motion (stance) always re-dispatched first. 2. `MyRunRate` cache updated from `ForwardSpeed` whenever currently running (side-channel — persists the "last known run speed" independent of whatever WeenieObj.InqRunRate later reports). 3. If contact allows movement: normal case re-dispatches Forward + (SideStep OR explicit SideStepRight-stop); StandingLongJump case instead forces Ready + explicit SideStepRight-stop (suppresses forward/sidestep animation while charging a standing jump, matching the earlier note in DoInterpretedMotion). 4. If contact does NOT allow movement: unconditionally dispatches `Falling` (Speed=1.0) regardless of what ForwardCommand was. 5. Turn is handled as its own independent branch — Turn re-dispatches like Forward/SideStep, but the "stop" path when TurnCommand==0 is inlined directly here (calls `PhysicsObj.StopInterpretedMotion` — the PHYSICS layer method, not `this.StopInterpretedMotion` — and duplicates the add_to_queue/RemoveMotion/RemoveLinkAnimations bookkeeping inline rather than delegating to `this.StopInterpretedMotion`). This inline duplication vs delegating is a structural oddity worth flagging — every other "stop with bookkeeping" path in this file goes through the member `StopInterpretedMotion` method, but this one open-codes it against `PhysicsObj.StopInterpretedMotion` directly. ### get_state_velocity (L678-700) — used by get_leave_ground_velocity (§7) ```csharp public Vector3 get_state_velocity() { var velocity = Vector3.Zero; if (InterpretedState.SideStepCommand == (uint)MotionCommand.SideStepRight) velocity.X = SidestepAnimSpeed * InterpretedState.SideStepSpeed; if (InterpretedState.ForwardCommand == (uint)MotionCommand.WalkForward) velocity.Y = WalkAnimSpeed * InterpretedState.ForwardSpeed; else if (InterpretedState.ForwardCommand == (uint)MotionCommand.RunForward) velocity.Y = RunAnimSpeed * InterpretedState.ForwardSpeed; var rate = MyRunRate; if (WeenieObj != null) WeenieObj.InqRunRate(ref rate); var maxSpeed = RunAnimSpeed * rate; if (velocity.Length() > maxSpeed) { velocity = Vector3.Normalize(velocity); velocity *= maxSpeed; } return velocity; } ``` X = lateral (sidestep) component, Y = forward component (local space — X/Y here are NOT world axes). Clamped to a max speed derived from `RunAnimSpeed * runRate` — note `WeenieObj.InqRunRate(ref rate)`'s return value is discarded here (unlike `get_adjusted_max_speed`/`get_max_speed` which check the bool return and fall back to `MyRunRate` only if it returns false) — `rate` is pre-seeded with `MyRunRate` and then `InqRunRate` is allowed to overwrite it unconditionally regardless of success/failure return. --- ## 10. PerformMovement (MotionInterp) vs PerformMovement (MovementManager) ### MotionInterp.PerformMovement (L236-262) ```csharp public WeenieError PerformMovement(MovementStruct mvs) { var result = WeenieError.None; switch (mvs.Type) { case MovementType.RawCommand: result = DoMotion(mvs.Motion, mvs.Params); break; case MovementType.InterpretedCommand: result = DoInterpretedMotion(mvs.Motion, mvs.Params); break; case MovementType.StopRawCommand: result = StopMotion(mvs.Motion, mvs.Params); break; case MovementType.StopInterpretedCommand: result = StopInterpretedMotion(mvs.Motion, mvs.Params); break; case MovementType.StopCompletely: result = StopCompletely(); break; default: return WeenieError.GeneralMovementFailure; } PhysicsObj.CheckForCompletedMotions(); return result; } ``` Dispatch table over `MovementType`; unconditionally drains completed motions (§8) after every dispatched call (except the `default`/unknown-type early-return, which skips the drain). ### MovementManager.PerformMovement (L124-157) ```csharp public WeenieError PerformMovement(MovementStruct mvs) { PhysicsObj.set_active(true); switch (mvs.Type) { case MovementType.RawCommand: case MovementType.InterpretedCommand: case MovementType.StopRawCommand: case MovementType.StopInterpretedCommand: case MovementType.StopCompletely: if (MotionInterpreter == null) { MotionInterpreter = MotionInterp.Create(PhysicsObj, WeenieObj); if (PhysicsObj != null) MotionInterpreter.enter_default_state(); } return MotionInterpreter.PerformMovement(mvs); case MovementType.MoveToObject: case MovementType.MoveToPosition: case MovementType.TurnToObject: case MovementType.TurnToHeading: if (MoveToManager == null) MoveToManager = MoveToManager.Create(PhysicsObj, WeenieObj); return MoveToManager.PerformMovement(mvs); default: return WeenieError.GeneralMovementFailure; } } ``` Outer layer: unconditionally activates the physics object (`set_active(true)`) BEFORE dispatch, lazy-constructs `MotionInterpreter` (with `enter_default_state()`) on first use for the motion-command group, lazy-constructs `MoveToManager` on first use for the moveto/turnto group. This is the true entry point most game-logic code calls — `MotionInterp.PerformMovement` is only reachable through this wrapper (or directly if something already holds a `MotionInterp` reference, e.g. via `get_minterp()`). --- ## 11. Lifecycle: enter_default_state / HandleExitWorld / HandleEnterWorld ### MotionInterp.enter_default_state (L604-616) ```csharp public void enter_default_state() { RawState = new RawMotionState(); InterpretedState = new InterpretedMotionState(); PhysicsObj.InitializeMotionTables(); PendingMotions = new LinkedList(); // ?? add_to_queue(0, (uint)MotionCommand.Ready, 0); Initted = true; LeaveGround(); } ``` Note the ACE dev's own `// ??` comment on the `PendingMotions` reset — another self-flagged uncertainty spot (worth checking retail decomp for whether PendingMotions is genuinely reset here or whether retail preserves/asserts-empty). Order: fresh Raw/Interpreted state objects → physics-layer motion table init → fresh pending-motion queue → seed the queue with one `Ready` motion (contextID=0, jumpErrorCode=`WeenieError.None`=0) → flip `Initted=true` → call `LeaveGround()` (§6) which itself is now unlocked since `Initted` gates nothing in LeaveGround directly, but LeaveGround calls `apply_current_movement` which DOES gate on `Initted`. ### MovementManager.EnterDefaultState (L38-46) ```csharp public void EnterDefaultState() { if (PhysicsObj == null) return; if (MotionInterpreter == null) MotionInterpreter = MotionInterp.Create(PhysicsObj, WeenieObj); MotionInterpreter.enter_default_state(); } ``` Thin lazy-construct + delegate wrapper. ### HandleExitWorld — two versions **MotionInterp.HandleExitWorld (L160-173):** ```csharp public void HandleExitWorld() { foreach (var pendingMotion in PendingMotions) { if (PhysicsObj != null && (pendingMotion.Motion & (uint)CommandMask.Action) != 0) { PhysicsObj.unstick_from_object(); InterpretedState.RemoveAction(); RawState.RemoveAction(); } } PendingMotions.Clear(); if (PhysicsObj != null) PhysicsObj.IsAnimating = false; } ``` Iterates ALL pending motions (not just the head, unlike `MotionDone`) — for EVERY pending motion that has the Action bit set, unsticks + pops an action off both state stacks (this will over-pop if multiple pending Action motions exist simultaneously and InterpretedState/RawState only track a bounded action stack — worth checking `GetNumActions()`'s cap of 6 against how many action-removals this loop could trigger). Then clears the whole queue and force-sets `IsAnimating = false` directly (bypassing the `Count > 0` recompute that `MotionDone` uses). **MovementManager.HandleExitWorld (L54-58):** thin delegate, `if (MotionInterpreter != null) MotionInterpreter.HandleExitWorld();` — no MoveToManager involvement (contrast with `HitGround` which drives both). ### MovementManager.HandleEnterWorld (L48-52) ```csharp public void HandleEnterWorld() { //if (MotionInterpreter != null) //NoticeHandler.RecvNotice_PrevSpellSelection(MotionInterpreter); } ``` Entirely commented out — dead/no-op in ACE. The commented reference to `NoticeHandler.RecvNotice_PrevSpellSelection` is a retail-symbol breadcrumb (spell-selection restore notice on world-enter) that ACE chose not to implement. **Flag for acdream: if named-retail decomp confirms this notice is meaningful (e.g. restoring a previously-selected spell/combat-style UI state on relog), this is a genuine ACE gap, not just dead weight — acdream may need to port it from decomp directly since ACE has nothing to copy.** Also note: `MotionTableManager.HandleEnterWorld(Sequence sequence)` (MotionTableManager.cs L103-108) is a DIFFERENT, unrelated `HandleEnterWorld` on a different class — it drains `PendingAnimations` via repeated `AnimationDone(false)` calls and clears link animations on the sequence. Not reachable from `MovementManager.HandleEnterWorld` (which is a no-op) — must be invoked from elsewhere (PartArray or the weenie enter-world path, out of scope). --- ## 12. adjust_motion / apply_run_to_command (raw→interpreted transform helpers) ### adjust_motion (L394-428) ```csharp public void adjust_motion(ref uint motion, ref float speed, HoldKey holdKey) { if (WeenieObj != null && !WeenieObj.IsCreature()) return; switch (motion) { case (uint)MotionCommand.RunForward: return; case (uint)MotionCommand.WalkBackwards: motion = (uint)MotionCommand.WalkForward; speed *= -BackwardsFactor; // -0.65 break; case (uint)MotionCommand.TurnLeft: motion = (uint)MotionCommand.TurnRight; speed *= -1.0f; break; case (uint)MotionCommand.SideStepLeft: motion = (uint)MotionCommand.SideStepRight; speed *= -1.0f; break; } if (motion == (uint)MotionCommand.SideStepRight) speed *= SidestepFactor * (WalkAnimSpeed / SidestepAnimSpeed); // 0.5 * (3.12/1.25) = 1.248 if (holdKey == HoldKey.Invalid) holdKey = RawState.CurrentHoldKey; if (holdKey == HoldKey.Run) apply_run_to_command(ref motion, ref speed); } ``` Canonicalizes "left/backward" variants into their "right/forward" counterparts with negated speed (single-animation-per-axis-direction retail convention: there's no separate WalkBackwards/TurnLeft/SideStepLeft animation state, just the canonical one played at negative speed). `WalkBackwards` gets an EXTRA `-BackwardsFactor` (0.65) scalar on top of the sign flip — backward walking is intentionally slower than forward. `SideStepRight` always gets rescaled by `SidestepFactor * (WalkAnimSpeed/SidestepAnimSpeed)` ≈ 1.248 regardless of hold-key, to convert a walk-speed-denominated input into the sidestep animation's own speed scale. `HoldKey.Invalid` falls back to whatever `RawState.CurrentHoldKey` currently is; if the resolved hold key is `Run`, defers to `apply_run_to_command` for the walk→run promotion. ### apply_run_to_command (L525-562) ```csharp public void apply_run_to_command(ref uint motion, ref float speed) { var speedMod = 1.0f; if (WeenieObj != null) { var runFactor = 0.0f; if (WeenieObj.InqRunRate(ref runFactor)) speedMod = runFactor; else speedMod = MyRunRate; } switch (motion) { case (uint)MotionCommand.WalkForward: if (speed > 0.0f) motion = (uint)MotionCommand.RunForward; speed *= speedMod; break; case (uint)MotionCommand.TurnRight: speed *= RunTurnFactor; // 1.5 break; case (uint)MotionCommand.SideStepRight: speed *= speedMod; if (MaxSidestepAnimRate < Math.Abs(speed)) speed = speed > 0.0f ? MaxSidestepAnimRate : -MaxSidestepAnimRate; break; } } ``` `WalkForward` promotes to `RunForward` ONLY if `speed > 0.0f` — i.e. a negative-speed "walk forward" (which per `adjust_motion` is actually a canonicalized WalkBackwards) does NOT get promoted to running even while the Run hold-key is active; backward movement stays at walk-animation regardless of hold-key. `TurnRight` while running gets a flat 1.5x speed multiplier (turns faster while running). `SideStepRight` gets the run-rate multiplier applied AND is then hard-clamped to ±`MaxSidestepAnimRate` (3.0) — this clamp exists ONLY in the run-path, not in the base `adjust_motion` sidestep scaling, meaning sidestep-while-walking is unclamped but sidestep-while-running is capped. --- ## 13. contact_allows_move / motion_allows_jump / is_standing_still (gating predicates) ### contact_allows_move (L584-602) ```csharp public bool contact_allows_move(uint motion) { if (PhysicsObj == null) return false; if (motion == (uint)MotionCommand.Dead || motion == (uint)MotionCommand.Falling || motion >= (uint)MotionCommand.TurnRight && motion <= (uint)MotionCommand.TurnLeft) return true; if (WeenieObj != null && !WeenieObj.IsCreature()) return true; if (!PhysicsObj.State.HasFlag(PhysicsState.Gravity)) return true; if (!PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact)) return false; if (PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable)) return true; return false; } ``` Always-allowed motion classes: `Dead`, `Falling`, and the [TurnRight..TurnLeft] enum range (turning is always allowed regardless of contact — matches `apply_interpreted_movement`'s independent Turn-branch handling). Non-creature weenies bypass all contact gating. Non-gravity objects bypass gating. Otherwise: requires BOTH `Contact` AND `OnWalkable` transient flags to allow movement; `Contact` without `OnWalkable` (e.g. touching a wall/ceiling, not a floor) explicitly disallows. ### motion_allows_jump (L770-779) ```csharp public WeenieError motion_allows_jump(uint substate) { if (substate >= (uint)MotionCommand.Reload && substate <= (uint)MotionCommand.Pickup || substate >= (uint)MotionCommand.TripleThrustLow && substate <= (uint)MotionCommand.MagicPowerUp07Purple || substate >= (uint)MotionCommand.MagicPowerUp01 && substate <= (uint)MotionCommand.MagicPowerUp10 || substate >= (uint)MotionCommand.Crouch && substate <= (uint)MotionCommand.Sleeping || substate >= (uint)MotionCommand.AimLevel && substate <= (uint)MotionCommand.MagicPray || substate == (uint)MotionCommand.Falling) { return WeenieError.YouCantJumpFromThisPosition; } return WeenieError.None; } ``` Five contiguous MotionCommand enum ranges + one exact match, ALL block jumping: [Reload..Pickup], [TripleThrustLow..MagicPowerUp07Purple], [MagicPowerUp01..MagicPowerUp10], [Crouch..Sleeping], [AimLevel..MagicPray], and exactly `Falling`. This depends entirely on the ORDER of values in the `MotionCommand` enum matching retail's numeric ordering — **any acdream MotionCommand enum that doesn't preserve retail's exact ordinal layout for these ranges will silently break this range-check port.** Called from: `DoInterpretedMotion` (jump-error precompute, twice — against incoming motion AND against ForwardCommand), `StopCompletely` (against ForwardCommand before reset), `jump_is_allowed` (against ForwardCommand), `move_to_interpreted_state` (against ForwardCommand, to gate `allowJump`). ### is_standing_still (L702-708) ```csharp public bool is_standing_still() { return PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact | TransientStateFlags.OnWalkable) && InterpretedState.ForwardCommand == (uint)MotionCommand.Ready && InterpretedState.SideStepCommand == 0 && InterpretedState.TurnCommand == 0; } ``` Same predicate shape as the `StandingLongJump` arm-condition inlined in `charge_jump()` (§7) — grounded+walkable, Ready forward, zero sidestep, zero turn. Not called anywhere within these two files (public API surface for external callers, e.g. WeenieObj/game-logic layer, out of scope). --- ## 14. Misc small methods - **InqStyle (L187-190):** `return InterpretedState.CurrentStyle;` — trivial accessor, returned as `long` despite `CurrentStyle` presumably being `uint` (implicit widening). - **SetHoldKey (L274-287):** only handles `HoldKey.None` explicitly — if the NEW key is `None` AND the current key was `Run`, clears to `None` and re-applies movement; the `HoldKey.Run` case (or others) falls through the switch with NO explicit case, meaning setting the hold key TO `Run` via `SetHoldKey` is a silent no-op unless `key == RawState.CurrentHoldKey` returns early first — the only way `HoldKey.Run` actually gets set appears to be `set_hold_run` (below), not `SetHoldKey`. Early-return guard: `if (key == RawState.CurrentHoldKey) return;` — no-op if unchanged. - **set_hold_run (L826-833):** ```csharp public void set_hold_run(int val, bool cancelMoveTo) { if ((val == 0) != (RawState.CurrentHoldKey != HoldKey.Run)) { RawState.CurrentHoldKey = val != 0 ? HoldKey.Run : HoldKey.None; apply_current_movement(cancelMoveTo, true); } } ``` XOR-style guard: `(val==0) != (CurrentHoldKey != Run)` is true exactly when `val` and the "is currently Run" state disagree, i.e. this is a real toggle. Sets `HoldKey.Run` or `HoldKey.None` (never `Invalid` or others) and re-applies movement with `allowJump=true` unconditionally. - **SetPhysicsObject (L289-293) / SetWeenieObject (L295-299):** both simply assign the field then call `apply_current_movement(true, true)` (force cancelMoveTo=true, allowJump=true) — any (re)binding of either owning object triggers a full movement re-application. - **get_adjusted_max_speed (L618-632):** ```csharp public float get_adjusted_max_speed() { var rate = 1.0f; if (WeenieObj != null) { if (!WeenieObj.InqRunRate(ref rate)) rate = MyRunRate; } if (InterpretedState.ForwardCommand == (uint)MotionCommand.RunForward) rate = InterpretedState.ForwardSpeed / CurrentSpeedFactor; return rate * 4.0f; } ``` Note: if currently running, `rate` is OVERWRITTEN entirely by `ForwardSpeed / CurrentSpeedFactor` — the `WeenieObj.InqRunRate`/`MyRunRate` lookup above becomes dead work in that branch. `4.0f` is presumably `RunAnimSpeed` inlined rather than referencing the constant (magic-number duplication — flag for acdream: use the named constant, don't inline `4.0f`). - **get_max_speed (L665-676):** simpler sibling — `RunAnimSpeed * rate` where `rate` comes from `InqRunRate`/`MyRunRate` fallback, no ForwardCommand override. Two "max speed" methods with different semantics (adjusted = "current effective speed accounting for what's actually playing," max = "theoretical max run speed") — both present, worth checking retail naming/usage split before porting just one. - **motions_pending (L784-787) — MotionInterp:** `return PendingMotions.Count > 0;` with an XML doc comment noting `PhysicsObj.IsAnimating` is the faster equivalent. **PhysicsObj.motions_pending() (PhysicsObj.cs L4244-4247)** is a DIFFERENT, simpler method: `return IsAnimating;` directly — i.e. PhysicsObj's own `motions_pending()` already takes the fast path the MotionInterp doc-comment recommends; **MovementManager.motions_pending() (L195-200)** delegates to `MotionInterpreter.motions_pending()` (the SLOW `Count>0` path, not the fast `IsAnimating` path) when `MotionInterpreter != null`, else returns `false` — so the three `motions_pending()` overloads across PhysicsObj/MovementManager/MotionInterp are NOT all equivalent-cost, and MovementManager's public-facing one takes the slower route. - **move_to_interpreted_state (L789-824):** syncs an externally-supplied `InterpretedMotionState` (e.g. from a network update / DR snapshot) into this instance. Computes `allowJump` from the OLD `ForwardCommand` (before `copy_movement_from` overwrites it) via `motion_allows_jump(...) == WeenieError.None`. Replays the incoming state's `Actions` list through a **sequence-stamp wraparound comparator**: `currentStamp = action.Stamp & 0x7FFF`, `serverStamp = ServerActionStamp & 0x7FFFF` (**note: differing mask widths, `0x7FFF` (15 bits) vs `0x7FFFF` (19 bits) — likely a typo, should probably both be the same mask; flag for retail cross-check**), `deltaStamp = abs(currentStamp - serverStamp)`, and picks a "is this newer" test based on whether the delta exceeds `0x3FFF` (half of 15-bit range) — a classic sequence-number wraparound comparison. Actions only replay if `WeenieObj.IsCreature() || action.Autonomous`. - **ReportExhaustion (L264-272):** ```csharp public void ReportExhaustion() { if (PhysicsObj == null || !Initted) return; if (WeenieObj == null || WeenieObj.IsCreature() && PhysicsObj.movement_is_autonomous()) apply_raw_movement(false, false); else apply_interpreted_movement(false, false); } ``` Same raw-vs-interpreted split condition shape as `apply_current_movement` but with the WeenieObj null-check ADDED to the raw-path condition (here: `WeenieObj==null || (IsCreature && autonomous)` routes to raw; there: `WeenieObj!=null && !IsCreature || !autonomous` routes to interpreted) — these two conditions are NOT exact logical complements of each other across the two methods (worth a careful truth-table comparison before assuming they're meant to be identical dispatch logic). Both calls pass `cancelMoveTo=false, allowJump=false` — exhaustion reporting never cancels an active moveto and never allows a fresh jump. MovementManager.ReportExhaustion (L159-165) is a thin delegate with a dead commented-out `// NoticeHandler::RecvNotice_PrevSpellSelection` line, same as HandleEnterWorld. --- ## 15. MovementManager remaining surface (not covered above) - **CancelMoveTo / HandleUpdateTarget / IsMovingTo / MakeMoveToManager / UseTime:** all thin null-guarded delegates to `MoveToManager` — no MotionInterp involvement. `MakeMoveToManager()` (L112-116) lazy-constructs `MoveToManager` WITHOUT calling any init-state method (contrast with the `MotionInterpreter` lazy-construct sites which always follow with `enter_default_state()`). - **InqInterpretedMotionState / InqRawMotionState / get_minterp:** identical three-line lazy-construct-with-enter_default_state pattern, just returning a different field (`.InterpretedState`, `.RawState`, or the interpreter itself). - **unpack_movement(object addr, uint size) (L213):** empty body — `{ }`. Stub/no-op, presumably a retail network-deserialization entry point ACE doesn't need (server already has structured data, doesn't need to unpack a wire buffer here) or hasn't ported. Flag: if named-retail decomp shows this doing real work, it's client-side-only logic ACE correctly skips (server doesn't need to interpret its own outbound format), OR it's a genuine gap — check the decomp before assuming either way. --- ## Summary table — ACE method inventory vs retail-decomp cross-check TODO | ACE method | File:Line | Retail decomp checked this pass? | |---|---|---| | `add_to_queue` | MotionInterp.cs:388 | No — ACE-only pass | | `MotionDone` | MotionInterp.cs:210 | No | | `DoMotion` | MotionInterp.cs:112 | No | | `DoInterpretedMotion` | MotionInterp.cs:51 | No | | `StopMotion` | MotionInterp.cs:367 | No | | `StopInterpretedMotion` | MotionInterp.cs:329 | No | | `StopCompletely` | MotionInterp.cs:301 | No | | `HitGround` | MotionInterp.cs:175 | No | | `LeaveGround` | MotionInterp.cs:192 | No | | `jump` | MotionInterp.cs:710 | No | | `jump_is_allowed` | MotionInterp.cs:742 | No — **flag: possible null-guard typo L747** | | `jump_charge_is_allowed` | MotionInterp.cs:729 | No | | `charge_jump` | MotionInterp.cs:564 | No — **flag: Falling/Fallen + range-inclusivity mismatch vs jump_charge_is_allowed** | | `get_jump_v_z` | MotionInterp.cs:634 | No | | `get_leave_ground_velocity` | MotionInterp.cs:654 | No | | `enter_default_state` | MotionInterp.cs:604 | No — **flag: `// ??` on PendingMotions reset** | | `apply_raw_movement` | MotionInterp.cs:506 | No | | `apply_interpreted_movement` | MotionInterp.cs:440 | No — **flag: inline PhysicsObj.StopInterpretedMotion duplication in Turn-stop branch** | | `apply_current_movement` | MotionInterp.cs:430 | No | | `adjust_motion` | MotionInterp.cs:394 | No | | `apply_run_to_command` | MotionInterp.cs:525 | No | | `contact_allows_move` | MotionInterp.cs:584 | No | | `motion_allows_jump` | MotionInterp.cs:770 | No — **depends on MotionCommand enum ordinal layout matching retail exactly** | | `move_to_interpreted_state` | MotionInterp.cs:789 | No — **flag: 0x7FFF vs 0x7FFFF mask width mismatch** | | `MovementManager.PerformMovement` | MovementManager.cs:124 | No | | `MovementManager.HandleEnterWorld` | MovementManager.cs:48 | No — **dead/commented; retail NoticeHandler::RecvNotice_PrevSpellSelection not ported** | | `MotionTableManager.AnimationDone` | MotionTableManager.cs:28 | No | | `MotionTableManager.CheckForCompletedMotions` | MotionTableManager.cs:63 | No | All rows above are candidates for Step 0 (`grep named-retail/acclient_2013_pseudo_c.txt` by `CMotionInterp::` — note retail almost certainly uses the `C`-prefixed class name ACE dropped) before any acdream port work proceeds, per CLAUDE.md's mandatory workflow.