using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using Xunit; namespace AcDream.Core.Tests.Physics.Motion; /// /// R4-V2 — UseTime (0052a780, raw 307776-307798, decomp §6a): /// the three-gate tick matrix (grounded / node-exists / object-move /// initialized), including the uninitialized type-6 stall case from the /// port plan's V2 test list. /// public sealed class MoveToManagerUseTimeGateTests { [Fact] public void NoNodeQueued_UseTimeIsANoOp() { var h = new MoveToManagerHarness(); // Fresh manager: no active move, no nodes. h.Manager.UseTime(); Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState); } [Fact] public void NotGrounded_ContactFalse_UseTimeDoesNothing_EvenWithNodesQueued() { var h = new MoveToManagerHarness { ContactValue = false }; h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity); h.Heading = 90f; // face the target so BeginMoveForward runs (no turn-to-face node needed) h.Manager.MoveToPosition(new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity), new MovementParameters()); h.DrainPendingMotions(); uint commandBefore = h.Manager.CurrentCommand; // Move the mover without letting UseTime process it (Contact=false blocks the gate). h.WorldPosition = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity); h.Advance(5.0); h.Manager.UseTime(); // State machine did not advance -- still the same command, same type. Assert.Equal(commandBefore, h.Manager.CurrentCommand); Assert.Equal(MovementType.MoveToPosition, h.Manager.MovementTypeState); } [Fact] public void Grounded_MoveToPositionNode_DispatchesToHandleMoveToPosition() { var h = new MoveToManagerHarness { ContactValue = true }; h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity); h.Heading = 90f; // face the target so BeginMoveForward runs (no turn-to-face node needed) h.Manager.MoveToPosition(new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity), new MovementParameters { DistanceToObject = 0.6f, UseSpheres = false }); h.DrainPendingMotions(); // Arrived: move the mover close to the TARGET (20,0,0), well within // DistanceToObject, and advance time so CheckProgressMade evaluates // true and the arrival branch pops. h.WorldPosition = new Position(1u, new Vector3(19.7f, 0f, 0f), Quaternion.Identity); h.Advance(2.0); h.Manager.UseTime(); Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState); // HandleMoveToPosition ran and completed the move. } [Fact] public void Grounded_TurnToHeadingNode_DispatchesToHandleTurnToHeading() { var h = new MoveToManagerHarness { ContactValue = true }; h.Heading = 0f; h.Manager.TurnToHeading(new MovementParameters { DesiredHeading = 90f }); h.DrainPendingMotions(); Assert.Equal(MotionCommand.TurnRight, h.Manager.CurrentCommand); h.Heading = 91f; // "passed" the target h.Manager.UseTime(); Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState); // HandleTurnToHeading ran and completed the turn. } [Fact] public void ObjectMove_UninitializedType6_StallsUntilFirstTargetCallback() { // The port plan's named "uninitialized type-6 stall" case: a // MoveToObject manager with TopLevelObjectId != 0 and // MovementTypeState != Invalid, but Initialized still false (no // HandleUpdateTarget callback has arrived yet) -- and CRITICALLY, // no node is queued yet either (MoveToObject defers node-building // to the first callback, §3b), so UseTime's node-exists gate (gate // 2) already blocks it. This test proves the stall holds even if a // node WERE somehow present (defense in depth for gate 3). var h = new MoveToManagerHarness { ContactValue = true }; h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity); h.Manager.MoveToObject(0x5000AAAAu, 0x5000AAAAu, 1f, 2f, new MovementParameters()); Assert.False(h.Manager.Initialized); Assert.Empty(h.Manager.PendingActions); // gate 2 alone already stalls it h.Manager.UseTime(); // No crash, no state change -- the manager is still waiting. Assert.Equal(MovementType.MoveToObject, h.Manager.MovementTypeState); Assert.False(h.Manager.Initialized); } [Fact] public void ObjectMove_Initialized_PassesGate3_ProcessesNormally() { var h = new MoveToManagerHarness { ContactValue = true }; h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity); h.Heading = 90f; // face the target so the internal node plan skips the turn-to-face step h.Manager.MoveToObject(0x5000BBBBu, 0x5000BBBBu, radius: 0.5f, height: 2f, new MovementParameters { UseSpheres = false }); var target = new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity); h.Manager.HandleUpdateTarget(new TargetInfo(0x5000BBBBu, TargetStatus.Ok, target, target)); Assert.True(h.Manager.Initialized); h.DrainPendingMotions(); h.WorldPosition = new Position(1u, new Vector3(19.5f, 0f, 0f), Quaternion.Identity); // within DistanceToObject default 0.6 h.Advance(2.0); h.Manager.UseTime(); Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState); // completed via UseTime -> HandleMoveToPosition. } [Fact] public void NonObjectMove_TopLevelIdZero_Gate3AlwaysPasses_RegardlessOfInitialized() { // Gate 3: (top_level_object_id == 0 || movement_type == Invalid) || // initialized. Position/heading moves never set TopLevelObjectId, // so the FIRST disjunct alone always satisfies gate 3 -- Initialized // staying false (as it does for MoveToPosition/TurnToHeading, per // §3c/§3e's notes) never blocks them. var h = new MoveToManagerHarness { ContactValue = true }; h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity); h.Heading = 90f; h.Manager.MoveToPosition(new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity), new MovementParameters { DistanceToObject = 0.6f, UseSpheres = false }); h.DrainPendingMotions(); Assert.Equal(0u, h.Manager.TopLevelObjectId); Assert.False(h.Manager.Initialized); h.WorldPosition = new Position(1u, new Vector3(19.7f, 0f, 0f), Quaternion.Identity); h.Advance(2.0); h.Manager.UseTime(); Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState); // gate 3 passed via the first disjunct. } }