acdream/docs/research/2026-07-02-r3-motioninterp/r3-ace-motioninterp.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

58 KiB

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)

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<MotionNode> 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<MotionNode>, 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

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)

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

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

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)

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)

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)

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)

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)

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)

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)

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)

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)

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)

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<AnimNode>, 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.PerformMovementMotionInterp.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

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)

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)

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)

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)

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)

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)

public void enter_default_state()
{
    RawState = new RawMotionState();
    InterpretedState = new InterpretedMotionState();

    PhysicsObj.InitializeMotionTables();
    PendingMotions = new LinkedList<MotionNode>();  // ??

    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)

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):

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)

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)

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)

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)

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)

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)

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):
    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):
    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):
    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::<method> — note retail almost certainly uses the C-prefixed class name ACE dropped) before any acdream port work proceeds, per CLAUDE.md's mandatory workflow.