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

987 lines
58 KiB
Markdown

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