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);
}
}
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);
}
}