The register's UN-2 row recorded a contradiction: the GetMaxSpeed XML doc claimed the bare run rate was retail-correct (~5.9 m/s catch-up, calling the xRunAnimSpeed multiply a misread), while the implementation multiplied by RunAnimSpeed citing ACE. Settled against the binary, not the pseudo-C: - BN pseudo-C (acclient_2013_pseudo_c.txt:305127) renders get_max_speed as void with a bare `this->my_run_rate;` because it DROPS x87 instructions. - Disassembling the PDB-matched v11.4186 binary at VA 0x00527cb0: all THREE return paths end `fld <rate>; fmul dword ptr [0x007C8918]; ret`, and the .rdata dword at 0x007C8918 is 4.0f. Sibling get_adjusted_max_speed (0x00527d00) carries the same trailing fmul. Verifier committed at tools/verify_un2_fmul.py (PE parse + byte decode, rerunnable). - Retail paths: weenie null -> 1.0 x4; InqRunRate ok -> queried x4; InqRunRate failed -> my_run_rate x4. ACE MotionInterp.cs:665-676 matches. Changes: - Doc-comment rewritten: the implementation is retail-correct; the catch-up speed 2 x get_max_speed ~= 23.5 m/s at run 200 IS retail. The 1-Hz remote-blip symptom the old comment attributed to this multiply is therefore UNEXPLAINED by it (if it recurs: #41 family, not this). - Weenie-null path aligned to retail's LITERAL 1.0 default (was MyRunRate). - Tests re-pinned to the three retail paths (the old NoWeenie test pinned the non-retail fallback). - Register: UN-2 row deleted per the retire rule (6 -> 5 UN rows); shortlist renumbered. This is the 2nd confirmed instance of the BN x87-dropout artifact class (memory: feedback_bn_decomp_field_names) deciding a register row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1018 lines
45 KiB
C#
1018 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 maximum movement speed in m/s: run rate × RunAnimSpeed (4.0).
|
||
/// Mirrors retail <c>CMotionInterp::get_max_speed</c> at <c>0x00527cb0</c>.
|
||
///
|
||
/// <para>
|
||
/// <b>The ×4.0 is byte-verified retail (UN-2 resolved 2026-06-12).</b>
|
||
/// The Binary Ninja pseudo-C (named-retail/acclient_2013_pseudo_c.txt:305127)
|
||
/// renders this function as <c>void</c> with a bare <c>this->my_run_rate;</c>
|
||
/// statement because it drops x87 instructions — a known BN artifact class.
|
||
/// Disassembling the PDB-matched v11.4186 binary at VA <c>0x00527cb0</c>
|
||
/// shows all THREE return paths end with
|
||
/// <c>fmul dword ptr [0x007C8918]</c>, and the .rdata dword at
|
||
/// <c>0x007C8918</c> is <c>0x40800000</c> = 4.0f (the sibling
|
||
/// <c>get_adjusted_max_speed</c> 0x00527d00 carries the same trailing
|
||
/// fmul). Re-derive with <c>py tools/verify_un2_fmul.py</c>. The three
|
||
/// retail paths: weenie_obj == null → 1.0×4; InqRunRate success →
|
||
/// queried×4; InqRunRate failure → my_run_rate×4. ACE's
|
||
/// MotionInterp.cs:665-676 ports it identically (RunAnimSpeed = 4.0f).
|
||
/// </para>
|
||
///
|
||
/// <para>
|
||
/// Consequence: the dead-reckoning catch-up speed
|
||
/// (<c>InterpolationManager::adjust_offset</c> 0x00555d30, pc:353122)
|
||
/// is <c>2 × get_max_speed()</c> ≈ 23.5 m/s for a run-rate-2.94
|
||
/// (run-skill-200) character — that IS retail's value. An earlier
|
||
/// doc-comment here claimed the bare rate (~5.9 m/s catch-up) was
|
||
/// retail-correct and blamed the ×4 for the multi-second 1-Hz blip on
|
||
/// observed retail remotes; that reading trusted the BN x87 dropout
|
||
/// and is refuted by the binary. If the blip recurs, its root cause is
|
||
/// elsewhere (node-fail handling / progress-quantum abandonment /
|
||
/// position-queue feed — the #41 family), NOT this multiply.
|
||
/// </para>
|
||
/// </summary>
|
||
public float GetMaxSpeed()
|
||
{
|
||
// Retail 0x00527cb0: weenie null → 1.0; InqRunRate ok → queried;
|
||
// InqRunRate failed → my_run_rate. Every path × RunAnimSpeed (4.0,
|
||
// .rdata 0x007C8918). Note the weenie-null default is the LITERAL 1.0
|
||
// (.rdata 0x007928B0), not my_run_rate.
|
||
float rate = 1.0f;
|
||
if (WeenieObj is not null && !WeenieObj.InqRunRate(out rate))
|
||
rate = MyRunRate;
|
||
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;
|
||
}
|
||
}
|
||
}
|