feat(core): port decompiled AC client MotionInterpreter — 10 methods, 45 tests
C# port of CMotionInterp from chunk_00520000.c — the AC client's motion state machine that controls walk/run/jump/turn. Methods ported: - PerformMovement: top-level 5-case dispatcher - DoMotion: process raw motion command from packet - StopCompletely: reset to Ready (0x41000003) - get_state_velocity: compute velocity for current interpreted state - apply_current_movement: push velocity to PhysicsBody - jump: initiate jump (validate + set extent + leave ground) - get_jump_v_z: vertical jump velocity (delegates to WeenieObj) - get_leave_ground_velocity: full 3D launch vector - jump_is_allowed: requires Gravity + Contact + OnWalkable - contact_allows_move: slope angle + state checks Supporting types: MotionCommand constants, MovementType enum, WeenieError enum, RawMotionState/InterpretedMotionState structs, IWeenieObject interface. 412 total tests (303 core + 109 net). 45 new MotionInterpreter tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6a5d8c1580
commit
e3f8f95dfc
2 changed files with 1496 additions and 0 deletions
842
src/AcDream.Core/Physics/MotionInterpreter.cs
Normal file
842
src/AcDream.Core/Physics/MotionInterpreter.cs
Normal file
|
|
@ -0,0 +1,842 @@
|
|||
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>0x10000057 — Dead.</summary>
|
||||
public const uint Dead = 0x10000057u;
|
||||
/// <summary>0x41000011 — Crouch lower bound for blocked-jump check.</summary>
|
||||
public const uint CrouchLowerBound = 0x41000011u;
|
||||
/// <summary>0x41000014 — upper bound of crouch/sit/sleep range.</summary>
|
||||
public const uint CrouchUpperBound = 0x41000014u;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
// ── 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.
|
||||
///
|
||||
/// Decompiled logic (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
|
||||
/// </summary>
|
||||
public Vector3 get_state_velocity()
|
||||
{
|
||||
var velocity = Vector3.Zero;
|
||||
|
||||
if (InterpretedState.SideStepCommand == MotionCommand.SideStepRight)
|
||||
velocity.X = SidestepAnimSpeed * InterpretedState.SideStepSpeed;
|
||||
|
||||
if (InterpretedState.ForwardCommand == MotionCommand.WalkForward)
|
||||
velocity.Y = WalkAnimSpeed * InterpretedState.ForwardSpeed;
|
||||
else if (InterpretedState.ForwardCommand == MotionCommand.RunForward)
|
||||
velocity.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;
|
||||
|
||||
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 == 0x10000057 (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.CrouchUpperBound)
|
||||
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);
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
654
tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs
Normal file
654
tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs
Normal file
|
|
@ -0,0 +1,654 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// MotionInterpreterTests — covers the 10 ported methods.
|
||||
//
|
||||
// Source addresses tested:
|
||||
// FUN_00529a90 PerformMovement
|
||||
// FUN_00529930 DoMotion
|
||||
// FUN_00528a50 StopCompletely
|
||||
// FUN_00528960 get_state_velocity
|
||||
// FUN_00529390 jump
|
||||
// FUN_005286b0 get_jump_v_z
|
||||
// FUN_00528cd0 get_leave_ground_velocity
|
||||
// FUN_00528ec0 jump_is_allowed
|
||||
// FUN_00528dd0 contact_allows_move
|
||||
// FUN_00529210 apply_current_movement
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Fake WeenieObject for test isolation — all methods return configurable values.
|
||||
/// </summary>
|
||||
file sealed class FakeWeenie : IWeenieObject
|
||||
{
|
||||
public float RunRate = 1.0f;
|
||||
public float JumpVz = 10.0f;
|
||||
public bool CanJumpResult = true;
|
||||
public bool InqRunRateResult = true;
|
||||
public bool InqJumpVelocityResult = true;
|
||||
|
||||
public bool InqJumpVelocity(float extent, out float vz)
|
||||
{
|
||||
vz = JumpVz * extent;
|
||||
return InqJumpVelocityResult;
|
||||
}
|
||||
|
||||
public bool InqRunRate(out float rate)
|
||||
{
|
||||
rate = RunRate;
|
||||
return InqRunRateResult;
|
||||
}
|
||||
|
||||
public bool CanJump(float extent) => CanJumpResult;
|
||||
}
|
||||
|
||||
public sealed class MotionInterpreterTests
|
||||
{
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static PhysicsBody MakeGrounded()
|
||||
{
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
|
||||
};
|
||||
body.TransientState = TransientStateFlags.Contact
|
||||
| TransientStateFlags.OnWalkable
|
||||
| TransientStateFlags.Active;
|
||||
return body;
|
||||
}
|
||||
|
||||
private static PhysicsBody MakeAirborne()
|
||||
{
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
|
||||
};
|
||||
body.TransientState = TransientStateFlags.Active;
|
||||
return body;
|
||||
}
|
||||
|
||||
private static MotionInterpreter MakeInterp(PhysicsBody? body = null, IWeenieObject? weenie = null)
|
||||
{
|
||||
body ??= MakeGrounded();
|
||||
return new MotionInterpreter(body, weenie);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PerformMovement (FUN_00529a90) — dispatcher
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData(MovementType.RawCommand)]
|
||||
[InlineData(MovementType.InterpretedCommand)]
|
||||
[InlineData(MovementType.StopRawCommand)]
|
||||
[InlineData(MovementType.StopInterpretedCommand)]
|
||||
[InlineData(MovementType.StopCompletely)]
|
||||
public void PerformMovement_ValidTypes_ReturnNone(MovementType type)
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
var mvs = new MovementStruct
|
||||
{
|
||||
Type = type,
|
||||
Motion = MotionCommand.WalkForward,
|
||||
Speed = 1.0f,
|
||||
ModifyInterpretedState = true,
|
||||
ModifyRawState = false,
|
||||
};
|
||||
|
||||
var result = interp.PerformMovement(mvs);
|
||||
|
||||
Assert.Equal(WeenieError.None, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerformMovement_InvalidType_ReturnsGeneralFailure()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
var mvs = new MovementStruct { Type = (MovementType)99, Motion = MotionCommand.WalkForward };
|
||||
|
||||
var result = interp.PerformMovement(mvs);
|
||||
|
||||
Assert.Equal(WeenieError.GeneralMovementFailure, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerformMovement_StopCompletely_ResetsToReady()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.RawState.ForwardCommand = MotionCommand.WalkForward;
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
|
||||
|
||||
var mvs = new MovementStruct { Type = MovementType.StopCompletely };
|
||||
interp.PerformMovement(mvs);
|
||||
|
||||
Assert.Equal(MotionCommand.Ready, interp.RawState.ForwardCommand);
|
||||
Assert.Equal(MotionCommand.Ready, interp.InterpretedState.ForwardCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerformMovement_NullPhysicsObj_ReturnsNoPhysicsObject()
|
||||
{
|
||||
var interp = new MotionInterpreter(); // no PhysicsObj
|
||||
var mvs = new MovementStruct { Type = MovementType.RawCommand, Motion = MotionCommand.WalkForward };
|
||||
|
||||
var result = interp.PerformMovement(mvs);
|
||||
|
||||
Assert.Equal(WeenieError.NoPhysicsObject, result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DoMotion (FUN_00529930) — raw motion command sets RawState
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void DoMotion_WalkForward_SetsRawForwardCommand()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
|
||||
interp.DoMotion(MotionCommand.WalkForward, 0.8f);
|
||||
|
||||
Assert.Equal(MotionCommand.WalkForward, interp.RawState.ForwardCommand);
|
||||
Assert.Equal(0.8f, interp.RawState.ForwardSpeed, precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoMotion_NullPhysicsObj_ReturnsNoPhysicsObject()
|
||||
{
|
||||
var interp = new MotionInterpreter();
|
||||
|
||||
var result = interp.DoMotion(MotionCommand.WalkForward);
|
||||
|
||||
Assert.Equal(WeenieError.NoPhysicsObject, result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// StopCompletely (FUN_00528a50)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void StopCompletely_ResetsRawAndInterpretedToReady()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.RawState.ForwardCommand = MotionCommand.WalkForward;
|
||||
interp.RawState.SideStepCommand = MotionCommand.SideStepRight;
|
||||
interp.RawState.TurnCommand = MotionCommand.TurnRight;
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
|
||||
interp.InterpretedState.TurnCommand = MotionCommand.TurnLeft;
|
||||
|
||||
var result = interp.StopCompletely();
|
||||
|
||||
Assert.Equal(WeenieError.None, result);
|
||||
Assert.Equal(MotionCommand.Ready, interp.RawState.ForwardCommand);
|
||||
Assert.Equal(1.0f, interp.RawState.ForwardSpeed, precision: 5);
|
||||
Assert.Equal(0u, interp.RawState.SideStepCommand);
|
||||
Assert.Equal(0u, interp.RawState.TurnCommand);
|
||||
Assert.Equal(MotionCommand.Ready, interp.InterpretedState.ForwardCommand);
|
||||
Assert.Equal(1.0f, interp.InterpretedState.ForwardSpeed, precision: 5);
|
||||
Assert.Equal(0u, interp.InterpretedState.SideStepCommand);
|
||||
Assert.Equal(0u, interp.InterpretedState.TurnCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StopCompletely_ZerosPhysicsBodyVelocity()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
body.set_velocity(new Vector3(5f, 3f, 0f));
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
interp.StopCompletely();
|
||||
|
||||
Assert.Equal(Vector3.Zero, body.Velocity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StopCompletely_NullPhysicsObj_ReturnsNoPhysicsObject()
|
||||
{
|
||||
var interp = new MotionInterpreter();
|
||||
|
||||
var result = interp.StopCompletely();
|
||||
|
||||
Assert.Equal(WeenieError.NoPhysicsObject, result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// get_state_velocity (FUN_00528960)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void GetStateVelocity_Idle_ReturnsZero()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
// Default interpreted state = Ready, no sidestep, no turn
|
||||
|
||||
var vel = interp.get_state_velocity();
|
||||
|
||||
Assert.Equal(Vector3.Zero, vel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStateVelocity_WalkForward_ReturnsWalkSpeed()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
|
||||
interp.InterpretedState.ForwardSpeed = 1.0f;
|
||||
|
||||
var vel = interp.get_state_velocity();
|
||||
|
||||
// Y = WalkAnimSpeed * 1.0 = 3.12
|
||||
Assert.Equal(0f, vel.X, precision: 5);
|
||||
Assert.Equal(MotionInterpreter.WalkAnimSpeed, vel.Y, precision: 4);
|
||||
Assert.Equal(0f, vel.Z, precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStateVelocity_RunForward_ReturnsRunSpeed()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
|
||||
interp.InterpretedState.ForwardSpeed = 1.0f;
|
||||
|
||||
var vel = interp.get_state_velocity();
|
||||
|
||||
// Y = RunAnimSpeed * 1.0 = 4.0
|
||||
Assert.Equal(MotionInterpreter.RunAnimSpeed, vel.Y, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStateVelocity_SidestepRight_ReturnsPositiveX()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.InterpretedState.SideStepCommand = MotionCommand.SideStepRight;
|
||||
interp.InterpretedState.SideStepSpeed = 1.0f;
|
||||
|
||||
var vel = interp.get_state_velocity();
|
||||
|
||||
Assert.Equal(MotionInterpreter.SidestepAnimSpeed, vel.X, precision: 4);
|
||||
Assert.Equal(0f, vel.Y, precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStateVelocity_ClampsToMaxSpeed_WhenExceedRunRate()
|
||||
{
|
||||
// Speed=10 for walk would produce velocity.Y = 3.12*10 = 31.2, well over
|
||||
// RunAnimSpeed * runRate = 4.0 * 1.0 = 4.0; must be clamped.
|
||||
var interp = MakeInterp();
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
|
||||
interp.InterpretedState.ForwardSpeed = 10.0f;
|
||||
|
||||
var vel = interp.get_state_velocity();
|
||||
|
||||
float maxSpeed = MotionInterpreter.RunAnimSpeed * interp.MyRunRate;
|
||||
Assert.True(vel.Length() <= maxSpeed + 1e-4f, $"velocity {vel.Length()} exceeds maxSpeed {maxSpeed}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStateVelocity_UsesWeenieRunRate()
|
||||
{
|
||||
var weenie = new FakeWeenie { RunRate = 2.0f };
|
||||
var interp = MakeInterp(weenie: weenie);
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
|
||||
interp.InterpretedState.ForwardSpeed = 1.0f;
|
||||
|
||||
var vel = interp.get_state_velocity();
|
||||
|
||||
// maxSpeed = 4.0 * 2.0 = 8.0; velocity.Y = RunAnimSpeed * 1.0 = 4.0 ≤ 8.0
|
||||
Assert.Equal(MotionInterpreter.RunAnimSpeed, vel.Y, precision: 4);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// jump (FUN_00529390)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void Jump_Grounded_SetsJumpExtentAndLeavesWalkable()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
var result = interp.jump(0.5f);
|
||||
|
||||
Assert.Equal(WeenieError.None, result);
|
||||
Assert.Equal(0.5f, interp.JumpExtent, precision: 5);
|
||||
Assert.False(body.OnWalkable, "Body should no longer be on walkable after jump");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jump_Airborne_ReturnsYouCantJumpWhileInTheAir()
|
||||
{
|
||||
var body = MakeAirborne();
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
var result = interp.jump(0.5f);
|
||||
|
||||
Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jump_WeenieBlocksJump_ClearStandingLongJump()
|
||||
{
|
||||
var weenie = new FakeWeenie { CanJumpResult = false };
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body, weenie);
|
||||
interp.StandingLongJump = true;
|
||||
|
||||
var result = interp.jump(0.5f);
|
||||
|
||||
Assert.Equal(WeenieError.CantJumpLoadedDown, result);
|
||||
Assert.False(interp.StandingLongJump);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jump_NullPhysicsObj_ReturnsNoPhysicsObject()
|
||||
{
|
||||
var interp = new MotionInterpreter();
|
||||
|
||||
var result = interp.jump(0.5f);
|
||||
|
||||
Assert.Equal(WeenieError.NoPhysicsObject, result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// get_jump_v_z (FUN_005286b0)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void GetJumpVz_ZeroExtent_ReturnsZero()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.JumpExtent = 0f;
|
||||
|
||||
Assert.Equal(0f, interp.get_jump_v_z(), precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJumpVz_NoWeenie_ReturnsDefault()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.JumpExtent = 0.5f;
|
||||
|
||||
Assert.Equal(MotionInterpreter.DefaultJumpVz, interp.get_jump_v_z(), precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJumpVz_WithWeenie_DelegatesToInqJumpVelocity()
|
||||
{
|
||||
var weenie = new FakeWeenie { JumpVz = 8.0f };
|
||||
var interp = MakeInterp(weenie: weenie);
|
||||
interp.JumpExtent = 1.0f;
|
||||
|
||||
float vz = interp.get_jump_v_z();
|
||||
|
||||
// FakeWeenie returns JumpVz * extent = 8.0 * 1.0 = 8.0
|
||||
Assert.Equal(8.0f, vz, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJumpVz_ExtentClampsAt1()
|
||||
{
|
||||
var weenie = new FakeWeenie { JumpVz = 10.0f };
|
||||
var interp = MakeInterp(weenie: weenie);
|
||||
interp.JumpExtent = 5.0f; // over-clamped
|
||||
|
||||
float vz = interp.get_jump_v_z();
|
||||
|
||||
// Should clamp extent to 1.0: FakeWeenie returns 10.0 * 1.0 = 10.0
|
||||
Assert.Equal(10.0f, vz, precision: 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetJumpVz_WeenieReturnsFailure_ReturnsZero()
|
||||
{
|
||||
var weenie = new FakeWeenie { InqJumpVelocityResult = false };
|
||||
var interp = MakeInterp(weenie: weenie);
|
||||
interp.JumpExtent = 0.5f;
|
||||
|
||||
Assert.Equal(0f, interp.get_jump_v_z(), precision: 5);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// get_leave_ground_velocity (FUN_00528cd0)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void GetLeaveGroundVelocity_WalkingForward_HasPositiveYAndZ()
|
||||
{
|
||||
var weenie = new FakeWeenie { JumpVz = 10.0f };
|
||||
var body = MakeGrounded();
|
||||
var interp = new MotionInterpreter(body, weenie)
|
||||
{
|
||||
JumpExtent = 1.0f,
|
||||
};
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
|
||||
interp.InterpretedState.ForwardSpeed = 1.0f;
|
||||
|
||||
var vel = interp.get_leave_ground_velocity();
|
||||
|
||||
Assert.True(vel.Y > 0f, "Y velocity should be positive when walking forward");
|
||||
Assert.True(vel.Z > 0f, "Z (jump) velocity should be positive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetLeaveGroundVelocity_Standing_ZeroExtent_ReturnsZeroXY()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = new MotionInterpreter(body)
|
||||
{
|
||||
JumpExtent = 0f, // below epsilon
|
||||
};
|
||||
// Default interpreted state = Ready (no walk/run command)
|
||||
|
||||
var vel = interp.get_leave_ground_velocity();
|
||||
|
||||
Assert.Equal(0f, vel.X, precision: 5);
|
||||
Assert.Equal(0f, vel.Y, precision: 5);
|
||||
Assert.Equal(0f, vel.Z, precision: 5);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// jump_is_allowed (FUN_00528ec0)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_Grounded_ReturnsNone()
|
||||
{
|
||||
var interp = MakeInterp(MakeGrounded());
|
||||
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.None, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_Airborne_ReturnsYouCantJumpWhileInTheAir()
|
||||
{
|
||||
var interp = MakeInterp(MakeAirborne());
|
||||
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_WeenieBlocks_ReturnsCantJumpLoadedDown()
|
||||
{
|
||||
var weenie = new FakeWeenie { CanJumpResult = false };
|
||||
var interp = MakeInterp(MakeGrounded(), weenie);
|
||||
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.CantJumpLoadedDown, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_NoGravityFlag_ReturnsYouCantJumpWhileInTheAir()
|
||||
{
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
State = PhysicsStateFlags.None, // no gravity
|
||||
TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
|
||||
};
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
// No gravity → must be airborne-style
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JumpIsAllowed_NullPhysicsObj_ReturnsGeneralFailure()
|
||||
{
|
||||
var interp = new MotionInterpreter();
|
||||
|
||||
var result = interp.jump_is_allowed(0.5f, 0);
|
||||
|
||||
Assert.Equal(WeenieError.GeneralMovementFailure, result);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// contact_allows_move (FUN_00528dd0)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ContactAllowsMove_Grounded_Ready_AllowsMove()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body);
|
||||
// InterpretedState.ForwardCommand defaults to Ready
|
||||
|
||||
bool allowed = interp.contact_allows_move(MotionCommand.WalkForward);
|
||||
|
||||
Assert.True(allowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContactAllowsMove_Airborne_RejectsMove()
|
||||
{
|
||||
var body = MakeAirborne();
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
bool allowed = interp.contact_allows_move(MotionCommand.WalkForward);
|
||||
|
||||
Assert.False(allowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContactAllowsMove_TurnCommand_AlwaysAllowed_EvenAirborne()
|
||||
{
|
||||
var body = MakeAirborne();
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
// Turn commands bypass the ground-contact check.
|
||||
bool allowRight = interp.contact_allows_move(MotionCommand.TurnRight);
|
||||
bool allowLeft = interp.contact_allows_move(MotionCommand.TurnLeft);
|
||||
|
||||
Assert.True(allowRight);
|
||||
Assert.True(allowLeft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContactAllowsMove_FallenState_RejectsMove()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body);
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.Fallen;
|
||||
|
||||
bool allowed = interp.contact_allows_move(MotionCommand.WalkForward);
|
||||
|
||||
Assert.False(allowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContactAllowsMove_CrouchRange_RejectsMove()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body);
|
||||
// 0x41000012 is inside (0x41000011, 0x41000015) — crouch state
|
||||
interp.InterpretedState.ForwardCommand = 0x41000012u;
|
||||
|
||||
bool allowed = interp.contact_allows_move(MotionCommand.WalkForward);
|
||||
|
||||
Assert.False(allowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContactAllowsMove_NoGravity_AlwaysAllows()
|
||||
{
|
||||
// E.g. flying or swimming object
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
State = PhysicsStateFlags.None,
|
||||
TransientState = TransientStateFlags.None,
|
||||
};
|
||||
var interp = MakeInterp(body);
|
||||
|
||||
bool allowed = interp.contact_allows_move(MotionCommand.WalkForward);
|
||||
|
||||
Assert.True(allowed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContactAllowsMove_GroundedAndIdle_SetsStandingLongJump()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body);
|
||||
// All interpreted commands at default (Ready, no sidestep, no turn)
|
||||
interp.StandingLongJump = false;
|
||||
|
||||
interp.contact_allows_move(MotionCommand.WalkForward);
|
||||
|
||||
Assert.True(interp.StandingLongJump, "Should set StandingLongJump when grounded and idle");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// apply_current_movement (FUN_00529210)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void ApplyCurrentMovement_WalkForward_PushesNonZeroVelocity()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body);
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
|
||||
interp.InterpretedState.ForwardSpeed = 1.0f;
|
||||
|
||||
interp.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
|
||||
// The body's local-space velocity should have been set to some non-zero forward component.
|
||||
// We can't easily test world-space (depends on orientation = Identity), but with
|
||||
// Identity orientation the local Y becomes world Y.
|
||||
Assert.True(body.Velocity.Length() > 0f, "Velocity should be non-zero when walking");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyCurrentMovement_Idle_PushesZeroVelocity()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body);
|
||||
// Default state = Ready
|
||||
|
||||
interp.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
|
||||
Assert.Equal(Vector3.Zero, body.Velocity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyCurrentMovement_Run_UpdatesMyRunRate()
|
||||
{
|
||||
var body = MakeGrounded();
|
||||
var interp = MakeInterp(body);
|
||||
interp.MyRunRate = 1.0f;
|
||||
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
|
||||
interp.InterpretedState.ForwardSpeed = 2.0f; // e.g. speed-buff
|
||||
|
||||
interp.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
|
||||
Assert.Equal(2.0f, interp.MyRunRate, precision: 5);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue