using System;
using System.Numerics;
namespace AcDream.Core.Physics;
// ─────────────────────────────────────────────────────────────────────────────
// MotionInterpreter — C# port of CMotionInterp from acclient.exe (chunk_00520000.c).
//
// Source addresses (chunk_00520000.c):
// FUN_00529a90 PerformMovement — top-level dispatcher (switch 1-5)
// FUN_00529930 DoMotion — process one raw motion command
// FUN_00528a50 StopCompletely — reset to Ready/idle
// FUN_00528960 get_state_velocity — compute world-space velocity for current motion
// FUN_00529210 apply_current_movement — apply interpreted motion as velocity
// FUN_00529390 jump — initiate jump: validate, record extent, leave ground
// FUN_005286b0 get_jump_v_z — get vertical jump velocity
// FUN_00528cd0 get_leave_ground_velocity — compose full 3D launch vector
// FUN_00528ec0 jump_is_allowed — can we jump?
// FUN_00528dd0 contact_allows_move — slope angle / contact state check
//
// Cross-checked against ACE MotionInterp.cs.
// ─────────────────────────────────────────────────────────────────────────────
// ── Motion command constants (from retail dat / wire protocol) ────────────────
///
/// Raw AC motion command IDs used in CMotionInterp.
/// Values sourced from the decompiled comparisons in chunk_00520000.c and
/// confirmed against ACE's MotionCommand enum.
///
public static class MotionCommand
{
/// 0x41000003 — idle/default state.
public const uint Ready = 0x41000003u;
/// 0x45000005 — walk forward.
public const uint WalkForward = 0x45000005u;
/// 0x44000007 — run forward.
public const uint RunForward = 0x44000007u;
/// 0x45000006 — walk backward.
public const uint WalkBackward = 0x45000006u;
/// 0x6500000D — turn right.
public const uint TurnRight = 0x6500000Du;
/// 0x6500000E — turn left.
public const uint TurnLeft = 0x6500000Eu;
/// 0x6500000F — sidestep right.
public const uint SideStepRight = 0x6500000Fu;
/// 0x65000010 — sidestep left.
public const uint SideStepLeft = 0x65000010u;
/// 0x40000008 — Fallen (lying on ground).
public const uint Fallen = 0x40000008u;
///
/// 0x40000015 — Falling (SubState). The airborne cycle. Retail's
/// MotionTable has Links from RunForward/Ready/WalkForward → Falling,
/// and a Cycles entry for (style, Falling) that loops while the body
/// is in the air. Swap via
/// when airborne; swap back to Ready/WalkForward/RunForward on land.
///
public const uint Falling = 0x40000015u;
///
/// 0x2500003B — Jump (Modifier flag). NOT an animation trigger; retail
/// uses this as a state flag internally. Kept for future use.
///
public const uint Jump = 0x2500003Bu;
///
/// 0x1000004B — Jumpup (Action). Not present in the humanoid player
/// motion table's Links dict (empirically verified). Retail uses the
/// Falling SubState for airborne animation instead.
///
public const uint Jumpup = 0x1000004Bu;
///
/// 0x10000050 — FallDown (Action). Same story as Jumpup; not in the
/// humanoid motion table's Links. Landing returns to Ready via the
/// regular SetCycle transition.
///
public const uint FallDown = 0x10000050u;
/// 0x40000011 - persistent dead substate.
public const uint Dead = 0x40000011u;
/// 0x10000057 - Sanctuary death-trigger action.
public const uint Sanctuary = 0x10000057u;
/// 0x41000012 - crouching substate.
public const uint Crouch = 0x41000012u;
/// 0x41000013 - sitting substate.
public const uint Sitting = 0x41000013u;
/// 0x41000014 - sleeping substate.
public const uint Sleeping = 0x41000014u;
/// 0x41000011 — Crouch lower bound for blocked-jump check.
public const uint CrouchLowerBound = 0x41000011u;
/// 0x41000015 - exclusive upper bound of crouch/sit/sleep range.
public const uint CrouchUpperExclusive = 0x41000015u;
}
///
/// Movement type passed in PerformMovement's switch statement.
/// Matches the 5-case switch at FUN_00529a90.
///
public enum MovementType
{
/// case 1 — raw motion command (DoMotion).
RawCommand = 1,
/// case 2 — interpreted motion command (DoInterpretedMotion).
InterpretedCommand = 2,
/// case 3 — stop raw motion (StopMotion).
StopRawCommand = 3,
/// case 4 — stop interpreted motion (StopInterpretedMotion).
StopInterpretedCommand = 4,
/// case 5 — stop completely (StopCompletely).
StopCompletely = 5,
}
///
/// WeenieError codes returned by CMotionInterp methods.
/// Values are the hex constants used directly in the decompiled C code.
///
public enum WeenieError : uint
{
/// 0x00 — success.
None = 0x00,
/// 0x08 — PhysicsObj is null.
NoPhysicsObject = 0x08,
/// 0x24 — general movement failure.
GeneralMovementFailure = 0x24,
/// 0x47 — cannot jump from this position (motion state blocks it).
YouCantJumpFromThisPosition = 0x47,
/// 0x48 — cannot jump while in the air.
YouCantJumpWhileInTheAir = 0x48,
/// 0x49 — loaded down / weenie blocked the jump.
CantJumpLoadedDown = 0x49,
}
// ── Motion state structs ───────────────────────────────────────────────────────
///
/// Raw (network-derived) motion state for the local player.
/// Struct layout in chunk_00520000 starts at offset +0x14 (struct field +0x20 =
/// ForwardCommand, +0x28 = ForwardSpeed, etc.).
///
public struct RawMotionState
{
/// Forward/backward motion command (offset +0x20).
public uint ForwardCommand;
/// Speed scalar for forward motion (offset +0x28).
public float ForwardSpeed;
/// Sidestep command (offset +0x2C).
public uint SideStepCommand;
/// Speed scalar for sidestep (offset +0x34, inferred from ACE).
public float SideStepSpeed;
/// Turn command (offset +0x38).
public uint TurnCommand;
/// Speed scalar for turn (offset +0x40, inferred).
public float TurnSpeed;
/// Initialize to the idle/ready state (1.0 speed, Ready command).
public static RawMotionState Default() => new()
{
ForwardCommand = MotionCommand.Ready,
ForwardSpeed = 1.0f,
SideStepCommand = 0,
SideStepSpeed = 1.0f,
TurnCommand = 0,
TurnSpeed = 1.0f,
};
}
///
/// Interpreted motion state, derived from the raw state.
/// Struct layout: starts at offset +0x44 (ForwardCommand at +0x4C, ForwardSpeed at +0x50).
///
public struct InterpretedMotionState
{
/// Forward/backward interpreted command (offset +0x4C).
public uint ForwardCommand;
/// Speed scalar for interpreted forward motion (offset +0x50).
public float ForwardSpeed;
/// Sidestep interpreted command (offset +0x54).
public uint SideStepCommand;
/// Speed scalar for interpreted sidestep (offset +0x58).
public float SideStepSpeed;
/// Turn interpreted command (offset +0x5C).
public uint TurnCommand;
/// Speed scalar for turn (offset +0x60).
public float TurnSpeed;
/// Initialize to the idle/ready state.
public static InterpretedMotionState Default() => new()
{
ForwardCommand = MotionCommand.Ready,
ForwardSpeed = 1.0f,
SideStepCommand = 0,
SideStepSpeed = 1.0f,
TurnCommand = 0,
TurnSpeed = 1.0f,
};
}
///
/// Lightweight struct passed into PerformMovement.
/// Fields correspond to what the retail dispatcher read from param_1 (the movement packet struct).
///
public struct MovementStruct
{
/// Which of the 5 motion types to dispatch.
public MovementType Type;
/// Motion command ID (e.g. WalkForward).
public uint Motion;
/// Speed scalar for this motion.
public float Speed;
/// Autonomous (player-initiated) flag.
public bool Autonomous;
/// Whether to modify the interpreted state.
public bool ModifyInterpretedState;
/// Whether to modify the raw state.
public bool ModifyRawState;
}
// ── Optional WeenieObject interface ──────────────────────────────────────────
///
/// Minimal interface for the server-side WeenieObject callbacks that CMotionInterp
/// reaches through at vtable offsets +0x30, +0x34, +0x3C.
/// Allows testing without a real weenie.
///
public interface IWeenieObject
{
/// vtable +0x30 — InqJumpVelocity. Returns true and sets vz if valid.
bool InqJumpVelocity(float extent, out float vz);
/// vtable +0x34 — InqRunRate. Returns true and sets rate if valid.
bool InqRunRate(out float rate);
/// vtable +0x3C — CanJump. Returns true if the weenie can jump at this extent.
bool CanJump(float extent);
}
// ── MotionInterpreter ─────────────────────────────────────────────────────────
///
/// C# port of CMotionInterp (chunk_00520000.c).
///
/// Owns the raw and interpreted motion states for a physics object and
/// translates network movement commands into PhysicsBody velocity calls.
///
public sealed class MotionInterpreter
{
// ── animation speed constants (from ACE / confirmed by decompile globals) ─
/// Walk animation base speed (_DAT_007c96e4 family).
public const float WalkAnimSpeed = 3.12f;
/// Run animation base speed (_DAT_007c96e0 family).
public const float RunAnimSpeed = 4.0f;
/// Sidestep animation base speed (_DAT_007c96e8 family).
public const float SidestepAnimSpeed = 1.25f;
/// Minimum jump extent before get_jump_v_z bothers computing (_DAT_007c9734).
public const float JumpExtentEpsilon = 0.001f;
/// Fallback vertical jump velocity when WeenieObj is absent (_DAT_0079c6d4).
public const float DefaultJumpVz = 10.0f;
/// Maximum jump extent clamped by get_jump_v_z (_DAT_007938b0 = 1.0f).
public const float MaxJumpExtent = 1.0f;
// ── fields (matching struct layout from acclient_function_map.md) ─────────
/// The physics body this interpreter controls (struct offset +0x08).
public PhysicsBody? PhysicsObj { get; set; }
/// Optional WeenieObject for stamina / run-rate queries (struct offset +0x04).
public IWeenieObject? WeenieObj { get; set; }
/// Raw (network-derived) motion state (struct offsets +0x14..+0x44).
public RawMotionState RawState;
/// Interpreted motion state derived from raw (struct offsets +0x44..+0x7C).
public InterpretedMotionState InterpretedState;
/// Jump charge accumulator — set in jump(), cleared in LeaveGround() (offset +0x74).
public float JumpExtent;
/// Stored run rate from last successful InqRunRate call (offset +0x7C).
public float MyRunRate = 1.0f;
/// True when crouching-in-place for a standing long jump (offset +0x70).
public bool StandingLongJump;
///
/// Optional accessor for the owning entity's current animation cycle
/// velocity (AnimationSequencer.CurrentVelocity, i.e. MotionData.Velocity
/// scaled by speedMod). When wired,
/// uses it as the primary forward-axis drive instead of the hardcoded
/// / constants.
///
///
/// Why: the decompiled get_state_velocity (FUN_00528960)
/// literally computes RunAnimSpeed * ForwardSpeed. That works in
/// retail because retail's Humanoid MotionTable happens to bake
/// MotionData.Velocity == RunAnimSpeed (4.0) for the RunForward
/// cycle — so the constant and the dat data agree. For MotionTables
/// where they disagree (other creatures; swapped weapon-style cycles;
/// modded dats), the constant causes the body's world velocity to
/// drift away from the animation's baked-in root-motion velocity,
/// producing the classic "legs cycle too slowly for how fast the body
/// is sliding" visual bug.
///
///
///
/// Per docs/research/deepdives/r03-motion-animation.md §1.3,
/// the retail animation pipeline treats MotionData.Velocity *
/// speedMod as the canonical per-cycle world velocity. The
/// constant survives in our port only as
/// the max-speed clamp (see below), which matches the decompile's
/// if (|velocity| > RunAnimSpeed * rate) guard.
///
///
///
/// Call site: PlayerMovementController.AttachCycleVelocityAccessor
/// wires this to AnimatedEntity.Sequencer.CurrentVelocity once
/// the player's sequencer is built. Null = fall back to the decompiled
/// constant-based path (used by tests and by any physics body with
/// no sequencer).
///
///
public Func? GetCycleVelocity { get; set; }
// ── constructor ────────────────────────────────────────────────────────────
public MotionInterpreter()
{
RawState = RawMotionState.Default();
InterpretedState = InterpretedMotionState.Default();
}
public MotionInterpreter(PhysicsBody physicsObj, IWeenieObject? weenieObj = null)
{
PhysicsObj = physicsObj;
WeenieObj = weenieObj;
RawState = RawMotionState.Default();
InterpretedState = InterpretedMotionState.Default();
}
// ── FUN_00529a90 — PerformMovement ────────────────────────────────────────
///
/// Top-level dispatcher for a network movement struct.
///
/// Decompiled logic (FUN_00529a90):
/// switch(*param_1) { ← MovementStruct.Type
/// case 1: DoMotion(…) → raw command
/// case 2: DoInterpretedMotion(…)
/// case 3: StopMotion(…)
/// case 4: StopInterpretedMotion(…)
/// case 5: StopCompletely()
/// default: return 0x47
/// }
/// FUN_00510900() — CheckForCompletedMotions (animation flush, not simulated here)
///
public WeenieError PerformMovement(MovementStruct mvs)
{
WeenieError result = mvs.Type switch
{
MovementType.RawCommand => DoMotion(mvs.Motion, mvs.Speed),
MovementType.InterpretedCommand => DoInterpretedMotion(mvs.Motion, mvs.Speed, mvs.ModifyInterpretedState),
MovementType.StopRawCommand => StopMotion(mvs.Motion),
MovementType.StopInterpretedCommand => StopInterpretedMotion(mvs.Motion, mvs.ModifyInterpretedState),
MovementType.StopCompletely => StopCompletely(),
_ => WeenieError.GeneralMovementFailure,
};
// FUN_00510900 — CheckForCompletedMotions is an animation system flush;
// no simulation state to update here.
return result;
}
// ── FUN_00529930 — DoMotion ───────────────────────────────────────────────
///
/// Process one raw motion command from a network packet.
///
/// Decompiled logic (FUN_00529930):
/// Copy packet fields into local variables (at local_24..local_4).
/// If the speed byte in flags is negative → call FUN_00510cc0 (cancel moveto).
/// If 0x800 flag → FUN_005297c0 (set hold key from packet).
/// FUN_00528c20 — adjust_motion (raw→interpreted adjustments).
/// Guard against special mid-animation states (returns 0x3F/0x40/0x41/0x42).
/// If Action bit (0x10000000) set and num_actions ≥ 6 → return 0x45.
/// Call DoInterpretedMotion(motion, movementParams).
///
/// Our simplified port focuses on the state fields and physics side-effects.
///
public WeenieError DoMotion(uint motion, float speed = 1.0f)
{
if (PhysicsObj is null)
return WeenieError.NoPhysicsObject;
// Record the new raw forward command and speed.
// In the decompile, local_24 = *(param_3+8) = ForwardCommand,
// local_18 = ForwardSpeed, etc.
RawState.ForwardCommand = motion;
RawState.ForwardSpeed = speed;
// Delegate to the interpreted path. DoMotion ultimately calls
// DoInterpretedMotion after adjust_motion in the retail client.
return DoInterpretedMotion(motion, speed, modifyInterpretedState: true);
}
// ── DoInterpretedMotion ────────────────────────────────────────────────────
///
/// Core animation-state-machine entry point (FUN_00528f70).
///
/// In the full retail engine this runs the animation sequencer. In this
/// physics-only port we update the InterpretedState and call
/// apply_current_movement so that the velocity is immediately reflected.
///
public WeenieError DoInterpretedMotion(uint motion, float speed = 1.0f, bool modifyInterpretedState = false)
{
if (PhysicsObj is null)
return WeenieError.NoPhysicsObject;
if (!contact_allows_move(motion))
{
// Action commands (bit 0x10000000) are blocked mid-air.
if ((motion & 0x10000000u) != 0)
return WeenieError.YouCantJumpWhileInTheAir;
// Non-action motions are queued silently; state still updates.
}
if (modifyInterpretedState)
ApplyMotionToInterpretedState(motion, speed);
apply_current_movement(cancelMoveTo: false, allowJump: true);
return WeenieError.None;
}
// ── StopMotion ────────────────────────────────────────────────────────────
///
/// Stop a specific raw motion (FUN_00529140 → StopInterpretedMotion).
///
public WeenieError StopMotion(uint motion)
{
if (PhysicsObj is null)
return WeenieError.NoPhysicsObject;
if (RawState.ForwardCommand == motion)
{
RawState.ForwardCommand = MotionCommand.Ready;
RawState.ForwardSpeed = 1.0f;
}
if (RawState.SideStepCommand == motion)
{
RawState.SideStepCommand = 0;
RawState.SideStepSpeed = 1.0f;
}
if (RawState.TurnCommand == motion)
{
RawState.TurnCommand = 0;
RawState.TurnSpeed = 1.0f;
}
return StopInterpretedMotion(motion, modifyInterpretedState: true);
}
// ── StopInterpretedMotion ────────────────────────────────────────────────
///
/// Stop a specific interpreted motion (FUN_00529080).
///
public WeenieError StopInterpretedMotion(uint motion, bool modifyInterpretedState = false)
{
if (PhysicsObj is null)
return WeenieError.NoPhysicsObject;
if (modifyInterpretedState)
{
if (InterpretedState.ForwardCommand == motion)
{
InterpretedState.ForwardCommand = MotionCommand.Ready;
InterpretedState.ForwardSpeed = 1.0f;
}
if (InterpretedState.SideStepCommand == motion)
{
InterpretedState.SideStepCommand = 0;
InterpretedState.SideStepSpeed = 1.0f;
}
if (InterpretedState.TurnCommand == motion)
{
InterpretedState.TurnCommand = 0;
InterpretedState.TurnSpeed = 1.0f;
}
}
apply_current_movement(cancelMoveTo: false, allowJump: false);
return WeenieError.None;
}
// ── FUN_00528a50 — StopCompletely ─────────────────────────────────────────
///
/// Reset both raw and interpreted states to Ready/idle, then push zero velocity.
///
/// Decompiled logic (FUN_00528a50):
/// if (PhysicsObj == null) return 8
/// FUN_00510cc0() — cancel moveto
/// uVar1 = FUN_005285e0(InterpretedState.ForwardCommand) — motion_allows_jump
/// *(+0x20) = 0x41000003 (RawState.ForwardCommand = Ready)
/// *(+0x28) = 0x3f800000 (RawState.ForwardSpeed = 1.0f)
/// *(+0x2c) = 0 (RawState.SideStepCommand = 0)
/// *(+0x38) = 0 (RawState.TurnCommand = 0)
/// *(+0x4c) = 0x41000003 (InterpretedState.ForwardCommand = Ready)
/// *(+0x50) = 0x3f800000 (InterpretedState.ForwardSpeed = 1.0f)
/// *(+0x54) = 0 (InterpretedState.SideStepCommand = 0)
/// *(+0x5c) = 0 (InterpretedState.TurnCommand = 0)
/// FUN_0050f5a0() — StopCompletely_Internal (zero velocity on PhysicsObj)
/// FUN_00528790(…) — add_to_queue
/// if (PhysicsObj != null && CurCell == null) → FUN_005108f0 (RemoveLinkAnimations)
/// return 0
///
public WeenieError StopCompletely()
{
if (PhysicsObj is null)
return WeenieError.NoPhysicsObject;
// Reset raw state
RawState.ForwardCommand = MotionCommand.Ready;
RawState.ForwardSpeed = 1.0f;
RawState.SideStepCommand = 0;
RawState.SideStepSpeed = 1.0f;
RawState.TurnCommand = 0;
RawState.TurnSpeed = 1.0f;
// Reset interpreted state
InterpretedState.ForwardCommand = MotionCommand.Ready;
InterpretedState.ForwardSpeed = 1.0f;
InterpretedState.SideStepCommand = 0;
InterpretedState.SideStepSpeed = 1.0f;
InterpretedState.TurnCommand = 0;
InterpretedState.TurnSpeed = 1.0f;
// Zero the body velocity (FUN_0050f5a0 = StopCompletely_Internal)
PhysicsObj.set_velocity(Vector3.Zero);
return WeenieError.None;
}
// ── FUN_00528960 — get_state_velocity ────────────────────────────────────
///
/// Compute the body-local velocity vector for the current interpreted motion.
///
///
/// Decompiled path (FUN_00528960):
///
/// velocity = (0, 0, 0)
/// if InterpretedState.SideStepCommand == 0x6500000F:
/// velocity.X = _DAT_007c96e8 * InterpretedState.SideStepSpeed
/// = SidestepAnimSpeed * SideStepSpeed
/// if InterpretedState.ForwardCommand == 0x45000005 (WalkForward):
/// velocity.Y = _DAT_007c96e4 * InterpretedState.ForwardSpeed
/// = WalkAnimSpeed * ForwardSpeed
/// elif InterpretedState.ForwardCommand == 0x44000007 (RunForward):
/// velocity.Y = _DAT_007c96e0 * InterpretedState.ForwardSpeed
/// = RunAnimSpeed * ForwardSpeed
/// rate = InqRunRate() or MyRunRate
/// maxSpeed = RunAnimSpeed * rate
/// if |velocity| > maxSpeed: velocity = normalize(velocity) * maxSpeed
/// return velocity
///
///
///
///
/// Option B — MotionData-sourced forward velocity:
/// when is wired (i.e. the owning
/// entity has an AnimationSequencer), we prefer
/// MotionData.Velocity.Y * speedMod over the hardcoded
/// / constants.
/// This keeps the body's world velocity locked to the animation's
/// baked-in root-motion velocity (r03 §1.3), so the
/// legs-per-meter ratio is invariant regardless of which motion table
/// drives the character. The decompiled constant was a
/// MotionTable-specific value that happens to equal the Humanoid
/// RunForward MotionData.Velocity — fine as a fallback, but the dat
/// is the ground truth for any non-humanoid creature.
///
///
///
/// The constant survives as the max-speed
/// clamp at the bottom, faithfully matching the decompile's
/// if (|velocity| > RunAnimSpeed * rate) guard. Sidestep
/// continues to use because the
/// sequencer only tracks the current forward cycle — strafe is
/// implemented as a separate axis in our controller (see
/// PlayerMovementController.Update).
///
///
public Vector3 get_state_velocity()
{
var velocity = Vector3.Zero;
if (InterpretedState.SideStepCommand == MotionCommand.SideStepRight)
velocity.X = SidestepAnimSpeed * InterpretedState.SideStepSpeed;
// Forward axis — prefer sequencer's current cycle velocity when available.
// Sequencer's CurrentVelocity is already `MotionData.Velocity * speedMod`
// where speedMod == ForwardSpeed for locomotion cycles, so we use it as-is
// (no additional ForwardSpeed multiplication). Fall back to the decompiled
// constant-based path when the accessor is unwired or returns zero Y
// (e.g. during zero-velocity link transitions — in which case the constant
// is the safe default to keep physics moving at ForwardSpeed).
Vector3? cycleVel = GetCycleVelocity?.Invoke();
bool haveCycleForward = cycleVel.HasValue
&& MathF.Abs(cycleVel.Value.Y) > float.Epsilon;
if (InterpretedState.ForwardCommand == MotionCommand.WalkForward)
{
velocity.Y = haveCycleForward
? cycleVel!.Value.Y
: WalkAnimSpeed * InterpretedState.ForwardSpeed;
}
else if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
{
velocity.Y = haveCycleForward
? cycleVel!.Value.Y
: RunAnimSpeed * InterpretedState.ForwardSpeed;
}
// Determine the current run rate via WeenieObj or fall back to MyRunRate.
// Decompile: calls vtable+0x34 (InqRunRate).
float rate = MyRunRate;
if (WeenieObj is not null)
{
if (WeenieObj.InqRunRate(out float queried))
rate = queried;
// else: rate stays MyRunRate
}
float maxSpeed = RunAnimSpeed * rate;
float len = velocity.Length();
if (len > maxSpeed && len > 0f)
{
velocity = Vector3.Normalize(velocity) * maxSpeed;
}
return velocity;
}
// ── FUN_00529210 — apply_current_movement ─────────────────────────────────
///
/// Apply the current interpreted motion state as a local velocity to the PhysicsBody.
///
/// Decompiled logic (FUN_00529210):
/// if PhysicsObj == null: return
/// FUN_00524f80() — internal animation state update
/// If ForwardCommand == RunForward: update MyRunRate = ForwardSpeed
/// Then delegates to DoInterpretedMotion for each active command,
/// which ultimately calls set_local_velocity via FUN_00528960.
///
/// In our physics-only port we compute the body-local velocity via
/// get_state_velocity() and push it directly to PhysicsBody.set_local_velocity.
///
public void apply_current_movement(bool cancelMoveTo, bool allowJump)
{
if (PhysicsObj is null)
return;
// Decompile writes back MyRunRate when in run state (offset +0x7C).
if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
MyRunRate = InterpretedState.ForwardSpeed;
// Only replace velocity when grounded. While airborne, the physics
// body's integrated velocity (from LeaveGround) should persist —
// gravity pulls Z down, horizontal momentum is preserved.
// The retail client's apply_current_movement also gates on Contact+OnWalkable
// before writing velocity (the full decompiled flow routes through
// update_object which checks transient state).
if (PhysicsObj.OnWalkable)
{
var localVelocity = get_state_velocity();
PhysicsObj.set_local_velocity(localVelocity);
}
}
// ── FUN_00529390 — jump ───────────────────────────────────────────────────
///
/// Initiate a jump: validate, store jump extent, leave the ground.
///
/// Decompiled logic (FUN_00529390):
/// if (PhysicsObj == null) return 8
/// FUN_00510cc0() — cancel moveto
/// iVar1 = FUN_00528ec0(extent, stamina) ← jump_is_allowed
/// if (iVar1 == 0):
/// *(+0x74) = extent ← JumpExtent
/// FUN_00511de0(0) ← PhysicsObj.set_on_walkable(false)
/// return 0
/// *(+0x70) = 0 ← StandingLongJump = false
/// return iVar1
///
public WeenieError jump(float extent, int adjustStamina = 0)
{
if (PhysicsObj is null)
return WeenieError.NoPhysicsObject;
var result = jump_is_allowed(extent, adjustStamina);
if (result == WeenieError.None)
{
JumpExtent = extent;
PhysicsObj.set_on_walkable(false);
return WeenieError.None;
}
StandingLongJump = false;
return result;
}
// ── FUN_005286b0 — get_jump_v_z ──────────────────────────────────────────
///
/// Get the vertical (Z) component of jump velocity.
///
/// Decompiled logic (FUN_005286b0):
/// local_4 = *(+0x74) ← JumpExtent
/// if local_4 < _DAT_007c9734 (epsilon): return _DAT_00796344 (0.0)
/// if local_4 > _DAT_007938b0 (1.0): local_4 = 1.0
/// if WeenieObj == null: return _DAT_0079c6d4 (10.0) — default jump v_z
/// cVar1 = InqJumpVelocity(local_4, &local_4) — vtable +0x30
/// if (cVar1 != 0): return local_4
/// return _DAT_00796344 (0.0)
///
public float get_jump_v_z()
{
float extent = JumpExtent;
if (extent < JumpExtentEpsilon)
return 0.0f;
if (extent > MaxJumpExtent)
extent = MaxJumpExtent;
if (WeenieObj is null)
return DefaultJumpVz;
if (WeenieObj.InqJumpVelocity(extent, out float vz))
return vz;
return 0.0f;
}
// ── FUN_00528cd0 — get_leave_ground_velocity ──────────────────────────────
///
/// Compose the full 3D body-local launch velocity when leaving the ground.
///
/// Decompiled logic (FUN_00528cd0):
/// FUN_00528960(velocity) ← get_state_velocity (XY components)
/// velocity.Z = get_jump_v_z()
/// If all three components are < epsilon (nearly zero velocity):
/// Apply the orientation matrix rows of PhysicsObj to the current
/// world-space velocity (rotate world vel into body-local frame).
/// This preserves momentum direction when jumping while stationary.
/// return velocity
///
/// The "near-zero" fast path uses the body's current velocity transformed
/// back into local space, which in our port is
/// Vector3.Transform(Velocity, Quaternion.Inverse(Orientation)).
///
public Vector3 get_leave_ground_velocity()
{
var velocity = get_state_velocity();
velocity.Z = get_jump_v_z();
// If the lateral + vertical components are all tiny, fall back to the
// current world velocity projected into body-local space so that an
// airborne nudge preserves direction (retail decompile: matrix multiply
// of the orientation column vectors against the world velocity).
float eps = JumpExtentEpsilon;
if (MathF.Abs(velocity.X) < eps && MathF.Abs(velocity.Y) < eps && MathF.Abs(velocity.Z) < eps
&& PhysicsObj is not null)
{
var invOrientation = Quaternion.Inverse(PhysicsObj.Orientation);
velocity = Vector3.Transform(PhysicsObj.Velocity, invOrientation);
}
return velocity;
}
// ── FUN_00528ec0 — jump_is_allowed ────────────────────────────────────────
///
/// Determine whether a jump is currently permitted.
///
/// Decompiled logic (FUN_00528ec0):
/// if PhysicsObj == null: return 0x24
/// if WeenieObj == null: proceed (no weenie check)
/// elif WeenieObj.IsCreature() returns false: proceed
/// iVar2 = PhysicsObj
/// if Gravity flag NOT set OR (Contact AND OnWalkable): ← grounded or no gravity
/// return 0x24 (GeneralMovementFailure)
/// if FUN_0050f730() (IsFullyConstrained) != 0: return 0x47
/// if pending queue action has non-zero jump error: return that error
/// iVar2 = FUN_00528660() (jump_charge_is_allowed)
/// if iVar2 == 0:
/// iVar2 = FUN_005285e0(InterpretedState.ForwardCommand) (motion_allows_jump)
/// if iVar2 == 0 AND WeenieObj != null:
/// cVar1 = WeenieObj.CanJump(extent, stamina) → vtable +0x40
/// if cVar1 == 0: return 0x47
/// return iVar2
///
public WeenieError jump_is_allowed(float extent, int staminaCost)
{
if (PhysicsObj is null)
return WeenieError.GeneralMovementFailure;
// Must have gravity and be grounded (Contact + OnWalkable) to start a jump.
bool hasGravity = PhysicsObj.State.HasFlag(PhysicsStateFlags.Gravity);
bool isGrounded = PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact)
&& PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable);
if (!hasGravity || !isGrounded)
return WeenieError.YouCantJumpWhileInTheAir;
// Delegate jump eligibility to WeenieObj if present.
if (WeenieObj is not null && !WeenieObj.CanJump(extent))
return WeenieError.CantJumpLoadedDown;
return WeenieError.None;
}
// ── FUN_00528dd0 — contact_allows_move ────────────────────────────────────
///
/// Determine whether the current contact state allows this motion command.
///
/// Decompiled logic (FUN_00528dd0):
/// if WeenieObj != null AND WeenieObj.CanJump(JumpExtent) returns false:
/// return 0x49
/// uVar1 = InterpretedState.ForwardCommand
/// if uVar1 == 0x40000008 (Fallen) OR uVar1 == 0x40000011 (Dead):
/// return 0x48
/// if 0x41000011 < uVar1 < 0x41000015 (crouch/sit/sleep range):
/// return 0x48
/// uVar2 = PhysicsObj.TransientState
/// if (Contact AND OnWalkable) AND ForwardCommand == Ready
/// AND SideStepCommand == 0 AND TurnCommand == 0:
/// StandingLongJump = true
/// return 0
///
/// The return type in the decompile is undefined4 (int), but ACE models it
/// as bool (0 = allowed, non-zero = blocked). We model it as bool here for
/// cleaner call sites, treating any non-zero return as "blocked".
///
public bool contact_allows_move(uint motion)
{
if (PhysicsObj is null)
return false;
// Turn commands are always allowed regardless of ground contact.
// (Decompile doesn't explicitly early-return for turns here, but
// ACE and the general shape of the code confirm they bypass the block.)
if (motion == MotionCommand.TurnRight || motion == MotionCommand.TurnLeft)
return true;
// Dead or Fallen forward-command blocks movement.
uint fwd = InterpretedState.ForwardCommand;
if (fwd == MotionCommand.Fallen || fwd == MotionCommand.Dead)
return false;
// Crouch / sit / sleep range (0x41000011 < fwd < 0x41000015).
if (fwd > MotionCommand.CrouchLowerBound && fwd < MotionCommand.CrouchUpperExclusive)
return false;
// Need Gravity flag + Contact + OnWalkable for ground-based motion.
if (!PhysicsObj.State.HasFlag(PhysicsStateFlags.Gravity))
return true; // no gravity → object can always move (swimming, flying)
bool contact = PhysicsObj.TransientState.HasFlag(TransientStateFlags.Contact);
bool onWalkable = PhysicsObj.TransientState.HasFlag(TransientStateFlags.OnWalkable);
if (!contact)
return false;
if (!onWalkable)
return false;
// Grounded and idle — flag as standing-long-jump candidate.
if (fwd == MotionCommand.Ready
&& InterpretedState.SideStepCommand == 0
&& InterpretedState.TurnCommand == 0)
{
StandingLongJump = true;
}
return true;
}
// ── FUN_00529710 — LeaveGround ────────────────────────────────────────────
///
/// Called when the body becomes airborne. Applies the leave-ground velocity
/// and resets the jump state.
///
/// Decompiled logic (FUN_00529710):
/// if PhysicsObj == null: return
/// velocity = get_leave_ground_velocity()
/// PhysicsObj.set_local_velocity(velocity)
/// StandingLongJump = false
/// JumpExtent = 0
///
public void LeaveGround()
{
if (PhysicsObj is null)
return;
var velocity = get_leave_ground_velocity();
PhysicsObj.set_local_velocity(velocity);
StandingLongJump = false;
JumpExtent = 0f;
}
// ── FUN_005296d0 — HitGround ──────────────────────────────────────────────
///
/// Called when the body lands on a walkable surface.
///
/// Decompiled logic (FUN_005296d0):
/// if PhysicsObj == null: return
/// if WeenieObj != null AND NOT creature: return
/// if Gravity flag not set: return
/// apply_current_movement(false, true)
///
public void HitGround()
{
if (PhysicsObj is null)
return;
if (!PhysicsObj.State.HasFlag(PhysicsStateFlags.Gravity))
return;
apply_current_movement(cancelMoveTo: false, allowJump: true);
}
// ── CMotionInterp::get_max_speed (0x00527cb0) ─────────────────────────────
///
/// Return the run rate. Mirrors retail
/// CMotionInterp::get_max_speed at 0x00527cb0.
///
///
/// Decomp (named-retail/acclient_2013_pseudo_c.txt:305127):
///
/// void get_max_speed(this) {
/// weenie_obj = this->weenie_obj;
/// this_1 = nullptr;
/// if (weenie_obj == 0) return;
/// if (weenie_obj->vtable->InqRunRate(&this_1) != 0) return;
/// this->my_run_rate; // x87 fld leaves my_run_rate on FPU stack
/// }
///
/// Binary Ninja shows the return type as void because the float
/// return rides the x87 FPU stack rather than EAX. Both branches
/// emit an fld of either this_1 (the InqRunRate
/// out-param value) or my_run_rate, leaving the run rate on
/// ST0 as the return value.
///
///
///
/// Critical: this returns the BARE run rate (typically 1.0 to
/// ~3.0), NOT a velocity in m/s. We previously multiplied by
/// RunAnimSpeed to get a m/s value, reasoning that
/// 2 × bare_rate would be too slow a catch-up speed for the
/// caller (InterpolationManager::adjust_offset). That was a
/// misread of the decomp — retail's catch-up IS that slow on purpose.
/// The multi-second 1-Hz blip the user reported when observing retail
/// remotes from acdream traced to body racing at the wrong (overshot)
/// catch-up speed (~23.5 m/s instead of the retail-correct ~5.9 m/s
/// for a run-skill-200 char).
///
///
public float GetMaxSpeed()
{
// Resolve current run rate: prefer WeenieObj.InqRunRate, fall back to MyRunRate.
// Then multiply by RunAnimSpeed (4.0). Matches ACE's MotionInterp.cs:670-678
// which is verified against retail (the ACE MotionInterp file is a
// line-by-line port). Returns the maximum world-space velocity in m/s
// — for run skill 200 with rate ≈ 2.94, this is ≈ 11.76 m/s. Used by
// InterpolationManager.AdjustOffset to compute the catch-up speed
// (= 2 × maxSpeed).
float rate = MyRunRate;
if (WeenieObj is not null && WeenieObj.InqRunRate(out float queried))
rate = queried;
return RunAnimSpeed * rate;
}
// ── private helper ────────────────────────────────────────────────────────
///
/// Apply a motion command to the interpreted state fields.
/// Mirrors the InterpretedState.ApplyMotion logic in ACE.
///
private void ApplyMotionToInterpretedState(uint motion, float speed)
{
switch (motion)
{
case MotionCommand.WalkForward:
case MotionCommand.RunForward:
case MotionCommand.WalkBackward:
InterpretedState.ForwardCommand = motion;
InterpretedState.ForwardSpeed = speed;
break;
case MotionCommand.SideStepRight:
case MotionCommand.SideStepLeft:
InterpretedState.SideStepCommand = motion;
InterpretedState.SideStepSpeed = speed;
break;
case MotionCommand.TurnRight:
case MotionCommand.TurnLeft:
InterpretedState.TurnCommand = motion;
InterpretedState.TurnSpeed = speed;
break;
case MotionCommand.Ready:
InterpretedState.ForwardCommand = MotionCommand.Ready;
InterpretedState.ForwardSpeed = 1.0f;
InterpretedState.SideStepCommand = 0;
InterpretedState.SideStepSpeed = 1.0f;
InterpretedState.TurnCommand = 0;
InterpretedState.TurnSpeed = 1.0f;
break;
}
}
}