using System.Linq; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using Xunit; namespace AcDream.Core.Tests.Physics; // ───────────────────────────────────────────────────────────────────────────── // MotionInterpreterGroundLifecycleTests — R3-W4 (closes J8, J10, J11-shape, // J12, J13; J18 one-liner rides along). // // Oracle: docs/research/2026-07-02-r3-motioninterp/r3-motioninterp-decomp.md // §4 (HitGround 0x00528ac0 @305996, LeaveGround 0x00528b00 @306022, // ReportExhaustion 0x005288d0 @305861, enter_default_state 0x00528c80 // @306124) + §5c/5d (set_hold_run 0x00528b70 @306053, SetHoldKey 0x00528bb0 // @306072) + raw acclient_2013_pseudo_c.txt:305838-305932 (apply_current_movement // 0x00528870, SetWeenieObject 0x00528920, SetPhysicsObject 0x00528970) + // W0-pins.md A3 (dual-dispatch gate) / A8 (enter_default_state no-clear). // ───────────────────────────────────────────────────────────────────────────── /// Fake WeenieObject — IsThePlayer/IsCreature independently /// configurable, for the A3 dispatch truth table. file sealed class FakeWeenie : IWeenieObject { public bool IsThePlayerResult; public bool IsCreatureResult = true; 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 IsThePlayer() => IsThePlayerResult; public bool IsCreature() => IsCreatureResult; } public sealed class MotionInterpreterGroundLifecycleTests { // ── 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); } // ========================================================================= // HitGround — 0x00528ac0 @305996 // ========================================================================= [Fact] public void HitGround_NoPhysicsObj_NoOp() { var interp = new MotionInterpreter(); bool called = false; interp.RemoveLinkAnimations = () => called = true; interp.HitGround(); Assert.False(called); } [Fact] public void HitGround_NonCreatureWeenie_NoOp() { // raw 305720-305724: (weenie_obj == 0 || eax_2 != 0) -- a WEENIE // present whose IsCreature() returns false skips the whole body. var weenie = new FakeWeenie { IsCreatureResult = false }; var body = MakeGrounded(); var interp = MakeInterp(body, weenie); bool called = false; interp.RemoveLinkAnimations = () => called = true; interp.HitGround(); Assert.False(called, "a non-creature weenie must skip HitGround's body entirely"); } [Fact] public void HitGround_NoWeenie_Proceeds() { // weenie_obj == 0 -> the OR short-circuits true, body proceeds. var body = MakeGrounded(); var interp = MakeInterp(body); bool called = false; interp.RemoveLinkAnimations = () => called = true; interp.HitGround(); Assert.True(called, "no weenie_obj must still proceed (weenie==0 half of the OR)"); } [Fact] public void HitGround_GravityFlagClear_NoOp() { // raw 305726-305730: state bit 0x400 (Gravity) gates the body. var body = MakeGrounded(); body.State &= ~PhysicsStateFlags.Gravity; var interp = MakeInterp(body); bool called = false; interp.RemoveLinkAnimations = () => called = true; interp.HitGround(); Assert.False(called, "HitGround must no-op when the Gravity state flag is clear"); } [Fact] public void HitGround_CreatureWithGravity_CallsRemoveLinkAnimationsThenReapplies() { var weenie = new FakeWeenie { IsCreatureResult = true }; var body = MakeGrounded(); var interp = MakeInterp(body, weenie); interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.InterpretedState.ForwardSpeed = 1.0f; bool called = false; interp.RemoveLinkAnimations = () => called = true; interp.HitGround(); Assert.True(called, "HitGround must call the RemoveLinkAnimations seam"); // apply_current_movement(false, true) re-syncs velocity from the // current interpreted state (grounded write, AP-75-adjacent). Assert.True(body.Velocity.Length() > 0f, "HitGround must re-apply current movement"); } // ========================================================================= // LeaveGround — 0x00528b00 @306022 // ========================================================================= [Fact] public void LeaveGround_NoPhysicsObj_NoOp() { var interp = new MotionInterpreter(); bool called = false; interp.RemoveLinkAnimations = () => called = true; interp.LeaveGround(); Assert.False(called); } [Fact] public void LeaveGround_NonCreatureWeenie_NoOp() { var weenie = new FakeWeenie { IsCreatureResult = false }; var body = MakeGrounded(); var interp = MakeInterp(body, weenie); interp.StandingLongJump = true; interp.JumpExtent = 0.7f; bool called = false; interp.RemoveLinkAnimations = () => called = true; interp.LeaveGround(); Assert.False(called); // Nothing in the body ran -- StandingLongJump/JumpExtent untouched. Assert.True(interp.StandingLongJump); Assert.Equal(0.7f, interp.JumpExtent); } [Fact] public void LeaveGround_GravityFlagClear_NoOp() { var body = MakeGrounded(); body.State &= ~PhysicsStateFlags.Gravity; var interp = MakeInterp(body); interp.StandingLongJump = true; bool called = false; interp.RemoveLinkAnimations = () => called = true; interp.LeaveGround(); Assert.False(called); Assert.True(interp.StandingLongJump); } [Fact] public void LeaveGround_CreatureWithGravity_SetsVelocityAndResetsJumpState() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.StandingLongJump = true; interp.JumpExtent = 0.5f; interp.LeaveGround(); Assert.False(interp.StandingLongJump, "standing_longjump = 0 on leave-ground"); Assert.Equal(0f, interp.JumpExtent, precision: 5); } [Fact] public void LeaveGround_CallsRemoveLinkAnimationsThenReapplies() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.InterpretedState.ForwardSpeed = 1.0f; bool called = false; interp.RemoveLinkAnimations = () => called = true; interp.LeaveGround(); Assert.True(called, "LeaveGround must call the RemoveLinkAnimations seam"); } [Fact] public void LeaveGround_VelocityCarriesGetLeaveGroundVelocity_WithNonzeroJumpExtent() { // A6/A5: get_leave_ground_velocity composes get_state_velocity() + // z=GetJumpVZ(). With JumpExtent set and a weenie present, the body // local velocity written must match GetLeaveGroundVelocity()'s Z. // // Real call-site precondition: by the time LeaveGround fires, the // body has ALREADY left the ground (jump() calls set_on_walkable(false) // before LeaveGround runs; a ledge walk-off similarly clears OnWalkable // via the physics resolver BEFORE this callback). With OnWalkable // false, LeaveGround's own tail apply_current_movement(0,0) call // takes the "airborne -- preserve integrated velocity" branch in the // AP-77 adaptation (ApplyCurrentMovementInterpreted), so the Z this // call just set is not immediately clobbered by a resync. var weenie = new AcDream.Core.Physics.PlayerWeenie(); var body = MakeGrounded(); body.set_on_walkable(false); var interp = MakeInterp(body, weenie); interp.JumpExtent = 0.5f; var expected = interp.GetLeaveGroundVelocity(); interp.LeaveGround(); // set_local_velocity transforms local->world via Orientation // (Identity in these fixtures), so world == local here. Assert.Equal(expected.Z, body.Velocity.Z, precision: 3); } [Fact] public void LeaveGround_SetLocalVelocity_UsesAutonomousFlag() { // raw 305763-305765: CPhysicsObj::set_local_velocity(&var_c, 1) -- // the autonomous=1 arg. PhysicsBody.LastMoveWasAutonomous should // reflect this after LeaveGround runs (movement_is_autonomous, A3's // apply_current_movement dispatch reads this same flag). var body = MakeGrounded(); body.LastMoveWasAutonomous = false; var interp = MakeInterp(body); interp.LeaveGround(); Assert.True(body.LastMoveWasAutonomous, "LeaveGround's set_local_velocity call carries autonomous=1"); } // ========================================================================= // enter_default_state — 0x00528c80 @306124 (A8: append sentinel, NO drain) // ========================================================================= [Fact] public void EnterDefaultState_AppendsReadySentinel_WithoutDrainingExistingNodes() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.AddToQueue(1, MotionCommand.RunForward, 0); interp.AddToQueue(2, MotionCommand.WalkForward, 0x48); interp.EnterDefaultState(); var nodes = interp.PendingMotions.ToArray(); Assert.Equal(3, nodes.Length); Assert.Equal(new MotionNode(1, MotionCommand.RunForward, 0), nodes[0]); Assert.Equal(new MotionNode(2, MotionCommand.WalkForward, 0x48), nodes[1]); Assert.Equal(new MotionNode(0, MotionCommand.Ready, 0), nodes[2]); } [Fact] public void EnterDefaultState_SetsInitted() { var interp = new MotionInterpreter { PhysicsObj = MakeGrounded() }; // Force Initted false to prove EnterDefaultState is what flips it // (the ctor already defaults it true -- see the Initted-gating // section below for why). interp.Initted = false; interp.EnterDefaultState(); Assert.True(interp.Initted); } [Fact] public void EnterDefaultState_ResetsRawAndInterpretedStateToCtorDefaults() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.ForwardCommand = MotionCommand.RunForward; interp.RawState.ForwardSpeed = 2.94f; interp.RawState.CurrentHoldKey = HoldKey.Run; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.TurnCommand = MotionCommand.TurnRight; interp.EnterDefaultState(); Assert.Equal(MotionCommand.Ready, interp.RawState.ForwardCommand); Assert.Equal(1.0f, interp.RawState.ForwardSpeed); Assert.Equal(HoldKey.None, interp.RawState.CurrentHoldKey); Assert.Equal(MotionCommand.Ready, interp.InterpretedState.ForwardCommand); Assert.Equal(0u, interp.InterpretedState.TurnCommand); } [Fact] public void EnterDefaultState_CallsInitializeMotionTablesSeam() { var interp = MakeInterp(); bool called = false; interp.InitializeMotionTables = () => called = true; interp.EnterDefaultState(); Assert.True(called); } [Fact] public void EnterDefaultState_TailCallsLeaveGround() { // enter_default_state's LAST step is an unconditional LeaveGround() // call (raw @306153). With a grounded creature+gravity body and a // nonzero JumpExtent pre-seeded, LeaveGround's reset (standing_longjump=0, // jump_extent=0) must be observable after EnterDefaultState. var body = MakeGrounded(); var interp = MakeInterp(body); interp.StandingLongJump = true; interp.JumpExtent = 0.9f; interp.EnterDefaultState(); Assert.False(interp.StandingLongJump, "EnterDefaultState's LeaveGround tail must fire"); Assert.Equal(0f, interp.JumpExtent, precision: 5); } // ========================================================================= // Initted gating — apply_current_movement / ReportExhaustion (A3 entry gate) // ========================================================================= [Fact] public void ApplyCurrentMovement_NotInitted_NoOp() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.Initted = false; interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.apply_current_movement(cancelMoveTo: false, allowJump: false); Assert.Equal(System.Numerics.Vector3.Zero, body.Velocity); } [Fact] public void ApplyCurrentMovement_Initted_Proceeds() { var body = MakeGrounded(); var interp = MakeInterp(body); Assert.True(interp.Initted, "the two-arg constructor defaults Initted=true (see final report)"); interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.apply_current_movement(cancelMoveTo: false, allowJump: false); Assert.True(body.Velocity.Length() > 0f); } [Fact] public void ReportExhaustion_NotInitted_NoOp() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.Initted = false; interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.ReportExhaustion(); Assert.Equal(System.Numerics.Vector3.Zero, body.Velocity); } [Fact] public void ReportExhaustion_NoPhysicsObj_NoOp() { var interp = new MotionInterpreter(); // No exception is the assertion -- retail's entry gate is // physics_obj != 0 && initted != 0, both ANDed. interp.ReportExhaustion(); } // ========================================================================= // Dual-dispatch truth table — A3: IsThePlayer && movement_is_autonomous // gates apply_raw_movement vs apply_interpreted_movement, for // apply_current_movement AND ReportExhaustion. // // Reading raw_movement's effect: apply_raw_movement copies RawState -> // InterpretedState (see apply_raw_movement's existing doc comment) then // re-normalizes via adjust_motion. apply_interpreted_movement instead // reads InterpretedState directly and pushes velocity via // get_state_velocity/set_local_velocity when grounded. We distinguish // the two dispatch targets by seeding RawState and InterpretedState with // DIFFERENT forward commands/speeds and observing which one drove the // resulting body velocity. // ========================================================================= [Theory] [InlineData(true, true, /* expectRaw */ true)] // IsThePlayer && autonomous -> raw [InlineData(true, false, /* expectRaw */ false)] // IsThePlayer but not autonomous -> interpreted [InlineData(false, true, /* expectRaw */ false)] // autonomous but NOT the player -> interpreted (remote player IS a creature but not "the player") [InlineData(false, false, /* expectRaw */ false)] // neither -> interpreted public void ApplyCurrentMovement_DualDispatch_MatchesA3TruthTable( bool isThePlayer, bool autonomous, bool expectRaw) { var weenie = new FakeWeenie { IsThePlayerResult = isThePlayer, IsCreatureResult = true }; var body = MakeGrounded(); body.LastMoveWasAutonomous = autonomous; var interp = MakeInterp(body, weenie); // RawState drives WalkForward (speed 1 => WalkAnimSpeed); InterpretedState // drives RunForward (speed 1 => RunAnimSpeed). WalkAnimSpeed != RunAnimSpeed // so the resulting body.Velocity.Y distinguishes which path ran. interp.RawState.ForwardCommand = MotionCommand.WalkForward; interp.RawState.ForwardSpeed = 1.0f; interp.RawState.ForwardHoldKey = HoldKey.None; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.apply_current_movement(cancelMoveTo: false, allowJump: false); float expectedY = expectRaw ? MotionInterpreter.WalkAnimSpeed // apply_raw_movement re-derives from RawState : MotionInterpreter.RunAnimSpeed; // apply_interpreted_movement reads InterpretedState as-is Assert.Equal(expectedY, body.Velocity.Y, precision: 2); } [Fact] public void ApplyCurrentMovement_NoWeenie_Autonomous_DispatchesRaw() { // weenie_obj == 0 short-circuits the OR to true (raw 305849: // weenie_obj == 0 || eax_2 != 0) -- no weenie means "treat as the // player" for dispatch purposes. var body = MakeGrounded(); body.LastMoveWasAutonomous = true; var interp = MakeInterp(body); // no weenie interp.RawState.ForwardCommand = MotionCommand.WalkForward; interp.RawState.ForwardSpeed = 1.0f; interp.RawState.ForwardHoldKey = HoldKey.None; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.apply_current_movement(cancelMoveTo: false, allowJump: false); Assert.Equal(MotionInterpreter.WalkAnimSpeed, body.Velocity.Y, precision: 2); } [Fact] public void ReportExhaustion_DualDispatch_PassesZeroZeroArgs() { // A3: ReportExhaustion's dispatch args are hardcoded (0, 0) -- // distinct from apply_current_movement which passes its own // (cancelMoveTo, allowJump) through. We can't observe the args // directly (both paths are void), so this proves ReportExhaustion // dispatches at all when initted+player+autonomous (raw path) by // observing the same velocity-divergence trick. var weenie = new FakeWeenie { IsThePlayerResult = true }; var body = MakeGrounded(); body.LastMoveWasAutonomous = true; var interp = MakeInterp(body, weenie); interp.RawState.ForwardCommand = MotionCommand.WalkForward; interp.RawState.ForwardSpeed = 1.0f; interp.RawState.ForwardHoldKey = HoldKey.None; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.ReportExhaustion(); Assert.Equal(MotionInterpreter.WalkAnimSpeed, body.Velocity.Y, precision: 2); } [Fact] public void ReportExhaustion_RemotePlayer_DispatchesInterpreted() { // The ACE-divergence pin: a remote player weenie (IsThePlayer=false, // IsCreature=true) must route INTERPRETED, not raw -- even though // it IS a creature. ACE's IsCreature-gated read would wrongly send // this down apply_raw_movement. var weenie = new FakeWeenie { IsThePlayerResult = false, IsCreatureResult = true }; var body = MakeGrounded(); body.LastMoveWasAutonomous = true; // even with autonomous=true... var interp = MakeInterp(body, weenie); interp.RawState.ForwardCommand = MotionCommand.WalkForward; interp.RawState.ForwardSpeed = 1.0f; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.ReportExhaustion(); // ...IsThePlayer==false forces the interpreted path. Assert.Equal(MotionInterpreter.RunAnimSpeed, body.Velocity.Y, precision: 2); } // ========================================================================= // SetWeenieObject / SetPhysicsObject — 0x00528920 / 0x00528970 re-apply // ========================================================================= [Fact] public void SetWeenieObject_WhilePhysicsBoundAndInitted_ReappliesMovement() { var body = MakeGrounded(); var interp = new MotionInterpreter { PhysicsObj = body, Initted = true }; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.SetWeenieObject(new FakeWeenie { IsThePlayerResult = false }); Assert.True(body.Velocity.Length() > 0f, "SetWeenieObject must re-apply movement when bound+initted"); } [Fact] public void SetWeenieObject_NoPhysicsObj_DoesNotReapply() { var interp = new MotionInterpreter(); interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; // Must not throw despite no PhysicsObj. interp.SetWeenieObject(new FakeWeenie()); Assert.Null(interp.PhysicsObj); } [Fact] public void SetWeenieObject_NotInitted_DoesNotReapply() { var body = MakeGrounded(); var interp = new MotionInterpreter { PhysicsObj = body, Initted = false }; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.SetWeenieObject(new FakeWeenie()); Assert.Equal(System.Numerics.Vector3.Zero, body.Velocity); } [Fact] public void SetPhysicsObject_BindsAndReappliesWhenInitted() { var interp = new MotionInterpreter { Initted = true }; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; var body = MakeGrounded(); interp.SetPhysicsObject(body); Assert.Same(body, interp.PhysicsObj); Assert.True(body.Velocity.Length() > 0f); } [Fact] public void SetPhysicsObject_NullArg_DoesNotReapply() { var interp = new MotionInterpreter { Initted = true }; // arg2 != 0 gates the reapply -- passing null must not throw and // must not attempt InterpretedState velocity work (no body to write to). interp.SetPhysicsObject(null); Assert.Null(interp.PhysicsObj); } // ========================================================================= // set_hold_run — 0x00528b70 @306053 (XOR toggle guard) // ========================================================================= [Fact] public void SetHoldRun_TogglesFromNoneToRun_WhenHoldingRunKey() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.CurrentHoldKey = HoldKey.None; interp.set_hold_run(holdingRun: true, interrupt: false); Assert.Equal(HoldKey.Run, interp.RawState.CurrentHoldKey); } [Fact] public void SetHoldRun_TogglesFromRunToNone_WhenReleasingRunKey() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.CurrentHoldKey = HoldKey.Run; interp.set_hold_run(holdingRun: false, interrupt: false); Assert.Equal(HoldKey.None, interp.RawState.CurrentHoldKey); } [Fact] public void SetHoldRun_NoChange_WhenAlreadyInRequestedState_IsANoOp() { // XOR guard: eax(=arg2==0) != edx(=current!=Run) is FALSE when // arg2 requests exactly the state we're already in -- skip. var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.CurrentHoldKey = HoldKey.Run; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; body.Velocity = System.Numerics.Vector3.Zero; interp.set_hold_run(holdingRun: true, interrupt: false); Assert.Equal(HoldKey.Run, interp.RawState.CurrentHoldKey); // No re-apply fired (still zero) -- the guard skipped the whole body. Assert.Equal(System.Numerics.Vector3.Zero, body.Velocity); } [Fact] public void SetHoldRun_OnChange_CallsApplyCurrentMovementWithInterruptArg() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.CurrentHoldKey = HoldKey.None; interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.set_hold_run(holdingRun: true, interrupt: true); // apply_current_movement(arg3, 0) fired -- observable via the // grounded velocity re-application (AP-75-adjacent write). Assert.True(body.Velocity.Length() > 0f); } // ========================================================================= // SetHoldKey — 0x00528bb0 @306072 (None-only-meaningful-from-Run) // ========================================================================= [Fact] public void SetHoldKey_None_FromRun_TakesEffect() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.CurrentHoldKey = HoldKey.Run; interp.SetHoldKey(HoldKey.None, cancelMoveTo: false); Assert.Equal(HoldKey.None, interp.RawState.CurrentHoldKey); } [Fact] public void SetHoldKey_None_FromInvalid_IsIgnored() { // raw @306072: setting None only takes effect from Run. Any other // starting state (Invalid, or already None) is silently ignored. var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.CurrentHoldKey = HoldKey.Invalid; interp.SetHoldKey(HoldKey.None, cancelMoveTo: false); Assert.Equal(HoldKey.Invalid, interp.RawState.CurrentHoldKey); } [Fact] public void SetHoldKey_AlreadyNone_IsNoOp() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.CurrentHoldKey = HoldKey.None; interp.SetHoldKey(HoldKey.None, cancelMoveTo: false); Assert.Equal(HoldKey.None, interp.RawState.CurrentHoldKey); } [Fact] public void SetHoldKey_Run_FromNone_TakesEffect() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.CurrentHoldKey = HoldKey.None; interp.SetHoldKey(HoldKey.Run, cancelMoveTo: false); Assert.Equal(HoldKey.Run, interp.RawState.CurrentHoldKey); } [Fact] public void SetHoldKey_Run_AlreadyRun_IsNoOp() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.CurrentHoldKey = HoldKey.Run; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; body.Velocity = System.Numerics.Vector3.Zero; interp.SetHoldKey(HoldKey.Run, cancelMoveTo: false); Assert.Equal(HoldKey.Run, interp.RawState.CurrentHoldKey); Assert.Equal(System.Numerics.Vector3.Zero, body.Velocity); } [Fact] public void SetHoldKey_EffectiveChange_ReappliesMovement() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.RawState.CurrentHoldKey = HoldKey.None; interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.SetHoldKey(HoldKey.Run, cancelMoveTo: false); Assert.True(body.Velocity.Length() > 0f, "an effective SetHoldKey change must reapply movement"); } // ========================================================================= // adjust_motion creature guard — J18 one-liner (retires register TS-34) // ========================================================================= [Fact] public void AdjustMotion_NonCreatureWeenie_SkipsNormalization() { var weenie = new FakeWeenie { IsCreatureResult = false }; var interp = MakeInterp(weenie: weenie); uint motion = MotionCommand.WalkBackward; float speed = 1.0f; interp.adjust_motion(ref motion, ref speed, HoldKey.Invalid); // Retail returns immediately for a non-creature weenie -- WalkBackward // must NOT be remapped to WalkForward/negated-speed. Assert.Equal(MotionCommand.WalkBackward, motion); Assert.Equal(1.0f, speed); } [Fact] public void AdjustMotion_CreatureWeenie_NormalizesAsBefore() { var weenie = new FakeWeenie { IsCreatureResult = true }; var interp = MakeInterp(weenie: weenie); uint motion = MotionCommand.WalkBackward; float speed = 1.0f; interp.adjust_motion(ref motion, ref speed, HoldKey.Invalid); Assert.Equal(MotionCommand.WalkForward, motion); Assert.Equal(-MotionInterpreter.BackwardsFactor, speed, precision: 5); } [Fact] public void AdjustMotion_NoWeenie_StillNormalizes() { // weenie == null must behave like a creature (the "always creature // unless proven otherwise" retail idiom -- weenie_obj != 0 gates // the query at all). var interp = MakeInterp(); uint motion = MotionCommand.WalkBackward; float speed = 1.0f; interp.adjust_motion(ref motion, ref speed, HoldKey.Invalid); Assert.Equal(MotionCommand.WalkForward, motion); } }