using XFEEDToolBox.Core.Navigation; using XFEEDToolBox.Core.Models; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class SystemNavigationTests { private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-22T00:00:00Z"); private static readonly SystemNavigationTarget Station=new(1,101,"Station",SystemTargetKind.Station,77); private static readonly PlanetApproachLimits Limits=new(1000000,100000,25000,2500,2500000,2500,5,60,25); private static readonly SystemNavigationTarget Surface=new(1,102,"Surface port",SystemTargetKind.SurfacePort,78,102,0,0); private static SystemFlightFrame Frame=>new(){Sequence=1,ObservedAt=Now,GameSha256=new('a',64),LoadoutFingerprint="ship",ShipEntity=1,SystemAddress=1,ReferenceWorld=101,InGame=true,GeometryVerified=true, Mode=LocalFlightMode.Normal,SelectedTarget=Station,Position=Vector3d.Zero,Forward=new(0,0,1),Up=new(0,1,0),TargetPosition=new(0,0,7000),TargetDistance=7000, MaximumNormalSpeed=300,MaximumCruiseSpeed=1e10,CruiseParametersVerified=true,CruiseApproachSeconds=6,CruiseApproachThrottle=.75, BrakingAcceleration=10,TranslationAcceleration=5,LegalApproachSpeed=100,AssistAlignmentDegrees=5}; private static DockingClearance Grant=>new(77,DockingPermission.Granted,3,Now,Now.AddMinutes(5)); [Test] public void FreshVerifiedScoCoolingModelOwnsOnlyItsSupercruiseHeatEnvelope() { var frame=Frame with{Mode=LocalFlightMode.Supercruise,TargetDistance=1e12,TargetPosition=new(0,0,1e12),Speed=1e7, HeatPercent=95,OriginalScoThermalControlVerified=true,OriginalScoThermalControlExpiresAt=Now.AddMilliseconds(100)}; Check.Equal(SystemNavigationPhase.Supercruise,new SystemFlightNavigator().Decide(frame,Station,Now).Phase); foreach(var invalid in new[]{frame with{OriginalScoThermalControlVerified=false},frame with{OriginalScoThermalControlExpiresAt=null}, frame with{OriginalScoThermalControlExpiresAt=Now.AddMilliseconds(-1)},frame with{OriginalScoThermalControlExpiresAt=Now.AddSeconds(1)}, frame with{Mode=LocalFlightMode.Normal},frame with{Assists=new(false,false,true)},frame with{SupercruiseAssist=NativeAssistState.Engaged}, frame with{HeatPercent=100}}) { var decision=new SystemFlightNavigator().Decide(invalid,Station,Now); Check.Equal(SystemNavigationPhase.Waiting,decision.Phase);Check.Equal(0d,decision.Throttle); } Check.Equal(SystemNavigationPhase.Stopped,new SystemFlightNavigator().Decide(frame with{ExternalThreat=true},Station,Now).Phase); } [Test] public void VerifiedCooldownCanAccelerateAndAlignWithoutStartingAnotherCharge() { Check.Equal(LocalFlightMode.Transition,NativeSystemFlightObservation.Mode(false,false,1)); var mode=NativeSystemFlightObservation.Mode(false,false,1,cooldownControls:true); Check.Equal(LocalFlightMode.Normal,mode); var frame=Frame with{Mode=mode,FsdReady=false,TargetDistance=1e8,TargetPosition=new(0,0,1e8)}; var decision=new SystemFlightNavigator().Decide(frame,Station,Now); Check.Equal(1d,decision.Throttle);Check.That(!decision.Actions.HasFlag(SystemNavigationAction.EnterSupercruise)); Check.That(decision.Direction is not null); } [Test] public void VerifiedCooldownInSupercruiseDoesNotBlockOrdinaryCruiseControl() { var mode=NativeSystemFlightObservation.Mode(false,true,1,cooldownControls:true); Check.Equal(LocalFlightMode.Supercruise,mode); var decision=new SystemFlightNavigator().Decide(Frame with{Mode=mode,FsdReady=false,Speed=1e7,TargetDistance=1e9,TargetPosition=new(0,0,1e9)},Station,Now); Check.Equal(SystemNavigationPhase.Supercruise,decision.Phase);Check.That(decision.Direction is not null&&decision.Throttle is >0); } [Test] public void CooldownCapabilityDoesNotPermitUnsettledFlightOrDockedTransitions() { foreach(var state in new[]{0,4,5,6,255}) Check.Equal(LocalFlightMode.Transition,NativeSystemFlightObservation.Mode(false,true,state,cooldownControls:true)); Check.Equal(LocalFlightMode.Transition,NativeSystemFlightObservation.Mode(false,false,1,true,true)); Check.Equal(LocalFlightMode.Docked,NativeSystemFlightObservation.Mode(true,false,1,true,true)); } [Test] public void SupercruiseDepartureAlignsBeforeAcceleratingAndRequestsFullThrottle() { var policy=new SystemFlightNavigator();var frame=Frame with{TargetDistance=1e8,TargetPosition=new(0,0,1e8),FsdReady=true,Forward=new(1,0,0)}; var turning=policy.Decide(frame,Station,Now);Check.Equal(0d,turning.Throttle);Check.That(!turning.Actions.HasFlag(SystemNavigationAction.EnterSupercruise)); var aligned=policy.Decide(frame with{Forward=new(0,0,1)},Station,Now);Check.Equal(1d,aligned.Throttle);Check.That(aligned.Actions.HasFlag(SystemNavigationAction.EnterSupercruise)); var locked=new SystemFlightNavigator().Decide(frame with{Forward=new(0,0,1),FsdReady=false},Station,Now);Check.That(!locked.Actions.HasFlag(SystemNavigationAction.EnterSupercruise)); } [Test] public void FiveKilometreDropZerosAndCallsAtcInTheFirstDecision() { var p=new SystemFlightNavigator();var f=Frame with{Assists=new(false,true,false),DockingAssist=NativeAssistState.Ready,Speed=222.6,Throttle=.75, TargetDistance=5000,TargetPosition=new(0,0,5000),ThrusterBoostAvailable=true}; var first=p.Decide(f,Station,Now);Check.Equal(SystemNavigationPhase.RequestingDocking,first.Phase);Check.Equal(0d,first.Throttle); Check.That(first.Actions.HasFlag(SystemNavigationAction.RequestDocking)&&!first.Actions.HasFlag(SystemNavigationAction.BoostThrusters)); } [Test] public void NormalBoostIsBoundedByAlignmentAtcDistanceAndFiveSecondCooldown() { var p=new SystemFlightNavigator();var f=Frame with{Assists=new(false,true,false),DockingAssist=NativeAssistState.Ready,TargetDistance=9000, TargetPosition=new(0,0,9000),ThrusterBoostAvailable=true,Throttle=1}; Check.That(p.Decide(f,Station,Now).Actions.HasFlag(SystemNavigationAction.BoostThrusters)); var early=Now.AddSeconds(4.99);Check.That(!p.Decide(f with{ObservedAt=early},Station,early).Actions.HasFlag(SystemNavigationAction.BoostThrusters)); var later=Now.AddSeconds(5);Check.That(p.Decide(f with{ObservedAt=later},Station,later).Actions.HasFlag(SystemNavigationAction.BoostThrusters)); foreach(var blocked in new[]{f with{TargetDistance=7500},f with{TargetDistance=5000},f with{LandingGearDown=true},f with{ThrusterBoostAvailable=false}, f with{Forward=new(1,0,0)},f with{PlayerOverride=true},f with{Clearance=Grant,DockingAssist=NativeAssistState.Engaged}}) Check.That(!new SystemFlightNavigator().Decide(blocked,Station,Now).Actions.HasFlag(SystemNavigationAction.BoostThrusters)); } [Test] public void ContactIsRequestedAtSevenPointFiveKilometresOnlyInNormalFlight() { var policy=new SystemFlightNavigator();var far=policy.Decide(Frame with{TargetDistance=7500.1},Station,Now); Check.That(!far.Actions.HasFlag(SystemNavigationAction.RequestDocking)); var close=policy.Decide(Frame with{TargetDistance=7500},Station,Now);Check.That(close.Actions.HasFlag(SystemNavigationAction.RequestDocking)); var cruise=new SystemFlightNavigator().Decide(Frame with{Mode=LocalFlightMode.Supercruise,TargetDistance=7000},Station,Now); Check.That(!cruise.Actions.HasFlag(SystemNavigationAction.RequestDocking)); } [Test] public void FittedDockingComputerKeepsFixedFullThrottleUntilAtcRangeAndHandsOverWhileMoving() { var policy=new SystemFlightNavigator(); foreach(var speed in new[]{32d,33d,150d,299d,300d}) { var full=policy.Decide(Frame with{Assists=new(false,true,false),DockingAssist=NativeAssistState.Ready,TargetDistance=7600,Speed=speed},Station,Now); Check.Equal(1d,full.Throttle);Check.That(!full.Actions.HasFlag(SystemNavigationAction.RequestDocking)); } var moving=Frame with{Assists=new(false,true,false),Speed=300,TargetDistance=7500,DockingAssist=NativeAssistState.Ready}; var request=policy.Decide(moving,Station,Now);Check.Equal(0d,request.Throttle);Check.That(request.Actions.HasFlag(SystemNavigationAction.RequestDocking)); var handover=policy.Decide(moving with{Clearance=Grant},Station,Now); Check.Equal(SystemNavigationPhase.NativeLanding,handover.Phase);Check.Equal(0d,handover.Throttle); Check.That(handover.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)); Check.That(policy.Decide(moving with{Clearance=Grant,DockingAssist=NativeAssistState.Engaged},Station,Now).Throttle is null); } [Test]public void NormalStationApproachDoesNotPulseThrottleWhenSpeedChanges() { var policy=new SystemFlightNavigator(); for(var i=0;i<100;i++) { var at=Now.AddMilliseconds(i*50);var distance=16000-i*80; var frame=Frame with{ObservedAt=at,TargetDistance=distance,TargetPosition=new(0,0,distance),Speed=i%2==0?178:50,Throttle=1, BrakingAcceleration=2.4729359,MaximumNormalSpeed=178,Assists=new(false,true,false),DockingAssist=NativeAssistState.Ready}; Check.Equal(1d,policy.Decide(frame,Station,at).Throttle); } var contact=Frame with{ObservedAt=Now.AddSeconds(6),TargetDistance=7500,TargetPosition=new(0,0,7500),Speed=178,Throttle=1,Assists=new(false,true,false),DockingAssist=NativeAssistState.Ready}; Check.Equal(0d,policy.Decide(contact,Station,Now.AddSeconds(6)).Throttle); } [Test] public void SoftwareCruiseHoldsTheOriginalThrottlePositionAcrossSmallSpeedFluctuations() { var policy=new SystemFlightNavigator();var frame=Frame with{Mode=LocalFlightMode.Supercruise,TargetDistance=6e6,TargetPosition=new(0,0,6e6),MaximumCruiseSpeed=2e6,SafeDropSpeed=1e6}; foreach(var speed in new[]{990000d,1000000d,1010000d,995000d}) Check.Equal(.75,policy.Decide(frame with{Speed=speed},Station,Now).Throttle); Check.Equal(0d,policy.Decide(frame with{Speed=1200000},Station,Now).Throttle); foreach(var speed in new[]{1010000d,990000d,810000d}) Check.Equal(0d,policy.Decide(frame with{Speed=speed},Station,Now).Throttle); Check.Equal(.75,policy.Decide(frame with{Speed=790000},Station,Now).Throttle); } [Test] public void DistantCruiseDoesNotBrakeForTheSampledAssistSpeedLimit() { var policy=new SystemFlightNavigator();var frame=Frame with{Mode=LocalFlightMode.Supercruise,TargetDistance=98.5e9,TargetPosition=new(0,0,98.5e9),MaximumCruiseSpeed=1e8,SafeDropSpeed=1e6}; foreach(var speed in new[]{2.2e6,1.2e8,8e7,3e8,1e8})Check.Equal(.75,policy.Decide(frame with{Speed=speed},Station,Now).Throttle); var close=frame with{TargetDistance=6e6,TargetPosition=new(0,0,6e6),Speed=1.2e6}; Check.Equal(0d,policy.Decide(close,Station,Now).Throttle); } [Test] public void FarSideDestinationLeavesNormalSpaceAlongTheVerifiedClearDirection() { var frame=Frame with{TargetDistance=1e8,TargetPosition=new(0,0,1e8),TargetObscured=true,ClearDirection=new(1,0,0),Forward=new(1,0,0),FsdReady=true}; var decision=new SystemFlightNavigator().Decide(frame,Station,Now); Check.Equal(SystemNavigationPhase.EnteringSupercruise,decision.Phase); Check.Equal(new Vector3d(1,0,0),decision.Direction);Check.Equal(1d,decision.Throttle); Check.That(decision.Actions.HasFlag(SystemNavigationAction.EnterSupercruise)); var waiting=new SystemFlightNavigator().Decide(frame with{ClearDirection=null},Station,Now); Check.That(!waiting.Actions.HasFlag(SystemNavigationAction.EnterSupercruise));Check.That(waiting.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes)); } [Test] public void ObscuredCruiseAcceleratesOnlyAlongVerifiedNormalThenZerosThrottleAsSoonAsItClears() { var policy=new SystemFlightNavigator();var frame=Frame with{Mode=LocalFlightMode.Supercruise,TargetDistance=1e9,TargetPosition=new(0,0,1e9), Assists=new(false,true,true),TargetObscured=true,ClearDirection=new(1,0,0)}; Check.Equal(0d,policy.Decide(frame,Station,Now).Throttle); var bypass=policy.Decide(frame with{Forward=new(1,0,0)},Station,Now); Check.Equal(1d,bypass.Throttle);Check.Equal(new Vector3d(1,0,0),bypass.Direction); var clear=policy.Decide(frame with{Forward=new(1,0,0),TargetObscured=false,ClearDirection=null,SupercruiseAssist=NativeAssistState.Aligning},Station,Now); Check.Equal(0d,clear.Throttle);Check.Equal(new Vector3d(0,0,1),clear.Direction); Check.That(!clear.Actions.HasFlag(SystemNavigationAction.EnableSupercruiseAssist)); Check.Equal(0d,new SystemFlightNavigator().Decide(frame with{ClearDirection=new(0,0,1)},Station,Now).Throttle); Check.Equal(0d,new SystemFlightNavigator().Decide(frame with{ClearDirection=null},Station,Now).Throttle); } [Test] public void SidewaysBypassChecksOtherMoonsAndDeclinesEveryBlockedNormal() { var planet=new StarEnvelope(1,"",new(0,0,100),10,null,null); var moon=new StarEnvelope(2,"",new(0,100,0),20,null,null); var origin=Vector3d.Zero;var destination=new Vector3d(0,0,1000); var direction=FlightGeometry.ClearPerpendicularDirection([planet,moon],origin,destination,new(0,1,0)); Check.That(direction is not null);Check.That(Math.Abs(Vector3d.Dot(direction!.Value,new(0,0,1)))<1e-9); Check.That(Vector3d.AngleDegrees(direction.Value,new(0,1,0))>5); Check.That(FlightGeometry.ClosestSegmentDistance(moon.Centre,origin,direction.Value*1000)>moon.SafeRadius); Check.That(FlightGeometry.ClearPerpendicularDirection([new(3,"",origin,10,null,null)],origin,destination,new(0,1,0)) is null); } [Test] public void BothStandardAndAdvancedDockingRequireExactPermissionThenZeroThrottle() { foreach(var assist in new[]{new NavigationAssists(true,false,false),new NavigationAssists(false,true,false)}) { var policy=new SystemFlightNavigator();var frame=Frame with{Assists=assist,DockingAssist=NativeAssistState.Ready,Throttle=.5}; var request=policy.Decide(frame,Station,Now);Check.Equal(0d,request.Throttle); Check.That(request.Actions.HasFlag(SystemNavigationAction.RequestDocking));Check.Equal(SystemNavigationPhase.RequestingDocking,request.Phase); var allowed=policy.Decide(frame with{Clearance=Grant},Station,Now);Check.Equal(0d,allowed.Throttle);Check.Equal(SystemNavigationPhase.NativeLanding,allowed.Phase); var accepted=policy.Decide(frame with{Clearance=Grant,Throttle=0,DockingAssist=NativeAssistState.Requested},Station,Now);Check.That(accepted.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes)); var engaged=policy.Decide(frame with{Clearance=Grant,Throttle=0,DockingAssist=NativeAssistState.Engaged},Station,Now);Check.That(engaged.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes));Check.That(engaged.Throttle is null); } } [Test] public void OtherStationExpiredOrMissingPadPermissionsCannotLand() { foreach(var clearance in new[]{Grant with{MarketId=999},Grant with{ExpiresAt=Now},Grant with{Pad=null},Grant with{Permission=DockingPermission.Requested},Grant with{ExpiresAt=null}}) { var decision=new SystemFlightNavigator().Decide(Frame with{Assists=new(true,false,false),DockingAssist=NativeAssistState.Ready,Clearance=clearance},Station,Now); Check.That(decision.Phase is SystemNavigationPhase.RequestingDocking or SystemNavigationPhase.AwaitingClearance); Check.That(!decision.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)); } } [Test] public void CachedZeroThrottleStillRequestsTheOriginalDockingInputZero() { // A physics overlay may report zero while the original docking helper // still sees the player's 5% input. Its actual input zero is separate. var frame=Frame with{Assists=new(false,true,false),DockingAssist=NativeAssistState.Ready,Clearance=Grant,Throttle=0,DockingAssistEnabled=false}; var decision=new SystemFlightNavigator().Decide(frame,Station,Now); Check.That(decision.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)); Check.That(decision.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes)); var zeroConfirmed=new SystemFlightNavigator().Decide(frame with{DockingAssistEnabled=true},Station,Now); Check.That(!zeroConfirmed.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)); Check.That(zeroConfirmed.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes)); Check.That(zeroConfirmed.Throttle is null&&zeroConfirmed.Direction is null); } [Test] public void UnconfirmedLandingHandoffWaitsUpToTenSecondsAndStopsAtTwoKilometres() { var policy=new SystemFlightNavigator(); var frame=Frame with{Assists=new(false,true,false),DockingAssist=NativeAssistState.Ready,Clearance=Grant,Speed=180}; Check.That(policy.Decide(frame,Station,Now).Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)); var at=Now.AddMilliseconds(2100); Check.That(!policy.Decide(frame with{ObservedAt=at,DockingAssistEnabled=true},Station,at).Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff)); at=Now.AddSeconds(10); Check.That(!policy.Decide(frame with{ObservedAt=at,DockingAssistEnabled=true},Station,at).Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff)); at=at.AddMilliseconds(1); var unsafeApproach=policy.Decide(frame with{ObservedAt=at,DockingAssistEnabled=true},Station,at); Check.Equal(SystemNavigationPhase.Stopped,unsafeApproach.Phase); Check.Equal(SystemNavigationAction.ExitForFailedHandoff,unsafeApproach.Actions); var stopped=new SystemFlightNavigator();stopped.Decide(frame with{Speed=0},Station,Now); Check.That(stopped.Decide(frame with{ObservedAt=at,Speed=0,DockingAssistEnabled=true},Station,at).Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff)); var near=new SystemFlightNavigator();near.Decide(frame,Station,Now);at=Now.AddMilliseconds(100); Check.That(near.Decide(frame with{ObservedAt=at,TargetDistance=2000,TargetPosition=new(0,0,2000)},Station,at).Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff)); } [Test] public void NativeDutyCanEndBeforeTheActualDockedJournalWithoutRestartingLanding() { var policy=new SystemFlightNavigator(); var frame=Frame with{Assists=new(false,true,false),DockingAssist=NativeAssistState.Engaged,Clearance=Grant}; policy.Decide(frame,Station,Now); var at=Now.AddSeconds(90); policy.Decide(frame with{ObservedAt=at,Clearance=Grant with{ObservedAt=at,ExpiresAt=at.AddMinutes(1)}},Station,at); at=at.AddSeconds(1); var waiting=policy.Decide(frame with{ObservedAt=at,DockingAssist=NativeAssistState.Ready,Clearance=Grant with{ObservedAt=at,ExpiresAt=at.AddMinutes(1)}},Station,at); Check.Equal(SystemNavigationPhase.NativeLanding,waiting.Phase); Check.That(waiting.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes)); Check.That(!waiting.Actions.HasFlag(SystemNavigationAction.EnableDockingAssist)); Check.That(!waiting.Complete); Check.That(policy.Decide(frame with{ObservedAt=at,Mode=LocalFlightMode.Docked,DockedMarketId=77},Station,at).Complete); } [Test] public void TemporaryQueueRetriesWithoutSendingRequestEveryFrame() { var policy=new SystemFlightNavigator();var frame=Frame with{Clearance=Grant with{Permission=DockingPermission.Denied,Reason="NoSpace",Retryable=true}}; Check.That(policy.Decide(frame,Station,Now).Actions.HasFlag(SystemNavigationAction.RequestDocking)); Check.That(!policy.Decide(frame,Station,Now).Actions.HasFlag(SystemNavigationAction.RequestDocking)); Check.That(policy.Decide(frame with{ObservedAt=Now.AddSeconds(10)},Station,Now.AddSeconds(10)).Actions.HasFlag(SystemNavigationAction.RequestDocking)); } [Test] public void PermanentDenialStopsAndArrivalNeedsActualDockedIdentity() { var decision=new SystemFlightNavigator().Decide(Frame with{Clearance=Grant with{Permission=DockingPermission.Denied,Reason="Hostile"}},Station,Now); Check.Equal(SystemNavigationPhase.Stopped,decision.Phase); Check.That(new SystemFlightNavigator().Decide(Frame with{Mode=LocalFlightMode.Docked,DockedMarketId=77},Station,Now).Complete); Check.That(!new SystemFlightNavigator().Decide(Frame with{Mode=LocalFlightMode.Docked,DockedMarketId=99},Station,Now).Complete); } [Test] public void SupercruiseAssistAlignsThenImmediatelyYieldsFromActivationOnward() { var policy=new SystemFlightNavigator();var frame=Frame with{Mode=LocalFlightMode.Supercruise,Assists=new(false,false,true),SupercruiseAssist=NativeAssistState.Ready,Forward=new(1,0,0)}; Check.That(!policy.Decide(frame,Station,Now).Actions.HasFlag(SystemNavigationAction.EnableSupercruiseAssist)); var aligned=policy.Decide(frame with{Forward=new(0,0,1)},Station,Now);Check.That(aligned.Actions.HasFlag(SystemNavigationAction.EnableSupercruiseAssist)); Check.That(aligned.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes)); Check.That(aligned.Direction is null&&aligned.Throttle is null); foreach(var state in new[]{NativeAssistState.Requested,NativeAssistState.Engaged}) { var active=policy.Decide(frame with{SupercruiseAssist=state,SafeDisengage=true,SafeDropDistance=1e6,SafeDropSpeed=1e6},Station,Now); Check.That(active.Direction is null&&active.Throttle is null); Check.Equal(SystemNavigationAction.ReleaseFlightAxes,active.Actions); } } [Test] public void NativeObstructionClearanceAllowsImmediateAssistReactivationAfterRealignment() { var policy=new SystemFlightNavigator(); var frame=Frame with{Mode=LocalFlightMode.Supercruise,Assists=new(false,false,true),SupercruiseAssist=NativeAssistState.Ready}; Check.That(policy.Decide(frame,Station,Now).Actions.HasFlag(SystemNavigationAction.EnableSupercruiseAssist)); var bypass=frame with{ObservedAt=Now.AddMilliseconds(100),SupercruiseAssist=NativeAssistState.Requested,TargetObscured=true,ClearDirection=new(1,0,0),Forward=new(1,0,0)}; var flying=policy.Decide(bypass,Station,bypass.ObservedAt); Check.Equal(1d,flying.Throttle);Check.That(flying.Actions.HasFlag(SystemNavigationAction.CancelOwnedAssists)); var clear=bypass with{ObservedAt=Now.AddMilliseconds(200),SupercruiseAssist=NativeAssistState.Ready,TargetObscured=false}; Check.Equal(0d,policy.Decide(clear,Station,clear.ObservedAt).Throttle); var aligned=clear with{ObservedAt=Now.AddMilliseconds(300),Forward=new(0,0,1)}; var resumed=policy.Decide(aligned,Station,aligned.ObservedAt); Check.That(resumed.Actions.HasFlag(SystemNavigationAction.EnableSupercruiseAssist)); Check.That(resumed.Actions.HasFlag(SystemNavigationAction.ReleaseFlightAxes));Check.That(resumed.Throttle is null); } [Test] public void SoftwareSupercruiseUsesMeasuredVelocityAndNeverEmergencyDrops() { var frame=Frame with{Mode=LocalFlightMode.Supercruise,TargetDistance=1e8,Speed=2e7,SafeDropDistance=1e6,SafeDropSpeed=1e6}; var decision=new SystemFlightNavigator().Decide(frame,Station,Now);Check.Equal(0d,decision.Throttle);Check.That(!decision.Actions.HasFlag(SystemNavigationAction.SafeDisengage)); Check.That(!new SystemFlightNavigator().Decide(frame with{TargetDistance=1000,Speed=100,SafeDisengage=false},Station,Now).Actions.HasFlag(SystemNavigationAction.SafeDisengage)); Check.That(new SystemFlightNavigator().Decide(frame with{TargetDistance=1000,Speed=100,SafeDisengage=true},Station,Now).Actions.HasFlag(SystemNavigationAction.SafeDisengage)); } [Test] public void FailedOrUnresponsiveSupercruiseAssistFallsBackToApplicationControl() { var policy=new SystemFlightNavigator();var frame=Frame with{Mode=LocalFlightMode.Supercruise,Assists=new(false,false,true),SupercruiseAssist=NativeAssistState.Requested}; var waiting=policy.Decide(frame,Station,Now); Check.Equal(SystemNavigationAction.ReleaseFlightAxes,waiting.Actions);Check.That(waiting.Throttle is null); var fallback=policy.Decide(frame with{ObservedAt=Now.AddSeconds(21)},Station,Now.AddSeconds(21)); Check.Equal(SystemNavigationPhase.Supercruise,fallback.Phase);Check.That(fallback.Direction is not null); Check.That(fallback.Actions.HasFlag(SystemNavigationAction.CancelOwnedAssists)); var failed=new SystemFlightNavigator().Decide(frame with{SupercruiseAssist=NativeAssistState.Failed},Station,Now); Check.Equal(SystemNavigationPhase.Supercruise,failed.Phase);Check.That(!failed.Actions.HasFlag(SystemNavigationAction.EnableSupercruiseAssist)); } [Test] public void SoftwareCruiseRequiresOriginalParametersAndBrakesWhenClosingTooFast() { var frame=Frame with{Mode=LocalFlightMode.Supercruise,TargetDistance=1e8,TargetPosition=new(0,0,1e8),MaximumCruiseSpeed=1e7,Speed=30000}; var driving=new SystemFlightNavigator().Decide(frame,Station,Now);Check.That(driving.Throttle is >0 and <=.75); foreach(var invalid in new[]{frame with{CruiseParametersVerified=false},frame with{CruiseApproachSeconds=0},frame with{CruiseApproachThrottle=double.NaN},frame with{Speed=2e7}}) Check.Equal(0d,new SystemFlightNavigator().Decide(invalid,Station,Now).Throttle); } [Test] public void AfterConfirmedSupercruiseExitTheNormalApproachDoesNotReenterSupercruise() { var policy=new SystemFlightNavigator();policy.Decide(Frame with{Mode=LocalFlightMode.Supercruise},Station,Now); var normal=policy.Decide(Frame with{TargetDistance=50000},Station,Now); Check.That(!normal.Actions.HasFlag(SystemNavigationAction.EnterSupercruise));Check.Equal(SystemNavigationPhase.NormalApproach,normal.Phase); } [Test] public void SoftwareDockingWaitsForOriginalPathAndFollowsCorrectGrantedPad() { var frame=Frame with{Clearance=Grant};var policy=new SystemFlightNavigator(); Check.Equal(SystemNavigationPhase.Waiting,policy.Decide(frame,Station,Now).Phase); var path=new DockingPath(77,3,1,[new(new(0,0,20),Vector3d.Zero,new(0,0,1),new(0,1,0),10,1,true,true)],Now,15); var landing=policy.Decide(frame with{DockingPath=path},Station,Now);Check.Equal(SystemNavigationPhase.ApplicationLanding,landing.Phase);Check.That(!landing.Complete); Check.That(landing.Actions.HasFlag(SystemNavigationAction.DeployLandingGear));Check.That(landing.Translation is {Finite:true,Length:<=1.001}); Check.Equal(SystemNavigationPhase.Waiting,policy.Decide(frame with{DockingPath=path with{MarketId=99}},Station,Now).Phase); Check.Equal(SystemNavigationPhase.Waiting,policy.Decide(frame with{DockingPath=path with{Pad=8}},Station,Now).Phase); } private static PlanetApproachFrame Planet=>new(102,Vector3d.Zero,new(0,1000000,0),new(0,1,0),new(0,0,1),Limits,30000,100000,25); [Test] public void PlanetaryPhasesUseObservedModeAndSlowBeforeOrbitalDescent() { var planet=Planet with{GroundDistance=10000,Altitude=26000,DescentAngleDegrees=70}; var frame=Frame with{SelectedTarget=Surface,ReferenceWorld=102,Planet=planet,Position=new(0,1026000,0),TargetPosition=Planet.SurfaceDestination,Mode=LocalFlightMode.OrbitalCruise,Speed=Limits.MaximumEntrySpeed*2}; var policy=new SystemFlightNavigator();var orbital=policy.Decide(frame,Surface,Now);Check.Equal(SystemNavigationPhase.OrbitalApproach,orbital.Phase);Check.Equal(0d,orbital.Throttle); var glide=policy.Decide(frame with{Mode=LocalFlightMode.Glide},Surface,Now);Check.Equal(SystemNavigationPhase.Glide,glide.Phase);Check.That(glide.Throttle is null); Check.That(Math.Abs(Vector3d.Dot(glide.Direction!.Value,(frame.Position-planet.Centre).Unit()))<.01,"Too-steep glide must level out."); var normal=policy.Decide(frame with{Mode=LocalFlightMode.Normal,TargetDistance=7000,Speed=50},Surface,Now);Check.That(normal.Phase is SystemNavigationPhase.RequestingDocking or SystemNavigationPhase.AwaitingClearance); Check.That(!normal.Actions.HasFlag(SystemNavigationAction.EnterSupercruise)); } [Test] public void PlanetaryApproachRefusesMissingOrWrongBodyParameters() { foreach(var planet in new[]{Planet with{BodyAddress=99},Planet with{Limits=Limits with{MaximumGlideAngleDegrees=95}},Planet with{Altitude=double.NaN}}) { var decision=new SystemFlightNavigator().Decide(Frame with{SelectedTarget=Surface,Mode=LocalFlightMode.OrbitalCruise,Planet=planet},Surface,Now); Check.Equal(SystemNavigationPhase.Waiting,decision.Phase);Check.That(decision.Direction is null); } } [Test] public void FarSideApproachNeverPointsThroughThePlanetAtHighAltitude() { var position=new Vector3d(0,-1500000,0);var planet=Planet with{Altitude=500000}; var entry=SurfaceApproachGeometry.EntryPoint(planet,position); Check.That(SurfaceApproachGeometry.IntersectsPlanet(position,entry,Vector3d.Zero,Limits.Radius+Limits.GlideEntryAltitude)); var decision=new SystemFlightNavigator().Decide(Frame with{SelectedTarget=Surface,Planet=planet,Position=position,Mode=LocalFlightMode.Supercruise},Surface,Now); Check.That(Math.Abs(Vector3d.Dot(decision.Direction!.Value,position.Unit()))<.01); } [Test] public void HumanOverrideAndTargetChangeStopBeforeAnyInput() { foreach(var frame in new[]{Frame with{PlayerOverride=true},Frame with{ExternalThreat=true},Frame with{SelectedTarget=Station with{MarketId=99}},Frame with{SystemAddress=2}}) Check.Equal(SystemNavigationPhase.Stopped,new SystemFlightNavigator().Decide(frame,Station,Now).Phase); Check.Equal(SystemNavigationPhase.Waiting,new SystemFlightNavigator().Decide(Frame with{ObservedAt=Now.AddSeconds(-1)},Station,Now).Phase); } [Test] public void PoweredStandardDockingIsRecognizedAndDisabledOrDestroyedModulesAreNot() { var player=new PlayerState{CurrentShipId=1,Ships=[new(){ShipId=1,Modules=[new(){Item="int_dockingcomputer_standard",Enabled=true,Health=1}]}]}; Check.That(NavigationAssists.From(player).AutoDock); foreach(var module in new[]{player.Ships[0].Modules[0] with{Enabled=false},player.Ships[0].Modules[0] with{Health=0}}) Check.That(!NavigationAssists.From(player with{Ships=[player.Ships[0] with{Modules=[module]}]}).AutoDock); } [Test] public void LocationFilteringKeepsDistinctPlacesOnTheSamePlanetAndSortsByRealDistance() { var locations=new[]{new SystemLocation(1,102,"A","",28,SystemLocationCategory.Settlement,10,new(0,0,1),20,false,"one"), new SystemLocation(2,102,"B","",28,SystemLocationCategory.Settlement,11,new(0,0,2),10,false,"two"), new SystemLocation(3,103,"C","",1,SystemLocationCategory.Star,null,null,null,false,"three")}; var snapshot=new CurrentSystemSnapshot(1,"Here",1,1,Now,locations); var selected=new SystemLocationFilter(SystemLocationCategory.Settlement).Apply(snapshot);Check.Equal(2,selected.Count);Check.Equal("B",selected[0].Name); Check.That(new SystemLocationFilter(SystemLocationCategory.None).Apply(snapshot).SequenceEqual(new SystemLocationFilter(SystemLocationCategory.All).Apply(snapshot))); Check.That(new SystemLocationFilter().Apply(snapshot).SequenceEqual(new SystemLocationFilter(SystemLocationCategory.All).Apply(snapshot))); Check.Equal(1,new SystemLocationFilter(Search:"a").Apply(snapshot).Count); } }