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