using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using Xunit; namespace AcDream.Core.Tests.Physics; // ───────────────────────────────────────────────────────────────────────────── // MotionInterpreterJumpFamilyTests — R3-W3 (closes J5, J6, J7-interp-side, // J16-epsilons). Pins per docs/research/2026-07-02-r3-motioninterp/W0-pins.md // (A1, A2, A5, A6, A10) and r3-motioninterp-decomp.md §3a-3h. // // Source addresses tested: // FUN_00527a50 (0x00527a50) jump_charge_is_allowed @304935 // FUN_005281c0 (0x005281c0) charge_jump @305448 // FUN_00527aa0 (0x00527aa0) get_jump_v_z @304953 // FUN_005280c0 (0x005280c0) get_leave_ground_velocity @305404 // FUN_005282b0 (0x005282b0) jump_is_allowed (full chain) // FUN_00528780 (0x00528780) jump @305792 // ───────────────────────────────────────────────────────────────────────────── /// Fake WeenieObject for jump-family test isolation. 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; /// Controls the JumpStaminaCost virtual (vtable +0x44 per the /// raw 305549-305556 shape): true = affordable (pass), false = 0x47. public bool JumpStaminaCostResult = true; public int JumpStaminaCostCalls; 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 bool JumpStaminaCost(float extent, out int cost) { JumpStaminaCostCalls++; cost = 0; return JumpStaminaCostResult; } } public sealed class MotionInterpreterJumpFamilyTests { // ── 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); } // ========================================================================= // jump_charge_is_allowed — 0x00527a50 @304935 // // Raw (r3-motioninterp-decomp.md §3b): // weenie_obj = this->weenie_obj; // if (weenie_obj != 0 && weenie_obj->vtable->CanJump(this->jump_extent) == 0) // return 0x49; // forward_command = this->interpreted_state.forward_command; // if (forward_command != 0x40000008 // && (forward_command <= 0x41000011 || forward_command > 0x41000014)) // return 0; // return 0x48; // ========================================================================= [Fact] public void JumpChargeIsAllowed_NoWeenie_ReadyForward_ReturnsNone() { var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.Ready; var result = interp.JumpChargeIsAllowed(0.5f); Assert.Equal(WeenieError.None, result); } [Fact] public void JumpChargeIsAllowed_WeenieCanJumpFalse_ReturnsCantJumpLoadedDown() { var weenie = new FakeWeenie { CanJumpResult = false }; var interp = MakeInterp(weenie: weenie); interp.InterpretedState.ForwardCommand = MotionCommand.Ready; var result = interp.JumpChargeIsAllowed(0.5f); Assert.Equal(WeenieError.CantJumpLoadedDown, result); } [Fact] public void JumpChargeIsAllowed_Fallen_ReturnsYouCantJumpFromThisPosition() { var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.Fallen; // 0x40000008 var result = interp.JumpChargeIsAllowed(0.5f); Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result); } [Theory] [InlineData(MotionCommand.Crouch)] // 0x41000012 [InlineData(MotionCommand.Sitting)] // 0x41000013 [InlineData(MotionCommand.Sleeping)] // 0x41000014 public void JumpChargeIsAllowed_CrouchSitSleepRange_ReturnsYouCantJumpFromThisPosition(uint forward) { var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = forward; var result = interp.JumpChargeIsAllowed(0.5f); Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result); } [Fact] public void JumpChargeIsAllowed_CrouchLowerBoundExact_Passes() { // forward_command <= 0x41000011 passes (the gate is a strict // inequality on the LOWER bound: forward_command > 0x41000011 is // required to even consider the block). 0x41000011 == CrouchLowerBound // itself must PASS (not blocked) — verbatim boundary. var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.CrouchLowerBound; // 0x41000011 var result = interp.JumpChargeIsAllowed(0.5f); Assert.Equal(WeenieError.None, result); } [Fact] public void JumpChargeIsAllowed_SleepUpperBoundExact_Blocked() { // forward_command > 0x41000014 passes; == 0x41000014 (Sleeping) is // still inside the blocked range (already covered above), but this // pins the boundary explicitly: 0x41000015 (one past Sleeping) must // PASS. var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.CrouchUpperExclusive; // 0x41000015 var result = interp.JumpChargeIsAllowed(0.5f); Assert.Equal(WeenieError.None, result); } [Fact] public void JumpChargeIsAllowed_Falling_Passes() { // Falling (0x40000015) is NOT Fallen (0x40000008) — must pass this // gate (though jump_is_allowed's separate ground check blocks // mid-air jumps by a different mechanism — A1 adjudication). var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.Falling; var result = interp.JumpChargeIsAllowed(0.5f); Assert.Equal(WeenieError.None, result); } [Fact] public void JumpChargeIsAllowed_CanJumpCheckedBeforePostureCheck() { // Raw order: CanJump gate FIRST, forward_command gate SECOND. A // weenie that refuses CanJump returns 0x49 even in a posture that // would otherwise pass. var weenie = new FakeWeenie { CanJumpResult = false }; var interp = MakeInterp(weenie: weenie); interp.InterpretedState.ForwardCommand = MotionCommand.Falling; // would pass posture gate var result = interp.JumpChargeIsAllowed(0.5f); Assert.Equal(WeenieError.CantJumpLoadedDown, result); } // ========================================================================= // charge_jump — 0x005281c0 @305448 (closes J6) // // Raw (r3-motioninterp-decomp.md §3e): // weenie_obj = this->weenie_obj; // if (weenie_obj != 0 && weenie_obj->vtable->CanJump(this->jump_extent) == 0) // return 0x49; // forward_command = this->interpreted_state.forward_command; // if (forward_command == 0x40000008 // || (forward_command > 0x41000011 && forward_command <= 0x41000014)) // return 0x48; // transient_state = physics_obj->transient_state; // if ((transient_state & 1) != 0 && (transient_state & 2) != 0 // && forward_command == 0x41000003 // && interpreted_state.sidestep_command == 0 // && interpreted_state.turn_command == 0) // standing_longjump = 1; // return 0; // ========================================================================= [Fact] public void ChargeJump_GroundedIdle_ArmsStandingLongJump() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.StandingLongJump = false; // Default interpreted state: ForwardCommand=Ready, SideStep=0, Turn=0. var result = interp.ChargeJump(); Assert.Equal(WeenieError.None, result); Assert.True(interp.StandingLongJump, "grounded + idle must arm StandingLongJump"); } [Fact] public void ChargeJump_GroundedButMoving_DoesNotArmStandingLongJump() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.StandingLongJump = false; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; var result = interp.ChargeJump(); Assert.Equal(WeenieError.None, result); Assert.False(interp.StandingLongJump, "must not arm while moving forward"); } [Fact] public void ChargeJump_GroundedIdleButSidestepping_DoesNotArmStandingLongJump() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.StandingLongJump = false; interp.InterpretedState.ForwardCommand = MotionCommand.Ready; interp.InterpretedState.SideStepCommand = MotionCommand.SideStepRight; var result = interp.ChargeJump(); Assert.Equal(WeenieError.None, result); Assert.False(interp.StandingLongJump, "must not arm while sidestepping"); } [Fact] public void ChargeJump_GroundedIdleButTurning_DoesNotArmStandingLongJump() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.StandingLongJump = false; interp.InterpretedState.ForwardCommand = MotionCommand.Ready; interp.InterpretedState.TurnCommand = MotionCommand.TurnRight; var result = interp.ChargeJump(); Assert.Equal(WeenieError.None, result); Assert.False(interp.StandingLongJump, "must not arm while turning"); } [Fact] public void ChargeJump_Airborne_DoesNotArmStandingLongJump() { var body = MakeAirborne(); var interp = MakeInterp(body); interp.StandingLongJump = false; var result = interp.ChargeJump(); Assert.Equal(WeenieError.None, result); Assert.False(interp.StandingLongJump, "must not arm while airborne (no Contact+OnWalkable)"); } [Fact] public void ChargeJump_WeenieBlocksCanJump_ReturnsCantJumpLoadedDown_NoArm() { var weenie = new FakeWeenie { CanJumpResult = false }; var body = MakeGrounded(); var interp = MakeInterp(body, weenie); interp.StandingLongJump = false; var result = interp.ChargeJump(); Assert.Equal(WeenieError.CantJumpLoadedDown, result); Assert.False(interp.StandingLongJump); } [Fact] public void ChargeJump_Fallen_ReturnsYouCantJumpFromThisPosition_NoArm() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.InterpretedState.ForwardCommand = MotionCommand.Fallen; interp.StandingLongJump = false; var result = interp.ChargeJump(); Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result); Assert.False(interp.StandingLongJump); } [Fact] public void ChargeJump_CrouchRange_ReturnsYouCantJumpFromThisPosition_NoArm() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.InterpretedState.ForwardCommand = MotionCommand.Sitting; interp.StandingLongJump = false; var result = interp.ChargeJump(); Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result); Assert.False(interp.StandingLongJump); } // ── J6 regression pin: contact_allows_move no longer arms StandingLongJump ── [Fact] public void ContactAllowsMove_GroundedAndIdle_DoesNotArmStandingLongJump() { // J6: the S2a-flagged misattribution is DELETED. Only ChargeJump // arms StandingLongJump now — contact_allows_move must have no side // effect on this flag at all, in either direction. var body = MakeGrounded(); var interp = MakeInterp(body); interp.StandingLongJump = false; interp.contact_allows_move(MotionCommand.WalkForward); Assert.False(interp.StandingLongJump, "contact_allows_move must never arm StandingLongJump (J6 — moved to ChargeJump exclusively)"); } [Fact] public void ContactAllowsMove_GroundedAndIdle_DoesNotClearPreArmedStandingLongJump() { // contact_allows_move must not touch the flag at all — verify it // doesn't clear a flag armed by a prior ChargeJump call either. var body = MakeGrounded(); var interp = MakeInterp(body); interp.StandingLongJump = true; interp.contact_allows_move(MotionCommand.WalkForward); Assert.True(interp.StandingLongJump, "contact_allows_move must not clear an externally-armed StandingLongJump flag"); } // ========================================================================= // get_jump_v_z — 0x00527aa0 @304953 (A5; closes J16-epsilons) // Epsilon: 0.000199999995f (NOT the old 0.001 JumpExtentEpsilon). // ========================================================================= [Fact] public void GetJumpVZ_EpsilonIsRetailLiteral() { Assert.Equal(0.000199999995f, MotionInterpreter.JumpVzEpsilon); } [Fact] public void GetJumpVZ_JustBelowEpsilon_ReturnsZero() { var weenie = new FakeWeenie { JumpVz = 10.0f }; var interp = MakeInterp(weenie: weenie); interp.JumpExtent = 0.0001f; // < 0.000199999995f Assert.Equal(0f, interp.GetJumpVZ(), precision: 6); } [Fact] public void GetJumpVZ_JustAboveEpsilon_DelegatesToWeenie() { var weenie = new FakeWeenie { JumpVz = 10.0f }; var interp = MakeInterp(weenie: weenie); interp.JumpExtent = 0.0003f; // > 0.000199999995f float vz = interp.GetJumpVZ(); // Not the zero-fallback: delegates to InqJumpVelocity(extent, ...). Assert.Equal(weenie.JumpVz * 0.0003f, vz, precision: 4); } [Fact] public void GetJumpVZ_ExactlyOldWrongEpsilon_0_001_IsAboveRetailEpsilon_NotZero() { // Regression pin: the OLD (wrong) epsilon was 0.001. At extent = // 0.0005 the old code would have returned 0 (0.0005 < 0.001), but // retail's epsilon (0.0002) means 0.0005 must NOT hit the zero path. var weenie = new FakeWeenie { JumpVz = 10.0f }; var interp = MakeInterp(weenie: weenie); interp.JumpExtent = 0.0005f; float vz = interp.GetJumpVZ(); Assert.NotEqual(0f, vz); Assert.Equal(weenie.JumpVz * 0.0005f, vz, precision: 4); } [Fact] public void GetJumpVZ_NoWeenie_ReturnsDefault() { var interp = MakeInterp(); interp.JumpExtent = 0.5f; Assert.Equal(MotionInterpreter.DefaultJumpVz, interp.GetJumpVZ(), precision: 4); } [Fact] public void GetJumpVZ_ExtentClampsAtMax1() { var weenie = new FakeWeenie { JumpVz = 10.0f }; var interp = MakeInterp(weenie: weenie); interp.JumpExtent = 5.0f; // over-clamped float vz = interp.GetJumpVZ(); Assert.Equal(10.0f, vz, precision: 4); } [Fact] public void GetJumpVZ_WeenieRefusesInqJumpVelocity_ReturnsZero() { var weenie = new FakeWeenie { InqJumpVelocityResult = false }; var interp = MakeInterp(weenie: weenie); interp.JumpExtent = 0.5f; Assert.Equal(0f, interp.GetJumpVZ(), precision: 5); } // ========================================================================= // get_leave_ground_velocity — 0x005280c0 @305404 (A6; closes J16-epsilons) // ========================================================================= [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.GetLeaveGroundVelocity(); 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_TrulyZero_FallsBackToWorldVelocityGlobalToLocal() { // A6: fallback fires ONLY when |x| AND |y| AND |z| are ALL < // epsilon. When it fires, ALL THREE components (including the // just-computed jump v_z) are overwritten with // globaltolocal(m_velocityVector) — the existing // Quaternion.Inverse(Orientation) transform IS global->local; keep. var body = MakeGrounded(); body.Velocity = new Vector3(2.0f, 3.0f, 0f); // nonzero WORLD velocity (momentum) body.Orientation = Quaternion.Identity; // identity: local == global for this pin var interp = new MotionInterpreter(body) { JumpExtent = 0f, // below epsilon -> get_jump_v_z returns 0 }; // Default interpreted state = Ready (no walk/run/sidestep command) -> XY also 0. var vel = interp.GetLeaveGroundVelocity(); // Fallback must have fired: velocity now reflects body.Velocity // (global->local via inverse-identity == identity here), NOT the // all-zero get_state_velocity()/get_jump_v_z() composition. Assert.Equal(2.0f, vel.X, precision: 4); Assert.Equal(3.0f, vel.Y, precision: 4); Assert.Equal(0.0f, vel.Z, precision: 4); } [Fact] public void GetLeaveGroundVelocity_TrulyZero_NoWorldVelocity_StaysZero() { var body = MakeGrounded(); body.Velocity = Vector3.Zero; var interp = new MotionInterpreter(body) { JumpExtent = 0f, }; var vel = interp.GetLeaveGroundVelocity(); Assert.Equal(0f, vel.X, precision: 5); Assert.Equal(0f, vel.Y, precision: 5); Assert.Equal(0f, vel.Z, precision: 5); } [Fact] public void GetLeaveGroundVelocity_NonzeroXOnly_DoesNotTriggerFallback() { // A6: the fallback requires ALL THREE axes below epsilon. A // sidestep-only launch (nonzero X, zero Y, zero jump Z) must NOT // trigger the momentum fallback — it should keep the computed X. var body = MakeGrounded(); body.Velocity = new Vector3(99f, 99f, 99f); // if fallback wrongly fired, this would show var interp = new MotionInterpreter(body) { JumpExtent = 0f, // jump v_z stays 0 }; interp.InterpretedState.SideStepCommand = MotionCommand.SideStepRight; interp.InterpretedState.SideStepSpeed = 1.0f; var vel = interp.GetLeaveGroundVelocity(); // X must be the sidestep-derived value, not 99 (fallback did not fire). Assert.NotEqual(99f, vel.X); Assert.True(vel.X > 0f, "sidestep should have produced nonzero X from get_state_velocity"); } [Fact] public void GetLeaveGroundVelocity_EpsilonBoundary_JustBelow_TriggersFallback() { var body = MakeGrounded(); body.Velocity = new Vector3(5f, 0f, 0f); body.Orientation = Quaternion.Identity; var interp = new MotionInterpreter(body) { JumpExtent = 0.0001f, // < 0.000199999995f -> get_jump_v_z() == 0 }; // Default interpreted state -> XY == 0 too. All three axes are // exactly at the "essentially zero" edge -> fallback fires. var vel = interp.GetLeaveGroundVelocity(); Assert.Equal(5f, vel.X, precision: 4); } // ========================================================================= // jump_is_allowed — 0x005282b0 (A2, A10; closes J5) // // Raw entry shape (r3-motioninterp-decomp.md §3h): // if (physics_obj != 0) { // if (weenie != 0) eax_2 = weenie->IsCreature(); // if (weenie != 0 && eax_2 == 0) // non-creature weenie // goto shared_gate; // if (physics_obj == 0 || (state bit 0x400) == 0) // gravity-state off // goto shared_gate; // if (Contact && OnWalkable) // grounded // goto shared_gate; // } // return 0x24; // // shared_gate: // if (IsFullyConstrained) return 0x47; // head = pending_motions.head_; // if (head != 0) eax_6 = head.jump_error_code; // if (head == 0 || eax_6 == 0) { // eax_6 = jump_charge_is_allowed(); // if (eax_6 == 0) { // eax_7 = motion_allows_jump(interpreted_state.forward_command); // if (eax_7 != 0) return eax_7; // if (weenie_obj_1 == 0) return eax_7; // == 0 here // eax_6 = 0x47; // if (weenie_obj_1->JumpStaminaCost(extent, &cost) != 0) // return eax_7; // == 0 (success) // // JumpStaminaCost returned 0 (false) -> falls through, eax_6 stays 0x47 // } // } // return eax_6; // ========================================================================= [Fact] public void JumpIsAllowed_NullPhysicsObj_ReturnsNotGrounded() { var interp = new MotionInterpreter(); var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.NotGrounded, result); } [Fact] public void JumpIsAllowed_CreatureWeenie_Airborne_GravityState_ReturnsNotGrounded() { var body = MakeAirborne(); var interp = MakeInterp(body); var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.NotGrounded, result); } [Fact] public void JumpIsAllowed_CreatureWeenie_Grounded_ReturnsNone() { var body = MakeGrounded(); var interp = MakeInterp(body); var result = interp.jump_is_allowed(0.5f, out int cost); Assert.Equal(WeenieError.None, result); Assert.Equal(0, cost); } [Fact] public void JumpIsAllowed_NonCreatureWeenie_Airborne_SkipsGroundGate_ReturnsNone() { // weenie present && !IsCreature() -> skip the ground check entirely, // go straight to the shared gate. Airborne here must NOT block. var weenie = new FakeWeenie(); var body = MakeAirborne(); var interp = MakeInterp(body, weenie); // Non-creature: use an explicit fake that overrides IsCreature. var nonCreature = new NonCreatureFakeWeenie(); interp.WeenieObj = nonCreature; var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.None, result); } [Fact] public void JumpIsAllowed_NoWeenie_GravityStateOff_SkipsGroundGate_ReturnsNone() { // "no weenie" -> weenie_obj == 0 branch is fine either way (the // `weenie != 0 && eax_2 == 0` non-creature-skip only applies when a // weenie exists); but the SEPARATE gravity-state-off skip // (state bit 0x400 clear) also reaches the shared gate regardless // of weenie presence. Pin: no gravity flag -> shared gate, not // blocked outright. var body = new PhysicsBody { State = PhysicsStateFlags.None, // gravity flag clear TransientState = TransientStateFlags.None, }; var interp = MakeInterp(body); var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.None, result); } [Fact] public void JumpIsAllowed_IsFullyConstrained_ReturnsGeneralMovementFailure() { var body = MakeGrounded(); body.IsFullyConstrained = true; var interp = MakeInterp(body); var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.GeneralMovementFailure, result); // 0x47 } [Fact] public void JumpIsAllowed_IsFullyConstrained_BeatsPendingHeadPeek() { // A2 ordering: IsFullyConstrained is checked BEFORE the pending-head // peek. Even if the head carries a DIFFERENT nonzero error, the // IsFullyConstrained code (0x47) must win. var body = MakeGrounded(); body.IsFullyConstrained = true; var interp = MakeInterp(body); interp.AddToQueue(0, MotionCommand.WalkForward, (uint)WeenieError.CantJumpLoadedDown); var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.GeneralMovementFailure, result); } [Fact] public void JumpIsAllowed_PendingHeadNonzeroError_ShortCircuitsChain() { // A2: the peek fires WHENEVER the queue is non-empty (no Count>1 // gate). A nonzero head.JumpErrorCode is returned VERBATIM, and the // charge/motion/stamina chain is skipped entirely (proven via the // weenie's CanJump/JumpStaminaCost never being consulted for THIS // code path — CanJump would return CantJumpLoadedDown some other // way, so instead assert JumpStaminaCost, which only the fallthrough // chain calls, is never invoked). var weenie = new FakeWeenie(); var body = MakeGrounded(); var interp = MakeInterp(body, weenie); interp.AddToQueue(0, MotionCommand.WalkForward, (uint)WeenieError.YouCantJumpFromThisPosition); var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result); Assert.Equal(0, weenie.JumpStaminaCostCalls); } [Fact] public void JumpIsAllowed_PendingHeadZeroError_FallsThroughToChain() { // A2: head exists but JumpErrorCode == 0 -> chain still runs. var body = MakeGrounded(); var interp = MakeInterp(body); interp.AddToQueue(0, MotionCommand.WalkForward, jumpErrorCode: 0); var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.None, result); } [Fact] public void JumpIsAllowed_EmptyQueue_FallsThroughToChain() { var body = MakeGrounded(); var interp = MakeInterp(body); // No AddToQueue call -> head is null -> chain runs normally. var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.None, result); } [Fact] public void JumpIsAllowed_ChargeBlocked_Fallen_ReturnsYouCantJumpFromThisPosition() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.InterpretedState.ForwardCommand = MotionCommand.Fallen; var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result); } [Fact] public void JumpIsAllowed_ChargeBlocked_WeenieCanJumpFalse_ReturnsCantJumpLoadedDown() { var weenie = new FakeWeenie { CanJumpResult = false }; var body = MakeGrounded(); var interp = MakeInterp(body, weenie); var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.CantJumpLoadedDown, result); } [Fact] public void JumpIsAllowed_MotionAllowsJumpBlocks_ReturnsYouCantJumpFromThisPosition() { // jump_charge_is_allowed passes (Ready forward command), but the // SEPARATE motion_allows_jump(forward_command) check blocks — use a // forward command in the [0x4000001e, 0x40000039] blocklist band // that jump_charge_is_allowed does NOT gate on (only Fallen/Crouch // range does), so this pins the double-check shape. var body = MakeGrounded(); var interp = MakeInterp(body); interp.InterpretedState.ForwardCommand = 0x40000020u; // inside [0x4000001e, 0x40000039] var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.YouCantJumpFromThisPosition, result); } [Fact] public void JumpIsAllowed_NoWeenie_PassesChain_ReturnsNone() { // motion_allows_jump passes and weenie_obj == null -> return eax_7 // (== 0) directly, skipping the JumpStaminaCost consult entirely. var body = MakeGrounded(); var interp = MakeInterp(body); // no weenie var result = interp.jump_is_allowed(0.5f, out int cost); Assert.Equal(WeenieError.None, result); Assert.Equal(0, cost); } [Fact] public void JumpIsAllowed_WeenieJumpStaminaCostRefuses_ReturnsGeneralMovementFailure() { var weenie = new FakeWeenie { JumpStaminaCostResult = false }; var body = MakeGrounded(); var interp = MakeInterp(body, weenie); var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.GeneralMovementFailure, result); // 0x47 Assert.Equal(1, weenie.JumpStaminaCostCalls); } [Fact] public void JumpIsAllowed_WeenieJumpStaminaCostAffords_ReturnsNone() { var weenie = new FakeWeenie { JumpStaminaCostResult = true }; var body = MakeGrounded(); var interp = MakeInterp(body, weenie); var result = interp.jump_is_allowed(0.5f, out _); Assert.Equal(WeenieError.None, result); Assert.Equal(1, weenie.JumpStaminaCostCalls); } /// Fake weenie whose IsCreature() returns false, for the /// non-creature ground-gate-skip pin. private sealed class NonCreatureFakeWeenie : IWeenieObject { public bool InqJumpVelocity(float extent, out float vz) { vz = 10f; return true; } public bool InqRunRate(out float rate) { rate = 1f; return true; } public bool CanJump(float extent) => true; public bool IsCreature() => false; } // ========================================================================= // jump — 0x00528780 @305792 (closes J7-interp-side) // ========================================================================= [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_ReturnsNotGrounded() { var body = MakeAirborne(); var interp = MakeInterp(body); var result = interp.jump(0.5f); Assert.Equal(WeenieError.NotGrounded, result); } [Fact] public void Jump_WeenieBlocksJump_ClearsStandingLongJump() { 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, "a failed jump attempt cancels any pending StandingLongJump"); } [Fact] public void Jump_NullPhysicsObj_ReturnsNoPhysicsObject() { var interp = new MotionInterpreter(); var result = interp.jump(0.5f); Assert.Equal(WeenieError.NoPhysicsObject, result); } [Fact] public void Jump_Success_DoesNotClearStandingLongJump() { // Only the FAILURE path clears StandingLongJump (raw 305800: // `this->standing_longjump = 0;` sits inside the `if (result != 0)` // branch only). A successful jump leaves whatever value the flag // already had. var body = MakeGrounded(); var interp = MakeInterp(body); interp.StandingLongJump = true; var result = interp.jump(0.5f); Assert.Equal(WeenieError.None, result); Assert.True(interp.StandingLongJump, "success path must not touch StandingLongJump"); } [Fact] public void Jump_CallsInterruptCurrentMovementSeam() { // J7 / "Adjacent findings": jump() ALWAYS calls // interrupt_current_movement first (raw @305800), regardless of // outcome. Register no-op seam until R4's cancel_moveto exists. var body = MakeGrounded(); var interp = MakeInterp(body); bool called = false; interp.InterruptCurrentMovement = () => called = true; interp.jump(0.5f); Assert.True(called, "jump() must invoke the InterruptCurrentMovement seam"); } [Fact] public void Jump_CallsInterruptCurrentMovementSeam_EvenOnFailure() { var body = MakeAirborne(); var interp = MakeInterp(body); bool called = false; interp.InterruptCurrentMovement = () => called = true; interp.jump(0.5f); Assert.True(called, "jump() calls interrupt_current_movement before jump_is_allowed, unconditionally"); } // ========================================================================= // IWeenieObject.IsThePlayer — default false; PlayerWeenie true (for W4's // A3 dispatch gate; no consumer yet in W3). // ========================================================================= [Fact] public void IWeenieObject_IsThePlayer_DefaultsFalse() { // Default interface members are only reachable through the // interface type, not the concrete implementer — call via // IWeenieObject to exercise the DIM (not a compile error site). IWeenieObject weenie = new FakeWeenie(); Assert.False(weenie.IsThePlayer()); } [Fact] public void PlayerWeenie_IsThePlayer_ReturnsTrue() { var weenie = new PlayerWeenie(); Assert.True(weenie.IsThePlayer()); } }