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>
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:
PendingMotionsisLinkedList<MotionNode>, not an array/ring buffer — ACE reimplemented retail's pending-motion queue as a doubly-linked list.MotionNode(see §2) carriesContextID,Motion,JumpErrorCode— this is the FIFO queue of in-flight interpreted motions waiting on animation completion.CurrentSpeedFactoris declared but the only read site in this file isget_adjusted_max_speed()(L629) — no write site anywhere in MotionInterp.cs. Likely set externally (WeenieObj or PhysicsObj) — not visible in this file.JumpExtentdoubles as both "how hard is the current jump charging" (set injump(), read incharge_jump()/get_jump_v_z()) and as a guard the WeenieObj layer checks viaCanJump(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_codeprecomputed at L73-83.StopCompletely(L321) — with hardcodedMotionCommand.Readyand ajumpprecomputed at L307.StopInterpretedMotion(L348) — with hardcodedMotionCommand.ReadyandWeenieError.None.apply_interpreted_movement(L495) — the turn-stop fallback branch, hardcodedMotionCommand.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:
- If the head's Motion has the
Actionbit 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. - Re-fetches
PendingMotions.Firsta second time (redundant re-read —motionDataunchanged since nothing between the two reads mutates the list) before removing it and recomputingIsAnimatingasCount > 0(as opposed toadd_to_queue's unconditionaltrue).
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 originalmotionparameter, notcurrentMotion(post-adjust_motionmutation) — combat-block checks happen on the raw pre-adjustment command.- Hardcoded action cap of 6 concurrent actions (
GetNumActions() >= 6). RawState.ApplyMotionuses the originalmotion, notcurrentMotion— raw state records what was actually requested, interpreted state (viaDoInterpretedMotion) 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:
- Gate on
contact_allows_move(motion)(§7) — governs whether the object is grounded enough to accept this motion at all. - 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.DoInterpretedMotioncall, 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. - Otherwise:
Deadmotion clears link animations first; then delegates the actual animation dispatch toPhysicsObj.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 incomingmotionitself, then — only if the incoming motion isn't itself an Action AND passed — against the currentForwardCommand), queues it, and conditionally applies to InterpretedState. - 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. - 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): callsMotionInterpreter.HitGround()THENMoveToManager.HitGround()— both interpreters get the hit-ground signal, MotionInterp first.LeaveGround()(L104-110): callsMotionInterpreter.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) →GeneralMovementFailureif 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.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
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:
CurrentStylemotion (stance) always re-dispatched first.MyRunRatecache updated fromForwardSpeedwhenever currently running (side-channel — persists the "last known run speed" independent of whatever WeenieObj.InqRunRate later reports).- 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).
- If contact does NOT allow movement: unconditionally dispatches
Falling(Speed=1.0) regardless of what ForwardCommand was. - 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, notthis.StopInterpretedMotion— and duplicates the add_to_queue/RemoveMotion/RemoveLinkAnimations bookkeeping inline rather than delegating tothis.StopInterpretedMotion). This inline duplication vs delegating is a structural oddity worth flagging — every other "stop with bookkeeping" path in this file goes through the memberStopInterpretedMotionmethod, but this one open-codes it againstPhysicsObj.StopInterpretedMotiondirectly.
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 aslongdespiteCurrentStylepresumably beinguint(implicit widening). - SetHoldKey (L274-287): only handles
HoldKey.Noneexplicitly — if the NEW key isNoneAND the current key wasRun, clears toNoneand re-applies movement; theHoldKey.Runcase (or others) falls through the switch with NO explicit case, meaning setting the hold key TORunviaSetHoldKeyis a silent no-op unlesskey == RawState.CurrentHoldKeyreturns early first — the only wayHoldKey.Runactually gets set appears to beset_hold_run(below), notSetHoldKey. Early-return guard:if (key == RawState.CurrentHoldKey) return;— no-op if unchanged. - set_hold_run (L826-833):
XOR-style guard: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); } }(val==0) != (CurrentHoldKey != Run)is true exactly whenvaland the "is currently Run" state disagree, i.e. this is a real toggle. SetsHoldKey.RunorHoldKey.None(neverInvalidor others) and re-applies movement withallowJump=trueunconditionally. - 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):
Note: if currently running,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; }rateis OVERWRITTEN entirely byForwardSpeed / CurrentSpeedFactor— theWeenieObj.InqRunRate/MyRunRatelookup above becomes dead work in that branch.4.0fis presumablyRunAnimSpeedinlined rather than referencing the constant (magic-number duplication — flag for acdream: use the named constant, don't inline4.0f). - get_max_speed (L665-676): simpler sibling —
RunAnimSpeed * ratewhereratecomes fromInqRunRate/MyRunRatefallback, 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 notingPhysicsObj.IsAnimatingis the faster equivalent. PhysicsObj.motions_pending() (PhysicsObj.cs L4244-4247) is a DIFFERENT, simpler method:return IsAnimating;directly — i.e. PhysicsObj's ownmotions_pending()already takes the fast path the MotionInterp doc-comment recommends; MovementManager.motions_pending() (L195-200) delegates toMotionInterpreter.motions_pending()(the SLOWCount>0path, not the fastIsAnimatingpath) whenMotionInterpreter != null, else returnsfalse— so the threemotions_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. ComputesallowJumpfrom the OLDForwardCommand(beforecopy_movement_fromoverwrites it) viamotion_allows_jump(...) == WeenieError.None. Replays the incoming state'sActionslist through a sequence-stamp wraparound comparator:currentStamp = action.Stamp & 0x7FFF,serverStamp = ServerActionStamp & 0x7FFFF(note: differing mask widths,0x7FFF(15 bits) vs0x7FFFF(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 exceeds0x3FFF(half of 15-bit range) — a classic sequence-number wraparound comparison. Actions only replay ifWeenieObj.IsCreature() || action.Autonomous. - ReportExhaustion (L264-272):
Same raw-vs-interpreted split condition shape aspublic 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); }apply_current_movementbut with the WeenieObj null-check ADDED to the raw-path condition (here:WeenieObj==null || (IsCreature && autonomous)routes to raw; there:WeenieObj!=null && !IsCreature || !autonomousroutes 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 passcancelMoveTo=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_PrevSpellSelectionline, 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-constructsMoveToManagerWITHOUT calling any init-state method (contrast with theMotionInterpreterlazy-construct sites which always follow withenter_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.