Replaces the hybrid that double-counted forward translation:
predicted body.Velocity (set per-tick by apply_current_movement) +
the seqVel-derived offset both pushed the remote body forward at
~11.7 m/s × dt for run, summing to ~23.4 m/s × dt — the user's
"way too fast" + 1-Hz blip.
Per the named-retail decomp investigation 2026-05-03 (research agent
report dispatched against acclient_2013_pseudo_c.txt for
CSequence::update + UpdatePositionInternal + UpdateObjectInternal +
adjust_offset, line citations in the env-var path comments):
CPhysicsObj::UpdateObjectInternal (0x005156b0)
→ UpdatePositionInternal (0x00512c30)
→ CPartArray::Update (writes anim root motion into the offset frame)
→ PositionManager::adjust_offset (REPLACES the offset with catch-up
when the body is far from the queue head; otherwise leaves the
anim root motion alone — Frame::operator=(arg2, &__return)
semantics, NOT additive)
→ Frame::combine (out = m_position + offset)
→ UpdatePhysicsInternal (out += body.Velocity × dt + 0.5·accel·dt²)
For a remote in steady-state RunForward where the server hasn't pushed
an explicit velocity, m_velocityVector ≈ 0 and ALL per-tick translation
comes from the animation root motion (CSequence::update_internal +
Frame::combine of crossed pos_frames keyframes). Our port doesn't
extract per-keyframe pos_frames from the .anm assets; instead
AnimationSequencer.CurrentVelocity is the synthesized equivalent
(RunAnimSpeed × ForwardSpeed averaged), passed through
PositionManager.ComputeOffset.
Concrete changes in the env-var (ACDREAM_INTERP_MANAGER=1) path:
* Pass seqVel = ae.Sequencer.CurrentVelocity to ComputeOffset (was
Vector3.Zero — that disabled the animation-root-motion source and
left only the queue catch-up to drive translation, which lagged
server pace).
* Clear rm.Body.Velocity to Vector3.Zero for grounded remotes each
tick. Mirrors retail's m_velocityVector ≈ 0 for remotes; prevents
UpdatePhysicsInternal from adding a second 11.7 m/s × dt on top of
the seqVel-driven translation.
* Stop calling apply_current_movement per tick. Retail only calls it
on motion-state changes (per cdb traces from the L.5 investigation),
not per physics tick. body.Velocity-based translation is now the
AIRBORNE-only path (gravity integration during jumps).
Also reverts an unacceptable "scaling hack" (per-tick body.Velocity
scaled by observed serverSpeed/predictedSpeed) the user explicitly
rejected as patching over an unsolved structural problem.
GetMaxSpeed reverted to RunAnimSpeed × rate (matches ACE
MotionInterp.cs:670-678; the earlier "return bare rate" change came
from a misread of an x87-decompiled get_max_speed where Binary Ninja
showed the return type as void).
AnimationSequencer.SetCycle now ALWAYS overwrites CurrentVelocity for
known locomotion cycles (Walk/WalkBackward/Run/SideStepRight/
SideStepLeft) instead of gating on `CurrentVelocity.LengthSquared() <
1e-9f`. The gate was correct for non-locomotion entities with
dat-baked HasVelocity, but for Humanoid where the dat is silent and
the only thing that could set CurrentVelocity before synthesis was a
transition link's HasVelocity flag, the gate would silently leave the
body advancing at the link's velocity instead of the cycle's intended
steady-state.
Adds wire-arrival diagnostics gated on ACDREAM_REMOTE_VEL_DIAG=1
(SETCYCLE, FWD_WIRE) used to trace the bug to ground truth.
User-confirmed improvements vs prior state:
- Steady-state run no longer "way too fast"
- Run-in-circles smoother (rectangle effect gone)
- Jump landing in correct location
- Turn-left visibly turns left
Outstanding (not addressed by this commit, deferred for next
investigation): walk↔run direct transitions don't visibly switch the
animation cycle until the next motion event fires. Both legacy and
new paths exhibit the same behavior, so the bug lives in the
SetCycle queue manipulation pipeline shared by both — not in the
per-tick translation path that this commit revises. Wire trace
confirms ACE delivers the WalkForward → RunForward transition
correctly and SetCycle does fire for it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1023 lines
45 KiB
C#
1023 lines
45 KiB
C#
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) ────────────────
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public static class MotionCommand
|
||
{
|
||
/// <summary>0x41000003 — idle/default state.</summary>
|
||
public const uint Ready = 0x41000003u;
|
||
/// <summary>0x45000005 — walk forward.</summary>
|
||
public const uint WalkForward = 0x45000005u;
|
||
/// <summary>0x44000007 — run forward.</summary>
|
||
public const uint RunForward = 0x44000007u;
|
||
/// <summary>0x45000006 — walk backward.</summary>
|
||
public const uint WalkBackward = 0x45000006u;
|
||
/// <summary>0x6500000D — turn right.</summary>
|
||
public const uint TurnRight = 0x6500000Du;
|
||
/// <summary>0x6500000E — turn left.</summary>
|
||
public const uint TurnLeft = 0x6500000Eu;
|
||
/// <summary>0x6500000F — sidestep right.</summary>
|
||
public const uint SideStepRight = 0x6500000Fu;
|
||
/// <summary>0x65000010 — sidestep left.</summary>
|
||
public const uint SideStepLeft = 0x65000010u;
|
||
/// <summary>0x40000008 — Fallen (lying on ground).</summary>
|
||
public const uint Fallen = 0x40000008u;
|
||
/// <summary>
|
||
/// 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 <see cref="AnimationSequencer.SetCycle"/>
|
||
/// when airborne; swap back to Ready/WalkForward/RunForward on land.
|
||
/// </summary>
|
||
public const uint Falling = 0x40000015u;
|
||
/// <summary>
|
||
/// 0x2500003B — Jump (Modifier flag). NOT an animation trigger; retail
|
||
/// uses this as a state flag internally. Kept for future use.
|
||
/// </summary>
|
||
public const uint Jump = 0x2500003Bu;
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public const uint Jumpup = 0x1000004Bu;
|
||
/// <summary>
|
||
/// 0x10000050 — FallDown (Action). Same story as Jumpup; not in the
|
||
/// humanoid motion table's Links. Landing returns to Ready via the
|
||
/// regular SetCycle transition.
|
||
/// </summary>
|
||
public const uint FallDown = 0x10000050u;
|
||
/// <summary>0x40000011 - persistent dead substate.</summary>
|
||
public const uint Dead = 0x40000011u;
|
||
/// <summary>0x10000057 - Sanctuary death-trigger action.</summary>
|
||
public const uint Sanctuary = 0x10000057u;
|
||
/// <summary>0x41000012 - crouching substate.</summary>
|
||
public const uint Crouch = 0x41000012u;
|
||
/// <summary>0x41000013 - sitting substate.</summary>
|
||
public const uint Sitting = 0x41000013u;
|
||
/// <summary>0x41000014 - sleeping substate.</summary>
|
||
public const uint Sleeping = 0x41000014u;
|
||
/// <summary>0x41000011 — Crouch lower bound for blocked-jump check.</summary>
|
||
public const uint CrouchLowerBound = 0x41000011u;
|
||
/// <summary>0x41000015 - exclusive upper bound of crouch/sit/sleep range.</summary>
|
||
public const uint CrouchUpperExclusive = 0x41000015u;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Movement type passed in PerformMovement's switch statement.
|
||
/// Matches the 5-case switch at FUN_00529a90.
|
||
/// </summary>
|
||
public enum MovementType
|
||
{
|
||
/// <summary>case 1 — raw motion command (DoMotion).</summary>
|
||
RawCommand = 1,
|
||
/// <summary>case 2 — interpreted motion command (DoInterpretedMotion).</summary>
|
||
InterpretedCommand = 2,
|
||
/// <summary>case 3 — stop raw motion (StopMotion).</summary>
|
||
StopRawCommand = 3,
|
||
/// <summary>case 4 — stop interpreted motion (StopInterpretedMotion).</summary>
|
||
StopInterpretedCommand = 4,
|
||
/// <summary>case 5 — stop completely (StopCompletely).</summary>
|
||
StopCompletely = 5,
|
||
}
|
||
|
||
/// <summary>
|
||
/// WeenieError codes returned by CMotionInterp methods.
|
||
/// Values are the hex constants used directly in the decompiled C code.
|
||
/// </summary>
|
||
public enum WeenieError : uint
|
||
{
|
||
/// <summary>0x00 — success.</summary>
|
||
None = 0x00,
|
||
/// <summary>0x08 — PhysicsObj is null.</summary>
|
||
NoPhysicsObject = 0x08,
|
||
/// <summary>0x24 — general movement failure.</summary>
|
||
GeneralMovementFailure = 0x24,
|
||
/// <summary>0x47 — cannot jump from this position (motion state blocks it).</summary>
|
||
YouCantJumpFromThisPosition = 0x47,
|
||
/// <summary>0x48 — cannot jump while in the air.</summary>
|
||
YouCantJumpWhileInTheAir = 0x48,
|
||
/// <summary>0x49 — loaded down / weenie blocked the jump.</summary>
|
||
CantJumpLoadedDown = 0x49,
|
||
}
|
||
|
||
// ── Motion state structs ───────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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.).
|
||
/// </summary>
|
||
public struct RawMotionState
|
||
{
|
||
/// <summary>Forward/backward motion command (offset +0x20).</summary>
|
||
public uint ForwardCommand;
|
||
/// <summary>Speed scalar for forward motion (offset +0x28).</summary>
|
||
public float ForwardSpeed;
|
||
/// <summary>Sidestep command (offset +0x2C).</summary>
|
||
public uint SideStepCommand;
|
||
/// <summary>Speed scalar for sidestep (offset +0x34, inferred from ACE).</summary>
|
||
public float SideStepSpeed;
|
||
/// <summary>Turn command (offset +0x38).</summary>
|
||
public uint TurnCommand;
|
||
/// <summary>Speed scalar for turn (offset +0x40, inferred).</summary>
|
||
public float TurnSpeed;
|
||
|
||
/// <summary>Initialize to the idle/ready state (1.0 speed, Ready command).</summary>
|
||
public static RawMotionState Default() => new()
|
||
{
|
||
ForwardCommand = MotionCommand.Ready,
|
||
ForwardSpeed = 1.0f,
|
||
SideStepCommand = 0,
|
||
SideStepSpeed = 1.0f,
|
||
TurnCommand = 0,
|
||
TurnSpeed = 1.0f,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Interpreted motion state, derived from the raw state.
|
||
/// Struct layout: starts at offset +0x44 (ForwardCommand at +0x4C, ForwardSpeed at +0x50).
|
||
/// </summary>
|
||
public struct InterpretedMotionState
|
||
{
|
||
/// <summary>Forward/backward interpreted command (offset +0x4C).</summary>
|
||
public uint ForwardCommand;
|
||
/// <summary>Speed scalar for interpreted forward motion (offset +0x50).</summary>
|
||
public float ForwardSpeed;
|
||
/// <summary>Sidestep interpreted command (offset +0x54).</summary>
|
||
public uint SideStepCommand;
|
||
/// <summary>Speed scalar for interpreted sidestep (offset +0x58).</summary>
|
||
public float SideStepSpeed;
|
||
/// <summary>Turn interpreted command (offset +0x5C).</summary>
|
||
public uint TurnCommand;
|
||
/// <summary>Speed scalar for turn (offset +0x60).</summary>
|
||
public float TurnSpeed;
|
||
|
||
/// <summary>Initialize to the idle/ready state.</summary>
|
||
public static InterpretedMotionState Default() => new()
|
||
{
|
||
ForwardCommand = MotionCommand.Ready,
|
||
ForwardSpeed = 1.0f,
|
||
SideStepCommand = 0,
|
||
SideStepSpeed = 1.0f,
|
||
TurnCommand = 0,
|
||
TurnSpeed = 1.0f,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Lightweight struct passed into PerformMovement.
|
||
/// Fields correspond to what the retail dispatcher read from param_1 (the movement packet struct).
|
||
/// </summary>
|
||
public struct MovementStruct
|
||
{
|
||
/// <summary>Which of the 5 motion types to dispatch.</summary>
|
||
public MovementType Type;
|
||
/// <summary>Motion command ID (e.g. WalkForward).</summary>
|
||
public uint Motion;
|
||
/// <summary>Speed scalar for this motion.</summary>
|
||
public float Speed;
|
||
/// <summary>Autonomous (player-initiated) flag.</summary>
|
||
public bool Autonomous;
|
||
/// <summary>Whether to modify the interpreted state.</summary>
|
||
public bool ModifyInterpretedState;
|
||
/// <summary>Whether to modify the raw state.</summary>
|
||
public bool ModifyRawState;
|
||
}
|
||
|
||
// ── Optional WeenieObject interface ──────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Minimal interface for the server-side WeenieObject callbacks that CMotionInterp
|
||
/// reaches through at vtable offsets +0x30, +0x34, +0x3C.
|
||
/// Allows testing without a real weenie.
|
||
/// </summary>
|
||
public interface IWeenieObject
|
||
{
|
||
/// <summary>vtable +0x30 — InqJumpVelocity. Returns true and sets vz if valid.</summary>
|
||
bool InqJumpVelocity(float extent, out float vz);
|
||
/// <summary>vtable +0x34 — InqRunRate. Returns true and sets rate if valid.</summary>
|
||
bool InqRunRate(out float rate);
|
||
/// <summary>vtable +0x3C — CanJump. Returns true if the weenie can jump at this extent.</summary>
|
||
bool CanJump(float extent);
|
||
}
|
||
|
||
// ── MotionInterpreter ─────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
public sealed class MotionInterpreter
|
||
{
|
||
// ── animation speed constants (from ACE / confirmed by decompile globals) ─
|
||
/// <summary>Walk animation base speed (_DAT_007c96e4 family).</summary>
|
||
public const float WalkAnimSpeed = 3.12f;
|
||
/// <summary>Run animation base speed (_DAT_007c96e0 family).</summary>
|
||
public const float RunAnimSpeed = 4.0f;
|
||
/// <summary>Sidestep animation base speed (_DAT_007c96e8 family).</summary>
|
||
public const float SidestepAnimSpeed = 1.25f;
|
||
/// <summary>Minimum jump extent before get_jump_v_z bothers computing (_DAT_007c9734).</summary>
|
||
public const float JumpExtentEpsilon = 0.001f;
|
||
/// <summary>Fallback vertical jump velocity when WeenieObj is absent (_DAT_0079c6d4).</summary>
|
||
public const float DefaultJumpVz = 10.0f;
|
||
/// <summary>Maximum jump extent clamped by get_jump_v_z (_DAT_007938b0 = 1.0f).</summary>
|
||
public const float MaxJumpExtent = 1.0f;
|
||
|
||
// ── fields (matching struct layout from acclient_function_map.md) ─────────
|
||
|
||
/// <summary>The physics body this interpreter controls (struct offset +0x08).</summary>
|
||
public PhysicsBody? PhysicsObj { get; set; }
|
||
|
||
/// <summary>Optional WeenieObject for stamina / run-rate queries (struct offset +0x04).</summary>
|
||
public IWeenieObject? WeenieObj { get; set; }
|
||
|
||
/// <summary>Raw (network-derived) motion state (struct offsets +0x14..+0x44).</summary>
|
||
public RawMotionState RawState;
|
||
|
||
/// <summary>Interpreted motion state derived from raw (struct offsets +0x44..+0x7C).</summary>
|
||
public InterpretedMotionState InterpretedState;
|
||
|
||
/// <summary>Jump charge accumulator — set in jump(), cleared in LeaveGround() (offset +0x74).</summary>
|
||
public float JumpExtent;
|
||
|
||
/// <summary>Stored run rate from last successful InqRunRate call (offset +0x7C).</summary>
|
||
public float MyRunRate = 1.0f;
|
||
|
||
/// <summary>True when crouching-in-place for a standing long jump (offset +0x70).</summary>
|
||
public bool StandingLongJump;
|
||
|
||
/// <summary>
|
||
/// Optional accessor for the owning entity's current animation cycle
|
||
/// velocity (AnimationSequencer.CurrentVelocity, i.e. MotionData.Velocity
|
||
/// scaled by speedMod). When wired, <see cref="get_state_velocity"/>
|
||
/// uses it as the primary forward-axis drive instead of the hardcoded
|
||
/// <see cref="RunAnimSpeed"/> / <see cref="WalkAnimSpeed"/> constants.
|
||
///
|
||
/// <para>
|
||
/// <b>Why:</b> the decompiled <c>get_state_velocity</c> (FUN_00528960)
|
||
/// literally computes <c>RunAnimSpeed * ForwardSpeed</c>. That works in
|
||
/// retail because retail's Humanoid MotionTable happens to bake
|
||
/// <c>MotionData.Velocity == RunAnimSpeed (4.0)</c> 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.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Per <c>docs/research/deepdives/r03-motion-animation.md</c> §1.3,
|
||
/// the retail animation pipeline treats <c>MotionData.Velocity *
|
||
/// speedMod</c> as the canonical per-cycle world velocity. The
|
||
/// <see cref="RunAnimSpeed"/> constant survives in our port only as
|
||
/// the max-speed clamp (see below), which matches the decompile's
|
||
/// <c>if (|velocity| > RunAnimSpeed * rate)</c> guard.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Call site: <c>PlayerMovementController.AttachCycleVelocityAccessor</c>
|
||
/// wires this to <c>AnimatedEntity.Sequencer.CurrentVelocity</c> 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).
|
||
/// </para>
|
||
/// </summary>
|
||
public Func<Vector3>? 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 ────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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)
|
||
/// </summary>
|
||
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 ───────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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 ────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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 ────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Stop a specific raw motion (FUN_00529140 → StopInterpretedMotion).
|
||
/// </summary>
|
||
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 ────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Stop a specific interpreted motion (FUN_00529080).
|
||
/// </summary>
|
||
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 ─────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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
|
||
/// </summary>
|
||
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 ────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Compute the body-local velocity vector for the current interpreted motion.
|
||
///
|
||
/// <para>
|
||
/// <b>Decompiled path (FUN_00528960)</b>:
|
||
/// <code>
|
||
/// 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
|
||
/// </code>
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <b>Option B — MotionData-sourced forward velocity</b>:
|
||
/// when <see cref="GetCycleVelocity"/> is wired (i.e. the owning
|
||
/// entity has an AnimationSequencer), we prefer
|
||
/// <c>MotionData.Velocity.Y * speedMod</c> over the hardcoded
|
||
/// <see cref="WalkAnimSpeed"/> / <see cref="RunAnimSpeed"/> constants.
|
||
/// This keeps the body's world velocity locked to the animation's
|
||
/// baked-in root-motion velocity (<c>r03 §1.3</c>), 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.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// The <see cref="RunAnimSpeed"/> constant survives as the max-speed
|
||
/// clamp at the bottom, faithfully matching the decompile's
|
||
/// <c>if (|velocity| > RunAnimSpeed * rate)</c> guard. Sidestep
|
||
/// continues to use <see cref="SidestepAnimSpeed"/> because the
|
||
/// sequencer only tracks the current forward cycle — strafe is
|
||
/// implemented as a separate axis in our controller (see
|
||
/// <c>PlayerMovementController.Update</c>).
|
||
/// </para>
|
||
/// </summary>
|
||
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 ─────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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 ───────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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
|
||
/// </summary>
|
||
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 ──────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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)
|
||
/// </summary>
|
||
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 ──────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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)).
|
||
/// </summary>
|
||
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 ────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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
|
||
/// </summary>
|
||
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 ────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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".
|
||
/// </summary>
|
||
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 ────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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
|
||
/// </summary>
|
||
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 ──────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 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)
|
||
/// </summary>
|
||
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) ─────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Return the run rate. Mirrors retail
|
||
/// <c>CMotionInterp::get_max_speed</c> at <c>0x00527cb0</c>.
|
||
///
|
||
/// <para>
|
||
/// <b>Decomp (named-retail/acclient_2013_pseudo_c.txt:305127):</b>
|
||
/// <code>
|
||
/// 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
|
||
/// }
|
||
/// </code>
|
||
/// Binary Ninja shows the return type as <c>void</c> because the float
|
||
/// return rides the x87 FPU stack rather than EAX. Both branches
|
||
/// emit an <c>fld</c> of either <c>this_1</c> (the InqRunRate
|
||
/// out-param value) or <c>my_run_rate</c>, leaving the run rate on
|
||
/// ST0 as the return value.
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// <b>Critical:</b> this returns the BARE run rate (typically 1.0 to
|
||
/// ~3.0), NOT a velocity in m/s. We previously multiplied by
|
||
/// <c>RunAnimSpeed</c> to get a m/s value, reasoning that
|
||
/// <c>2 × bare_rate</c> would be too slow a catch-up speed for the
|
||
/// caller (<c>InterpolationManager::adjust_offset</c>). 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).
|
||
/// </para>
|
||
/// </summary>
|
||
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 ────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Apply a motion command to the interpreted state fields.
|
||
/// Mirrors the InterpretedState.ApplyMotion logic in ACE.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|