using System.Text.Json; using System.Text.Json.Nodes; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public sealed class StationDepartureContinuityTests { private static readonly DateTimeOffset Now=DateTimeOffset.UtcNow; private static PlayerState Player=>new(){InGame=true,Docked=false,CommanderId="F1",CurrentShipId=7,LoadGameAt=Now.AddMinutes(-5),SystemName="System 1", Status=new(1L<<16,0,100,1,0,null,null,null,null,[]),StatusAt=Now,StatusReadAt=Now,StatusWrittenAt=Now, Navigation=new(){CurrentSystemAddress=1,UndockedAt=Now.AddSeconds(-45),UndockedMarketId=10,UndockedStationName="Port",UndockedSystemAddress=1}}; private static JsonObject Main()=>JsonSerializer.SerializeToNode(new{sequence=1,observedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds(),entityId="100",referenceWorldAddress="1", inGame=true,localPlayerShipVerified=true,playerOverride=false,rawFsdState=2,shipHealth=new{verified=true,entityId="100",observedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds(),hullFraction=.702915,emergency=false,damageObserved=false}})!.AsObject(); private static JsonObject Local(bool launch=true)=>JsonSerializer.SerializeToNode(new{sequence=1,observedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds(),entityId="100",referenceWorldAddress="1", geometryVerified=true,inSupercruise=false,rawDockingState=9,target=new{marketId=10},docking=new{verified=true,computerAvailable=true,nativeLaunchActive=launch,nativeLaunchRequested=false, nativeDutyActive=launch,clearanceGranted=false}})!.AsObject(); private static bool Needs(PlayerState player,JsonObject main,JsonObject local,long? destination=null) =>StationDepartureContinuity.NeedsContinuation(player,JsonSerializer.SerializeToElement(main),JsonSerializer.SerializeToElement(local),destination,Now); [Test]public void Live12UndockedInsideTheStationWithOriginalLaunchActiveCannotStartAnotherFlightPhase() { Check.That(Needs(Player,Main(),Local())); Check.Equal(10L,StationDepartureContinuity.KnownOrigin(Player,Now)!.MarketId); Check.That(Needs(Player,Main(),Local(),10),"Returning to the origin does not permit fighting an active original launch."); } [Test]public void InactiveLaunchStillMassLockedNeedsExitEvidenceExceptForNormalReturnToTheConfirmedOrigin() { Check.That(Needs(Player,Main(),Local(false)));Check.That(Needs(Player,Main(),Local(false),20)); Check.That(!Needs(Player,Main(),Local(false),10)); var unknown=Player with{Navigation=Player.Navigation with{UndockedMarketId=null,UndockedStationName=""}}; Check.That(Needs(unknown,Main(),Local(false),10));Check.That(StationDepartureContinuity.KnownOrigin(unknown,Now) is null); } [Test]public void NormalFreeFlightAndActualSupercruiseOrHyperspaceDoNotNeedAnInventedOrigin() { var free=Player with{Status=Player.Status! with{Flags=0},Navigation=Player.Navigation with{UndockedAt=null,UndockedMarketId=null,UndockedStationName=""}}; Check.That(!Needs(free,Main(),Local(false))); var cruise=free with{Status=free.Status! with{Flags=16}}; Check.That(!StationDepartureContinuity.NeedsContinuation(cruise,JsonSerializer.SerializeToElement(Main()),default,null,Now)); var jumping=Main();jumping["rawFsdState"]=4; Check.That(!StationDepartureContinuity.NeedsContinuation(free,JsonSerializer.SerializeToElement(jumping),default,null,Now)); } [Test]public void AnOriginalLandingAtTheRequestedLocalStationRemainsObservationOnly() { var landing=Local(false);landing["rawDockingState"]=3;landing["docking"]!["nativeDutyActive"]=true;landing["docking"]!["clearanceGranted"]=true; Check.That(!Needs(Player,Main(),landing,10));Check.That(Needs(Player,Main(),landing,20)); } [Test]public void ManualTakeoverFreshDamageAndUnverifiedOrForeignObservationsCannotClearTheDepartureGate() { var manual=Main();manual["playerOverride"]=true;Check.Throws(()=>Needs(Player,manual,Local())); foreach(var key in new[]{"emergency","damageObserved"}){var damaged=Main();damaged["shipHealth"]![key]=true;Check.Throws(()=>Needs(Player,damaged,Local()));} var dead=Main();dead["shipHealth"]!["hullFraction"]=0;Check.Throws(()=>Needs(Player,dead,Local())); var stale=Local();stale["observedAtUnixMilliseconds"]=Now.AddSeconds(-1).ToUnixTimeMilliseconds();Check.Throws(()=>Needs(Player,Main(),stale)); var foreign=Local();foreign["entityId"]="200";Check.Throws(()=>Needs(Player,Main(),foreign)); var unknown=Local();unknown["docking"]!.AsObject().Remove("nativeLaunchActive");Check.That(Needs(Player,Main(),unknown)); } [Test]public void TheExistingUndockReceiptIsUsableButNeverAcrossAnotherSessionSystemOrCompletedCruise() { Check.That(StationDepartureContinuity.KnownOrigin(Player,Now) is not null); Check.That(StationDepartureContinuity.KnownOrigin(Player with{LoadGameAt=Now},Now) is null); Check.That(StationDepartureContinuity.KnownOrigin(Player with{Navigation=Player.Navigation with{CurrentSystemAddress=2}},Now) is null); Check.That(StationDepartureContinuity.KnownOrigin(Player with{Navigation=Player.Navigation with{SupercruiseEnteredAt=Now.AddSeconds(-1)}},Now) is null); Check.That(StationDepartureContinuity.KnownOrigin(Player with{Navigation=Player.Navigation with{LastJumpAt=Now.AddSeconds(-1)}},Now) is null); } [Test]public void JournalProjectionRetainsTheActualUndockedMarketNameAndSystemForANewHost() { var projector=new NavigationProjector();projector.Apply(JsonSerializer.SerializeToElement(new{@event="Location",timestamp=Now.AddSeconds(-10),SystemAddress=1})); projector.Apply(JsonSerializer.SerializeToElement(new{@event="Undocked",timestamp=Now,StationName="Port",MarketID=10})); Check.Equal(10L,projector.State.UndockedMarketId!.Value);Check.Equal("Port",projector.State.UndockedStationName);Check.Equal(1L,projector.State.UndockedSystemAddress!.Value); } [Test]public void ComputerZeroDefaultFlagsCannotReleaseAnOriginObligationOrBecomeInactiveCancellationProof() { var local=Local(false);local["docking"]!["computerAvailable"]=false; Check.That(!StationDepartureContinuity.NativeDutySourceVerified(JsonSerializer.SerializeToElement(local["docking"]))); Check.That(!StationDepartureContinuity.NativeLaunchInactive(JsonSerializer.SerializeToElement(local["docking"]))); Check.That(Needs(Player,Main(),local));Check.That(Needs(Player,Main(),local,10)); local["docking"]!["throttleZero"]=true; Check.That(StationDepartureContinuity.NativeDutySourceVerified(JsonSerializer.SerializeToElement(local["docking"]))); Check.That(StationDepartureContinuity.NativeLaunchInactive(JsonSerializer.SerializeToElement(local["docking"]))); Check.That(!Needs(Player,Main(),local,10)); local["docking"]!.AsObject().Remove("nativeDutyActive"); Check.That(!StationDepartureContinuity.NativeLaunchInactive(JsonSerializer.SerializeToElement(local["docking"])));Check.That(Needs(Player,Main(),local,10)); } [Test]public void OnlyACompleteCurrentLoadoutWithNoInstalledComputerCanEstablishFreeFlightWithoutAComputerSource() { var free=Player with{Status=Player.Status! with{Flags=0},Navigation=Player.Navigation with{UndockedAt=null,UndockedMarketId=null,UndockedStationName=""}, Ships=[new(){ShipId=7,Type="type9",ConfigurationAt=Now,Modules=[new(){Slot="FrameShiftDrive",Item="int_hyperdrive_size7_class5",Enabled=true}]}]}; var unknown=Local(false);unknown["docking"]!["computerAvailable"]=false; Check.That(StationDepartureContinuity.VerifiedNoInstalledDockingComputer(free,Now));Check.That(!Needs(free,Main(),unknown)); foreach(var computer in new[]{"int_dockingcomputer_standard","int_dockingcomputer_advanced"}) { var disabled=free with{Ships=[free.Ships[0] with{Modules=[..free.Ships[0].Modules,new(){Slot="Slot01",Item=computer,Enabled=false,Health=0}]}]}; Check.That(!StationDepartureContinuity.VerifiedNoInstalledDockingComputer(disabled,Now));Check.Throws(()=>Needs(disabled,Main(),unknown)); } foreach(var ship in new[]{free.Ships[0] with{ConfigurationChanged=true},free.Ships[0] with{ConfigurationAt=null},free.Ships[0] with{Modules=[]}}) Check.Throws(()=>Needs(free with{Ships=[ship]},Main(),unknown)); } [Test]public void ObservationSelectionIgnoresSelectedSignalsAndRequiresARealCompletePhysicalBody() { SystemLocation Body(int key,ulong world,SystemLocationCategory kind,bool selected=false,string? identity=null)=>new(key,world,"Body "+key,"",1,kind,null,null,null,selected,identity??new string('a',64)); var signal=Body(1,0,SystemLocationCategory.Signal,true);var planet=Body(2,22,SystemLocationCategory.Planet,true);var star=Body(3,33,SystemLocationCategory.Star); var catalogue=new CurrentSystemSnapshot(1,"System 1",100,1,Now,[signal,planet,star]); Check.Equal(22UL,StationDepartureContinuity.ObservationTarget(catalogue,22).BodyAddress); Check.Equal(33UL,StationDepartureContinuity.ObservationTarget(catalogue,99).BodyAddress); Check.Equal(22UL,StationDepartureContinuity.ObservationTarget(catalogue with{Locations=[signal,star with{Identity="partial"},planet]},99).BodyAddress); Check.Throws(()=>StationDepartureContinuity.ObservationTarget(catalogue with{Locations=[signal]},99)); } [Test]public void ARealSameSessionSupercruiseEntryAndExitDischargeTheLaunchEvenWhenTheArrivalIsStillMassLocked() { var travelled=Player with{Navigation=Player.Navigation with{SupercruiseEnteredAt=Now.AddSeconds(-30),SupercruiseExitedAt=Now.AddSeconds(-5),LastJumpAt=Now.AddSeconds(-15)}}; Check.That(!Needs(travelled,Main(),Local(false)));Check.That(!Needs(travelled,Main(),Local(false),20)); foreach(var navigation in new[] { travelled.Navigation with{SupercruiseEnteredAt=null},travelled.Navigation with{SupercruiseExitedAt=null}, travelled.Navigation with{SupercruiseEnteredAt=Now.AddSeconds(-4)},travelled.Navigation with{SupercruiseEnteredAt=Now.AddMinutes(-6)}, travelled.Navigation with{UndockedAt=Now.AddSeconds(-20)},travelled.Navigation with{UndockedAt=Now.AddSeconds(-2)}, travelled.Navigation with{LastJumpAt=Now.AddSeconds(-2)},travelled.Navigation with{SupercruiseExitedAt=Now.AddSeconds(6)} })Check.That(Needs(travelled with{Navigation=navigation},Main(),Local(false)),"Incomplete, prior-session, reversed or superseded transit must not discharge departure."); var unknown=Local(false);unknown["docking"]!["computerAvailable"]=false; Check.Throws(()=>Needs(travelled,Main(),unknown)); Check.That(Needs(travelled,Main(),Local())); var duty=Local(false);duty["docking"]!["nativeDutyActive"]=true;Check.That(Needs(travelled,Main(),duty)); } [Test]public void VerifiedAbsenceAndUnlockedFreeFlightAreIndependentOfAStillRetainedManualUndockOrigin() { var manual=Player with{Status=Player.Status! with{Flags=0},Ships=[new(){ShipId=7,Type="type9",ConfigurationAt=Now, Modules=[new(){Slot="FrameShiftDrive",Item="int_hyperdrive_size7_class5",Enabled=true}]}]}; Check.That(StationDepartureContinuity.KnownOrigin(manual,Now) is not null); var unknown=Local(false);unknown["docking"]!["computerAvailable"]=false; Check.That(!Needs(manual,Main(),unknown)); Check.That(Needs(manual with{Status=manual.Status! with{Flags=1L<<16}},Main(),unknown)); foreach(var name in new[]{"int_dockingcomputer_standard","int_dockingcomputer_advanced"}) { var disabled=manual with{Ships=[manual.Ships[0] with{Modules=[..manual.Ships[0].Modules,new(){Slot="Slot01",Item=name,Enabled=false,Health=0}]}]}; Check.That(Needs(disabled,Main(),unknown)); } Check.That(Needs(manual with{Ships=[manual.Ships[0] with{ConfigurationChanged=true}]},Main(),unknown)); } }