XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEEDToolBox

Preserve original landing and destination arrival during flight recovery

0ab851e
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

8 个文件 +188 -10
Modified XFEEDToolBox.Core/Navigation/FlightAutomationCoordinator.cs +6 -1
@@ -109,7 +109,8 @@ public sealed class FlightAutomationCoordinator(Func<PlayerState> player)
109 109 {
110 110 if(destination.MarketId<=0||string.IsNullOrWhiteSpace(destination.SystemName))throw new InvalidDataException("目的站身份缺失。");
111 111 await driver.EnsureReadyAsync(stop);RequireSession(initial);
112 if(initial.Docked&&initial.MarketId==destination.MarketId&&initial.SystemName.Equals(destination.SystemName,StringComparison.OrdinalIgnoreCase))return new(NavigationRunState.Completed);
112 stop.ThrowIfCancellationRequested();
113 if(AtDestinationStation(destination))return new(NavigationRunState.Completed);
113 114 await DepartIfNeededAsync(initial,driver,stop,report);
114 115 if(!player().SystemName.Equals(destination.SystemName,StringComparison.OrdinalIgnoreCase))
115 116 {
@@ -123,6 +124,7 @@ public sealed class FlightAutomationCoordinator(Func<PlayerState> player)
123 124 if(!player().SystemName.Equals(destination.SystemName,StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("游戏尚未确认抵达目的星系。");
124 125 }
125 126 await driver.EnsureReadyAsync(stop);RequireSession(initial);stop.ThrowIfCancellationRequested();
127 if(AtDestinationStation(destination))return new(NavigationRunState.Completed);
126 128 report(FlightAutomationStage.Preparing,"正在读取目的站的最新身份。");
127 129 var arrival=await driver.ResolveArrivalAsync(destination,stop);
128 130 if(arrival.Target.SystemAddress!=player().Navigation.CurrentSystemAddress||arrival.Target.MarketId!=destination.MarketId)
@@ -143,6 +145,9 @@ public sealed class FlightAutomationCoordinator(Func<PlayerState> player)
143 145 return new(NavigationRunState.Completed);
144 146 },token);
145 147
148 private bool AtDestinationStation(Station destination)=>player() is {Docked:true} current&&current.MarketId==destination.MarketId
149 &&current.SystemName.Equals(destination.SystemName,StringComparison.OrdinalIgnoreCase);
150
146 151 private async Task DepartIfNeededAsync(PlayerState initial,IFlightDepartureDriver driver,CancellationToken token,Action<FlightAutomationStage,string> report)
147 152 {
148 153 RequireSession(initial);token.ThrowIfCancellationRequested();
Modified XFEEDToolBox.Core/Navigation/NativeSystemFlightControlDriver.cs +39 -2
@@ -92,7 +92,39 @@ public sealed partial class NativeSystemFlightControlDriver(int processId,string
92 92 {
93 93 if(ensureFlightProtection is not null)await ensureFlightProtection(token);
94 94 if(selected is null)throw new InvalidOperationException("请先选择星系内目的地。");
95 if(player() is {Docked:true} docked&&docked.MarketId!=selected.MarketId)await DepartDockedStationAsync(token);
95 void RequirePreparationIdentity()
96 {
97 token.ThrowIfCancellationRequested();var current=player();
98 if(!current.InGame||current.CommanderId!=commander||current.CurrentShipId!=journalShip||current.LoadGameAt!=sessionAt
99 ||current.Navigation.CurrentSystemAddress!=selected.SystemAddress||NavigationSafety.UnhealthyHull(current))
100 throw new InvalidOperationException("准备星系内驾驶时飞船、会话或安全状态改变。");
101 if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
102 }
103 async Task<bool> PreserveArrivalAsync(CancellationToken cancellation)
104 {
105 var observation=Stopwatch.StartNew();
106 while(true)
107 {
108 cancellation.ThrowIfCancellationRequested();RequirePreparationIdentity();
109 if(TravelPreparation.AtDockedDestination(player(),selected))return true;
110 var observed=await ReadSystemFlightAsync(cancellation);RequirePreparationIdentity();
111 if(observed.PlayerOverride)throw new InvalidOperationException("玩家已主动接管星系内驾驶。");
112 if(TravelPreparation.AtDockedDestination(player(),selected)||TravelPreparation.PreserveOriginalLanding(observed,selected,DateTimeOffset.UtcNow))return true;
113 var local=LastTelemetry!.Value;
114 if(!NativeSystemFlightObservation.ObserveOriginalArrival(local.GetProperty("docking"),local.GetProperty("rawDockingState").GetInt32()))return false;
115 // Native states 5/7 are physically docked, before Journal may
116 // arrive. An active original duty without a fresh grant is also
117 // observation-only; neither state permits retracting its gear.
118 if(observation.Elapsed>=TimeSpan.FromSeconds(30))throw new FlightTelemetryUnavailableException("正在等待原版降落与实际停靠记录同步,不发送设备输入。");
119 await Task.Delay(80,cancellation);
120 }
121 }
122 RequirePreparationIdentity();if(TravelPreparation.AtDockedDestination(player(),selected))return;
123 if(player() is {Docked:true} docked&&docked.MarketId!=selected.MarketId)
124 {
125 await DepartDockedStationAsync(token);
126 RequirePreparationIdentity();
127 }
96 128 SystemFlightFrame frame;var watch=Stopwatch.StartNew();
97 129 while(true)
98 130 {
@@ -100,13 +132,15 @@ public sealed partial class NativeSystemFlightControlDriver(int processId,string
100 132 catch(Exception error)when(NavigationRecovery.CanRetry(error,token)&&watch.Elapsed<TimeSpan.FromSeconds(30))
101 133 {await Task.Delay(80,token);}
102 134 }
135 if(await PreserveArrivalAsync(token))return;
103 136 await PhysicalInputPreparation.RequireAsync(pipe,token);
104 137 if(gameInputs is not null)
105 138 {
106 139 await gameInputs.ConnectAsync(owningThread,token);
107 await TravelPreparation.EnsureRetractedAsync(gameInputs,player,token,frame.ShipEntity);
140 if(!await TravelPreparation.EnsureRetractedAsync(gameInputs,player,token,frame.ShipEntity,PreserveArrivalAsync))return;
108 141 }
109 142 else if(TravelPreparation.HasDeployedEquipment(player()))throw new InvalidOperationException("请先收起武器挂点、货舱舱门和起落架。");
143 if(await PreserveArrivalAsync(token))return;
110 144 if(!useNativeSupercruiseAssist)
111 145 {
112 146 var until=DateTimeOffset.UtcNow.AddSeconds(12);
@@ -581,6 +615,9 @@ public sealed partial class NativeSystemFlightControlDriver(int processId,string
581 615 /// <summary>Original duty engagement can outlive its enable flag during a scene drop.</summary>
582 616 public static class NativeSystemFlightObservation
583 617 {
618 public static bool ObserveOriginalArrival(JsonElement docking,int dockingState)=>Flag(docking,"verified")
619 &&!LaunchNeedsCancellation(docking)&&(dockingState is 5 or 7||Flag(docking,"nativeDutyActive"));
620 private static bool Flag(JsonElement value,string name)=>value.TryGetProperty(name,out var flag)&&flag.ValueKind==JsonValueKind.True;
584 621 public static bool DockingInputZeroConfirmed(JsonElement local)
585 622 {
586 623 if(!local.TryGetProperty("throttle",out var throttle)||throttle.ValueKind!=JsonValueKind.Number
Modified XFEEDToolBox.Core/Navigation/SystemNavigationRunner.cs +5 -1
@@ -49,14 +49,17 @@ public sealed class SystemNavigationRunner(Func<PlayerState> player,Func<DateTim
49 49 }
50 50 try
51 51 {
52 stop.Token.ThrowIfCancellationRequested();
52 53 var initial=player();var initialShip=initial.CurrentShipId;var initialCommander=initial.CommanderId;var initialSession=initial.LoadGameAt;
53 54 if(!initial.InGame||initial.CurrentShipId is null||initial.Navigation.CurrentSystemAddress!=target.SystemAddress||NavigationSafety.UnhealthyHull(initial))
54 55 throw new InvalidOperationException("请先进入健康、安全的飞船驾驶状态。");
55 56 if(NavigationSafety.HasExternalThreat(initial))throw new FlightThreatPauseException();
57 stop.Token.ThrowIfCancellationRequested();
58 if(TravelPreparation.AtDockedDestination(initial,target)){state=NavigationRunState.Completed;terminal=$"已停靠 {target.Name}";return;}
56 59 var capabilities=await driver.InspectAsync(stop.Token);
57 60 if(!capabilities.Supports(target,NavigationAssists.From(initial)))throw new NotSupportedException("当前游戏接口尚未完成此类星系内驾驶的验证。");
58 61 await driver.SelectAsync(target,stop.Token);
59 await driver.PrepareAsync(stop.Token);
62 if(!TravelPreparation.AtDockedDestination(player(),target))await driver.PrepareAsync(stop.Token);
60 63 var policy=new SystemFlightNavigator();long previous=0;ulong entity=0;string? loadout=null;DateTimeOffset? missingSince=null;
61 64 DockingResetObservationPendingException? pendingReset=null;var landingObserved=false;var requestFailures=0;
62 65 Report(phase,"正在核对星系内目标和驾驶状态。");
@@ -66,6 +69,7 @@ public sealed class SystemNavigationRunner(Func<PlayerState> player,Func<DateTim
66 69 if(!current.InGame||current.CurrentShipId!=initialShip||current.CommanderId!=initialCommander||current.LoadGameAt!=initialSession
67 70 ||NavigationSafety.UnhealthyHull(current))throw new InvalidOperationException("飞船、会话或安全状态改变,已停止星系内导航。");
68 71 if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
72 if(TravelPreparation.AtDockedDestination(current,target)){state=NavigationRunState.Completed;terminal=$"已停靠 {target.Name}";break;}
69 73 SystemFlightFrame frame;
70 74 try
71 75 {
Modified XFEEDToolBox.Core/Navigation/TravelPreparation.cs +36 -4
@@ -23,14 +23,42 @@ public static class TravelPreparation
23 23 &&player.StatusAt is {} eventAt&&(eventAt<=now.AddSeconds(5)||GameJournalClock.Offset(player,now) is not null)
24 24 &&(player.LoadGameAt is not {} sessionAt||eventAt>=sessionAt)
25 25 &&player.StatusReadAt is {} readAt&&readAt<=now.AddMilliseconds(100)&&now-readAt<=TimeSpan.FromSeconds(2);
26 public static async Task EnsureRetractedAsync(IGameInputChannel channel,Func<PlayerState> player,CancellationToken token,ulong? expectedEntity=null)
26 public static bool AtDockedDestination(PlayerState player,SystemNavigationTarget target)=>target.NeedsDocking&&player.InGame&&player.Docked
27 &&player.MarketId==target.MarketId&&player.Navigation.CurrentSystemAddress==target.SystemAddress;
28 public static bool PreserveOriginalLanding(SystemFlightFrame frame,SystemNavigationTarget target,DateTimeOffset now)=>target.NeedsDocking
29 &&frame.InGame&&!frame.PlayerOverride&&!frame.ExternalThreat&&frame.GeometryVerified&&frame.Sequence>0&&frame.ShipEntity!=0&&frame.ReferenceWorld!=0
30 &&frame.ObservedAt<=now&&now-frame.ObservedAt<=TimeSpan.FromMilliseconds(300)
31 &&frame.SystemAddress==target.SystemAddress&&frame.SelectedTarget is {} selected&&selected.SameIdentity(target)
32 &&frame.Mode is LocalFlightMode.Normal or LocalFlightMode.Transition&&frame.Assists.AutoDock&&frame.DockingAssist==NativeAssistState.Engaged
33 &&frame.Clearance is {Permission:DockingPermission.Granted} grant&&grant.MarketId==target.MarketId&&grant.ExpiresAt>=now
34 &&grant.ObservedAt<=now&&now-grant.ObservedAt<=TimeSpan.FromMilliseconds(300);
35 /// <returns>False when a verified arrival takes priority over travel equipment preparation.</returns>
36 public static async Task<bool> EnsureRetractedAsync(IGameInputChannel channel,Func<PlayerState> player,CancellationToken token,ulong? expectedEntity=null,
37 Func<CancellationToken,Task<bool>>? preserveArrival=null)
27 38 {
39 var initial=player();
40 PlayerState RequireState()
41 {
42 token.ThrowIfCancellationRequested();var current=player();
43 if(!current.InGame||current.CommanderId!=initial.CommanderId||current.CurrentShipId!=initial.CurrentShipId||current.LoadGameAt!=initial.LoadGameAt
44 ||current.Navigation.CurrentSystemAddress!=initial.Navigation.CurrentSystemAddress||NavigationSafety.UnhealthyHull(current))
45 throw new InvalidOperationException("收起设备时游戏会话或飞行安全状态已改变。");
46 if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
47 return current;
48 }
49 async Task<bool> YieldToArrivalAsync()
50 {
51 if(preserveArrival is null)return false;
52 var shouldYield=await preserveArrival(token);RequireState();return shouldYield;
53 }
28 54 var clock=Stopwatch.StartNew();ulong entity=0;long waitingGeneration=0;string? waitingInput=null;
29 55 DateTimeOffset? clearSince=null;var sentAt=TimeSpan.Zero;
30 56 while(clock.Elapsed<TimeSpan.FromSeconds(12))
31 57 {
32 token.ThrowIfCancellationRequested();var state=player();
33 if(!state.InGame||state.Docked||NavigationSafety.HasExternalThreat(state))throw new InvalidOperationException("收起设备时游戏会话或飞行安全状态已改变。");
58 var state=RequireState();
59 if(await YieldToArrivalAsync())return false;
60 state=RequireState();
61 if(state.Docked)throw new InvalidOperationException("收起设备时游戏会话或飞行安全状态已改变。");
34 62 GameInputState frame;
35 63 try{frame=await channel.ReadAsync(token);}
36 64 catch(Exception error)when(NavigationRecovery.CanRetry(error,token))
@@ -43,6 +71,10 @@ public static class TravelPreparation
43 71 var now=DateTimeOffset.UtcNow;
44 72 if(frame.Sequence<=0||frame.EntityId==0||frame.ObservedAt>now.AddMilliseconds(100)||now-frame.ObservedAt>TimeSpan.FromMilliseconds(300))throw new FlightTelemetryUnavailableException("收起设备的原生状态已过期。");
45 73 if(expectedEntity is {} expected&&expected!=frame.EntityId)throw new InvalidOperationException("输入通道与当前飞船身份不一致。");
74 if(await YieldToArrivalAsync())return false;
75 state=RequireState();if(state.Docked)throw new InvalidOperationException("收起设备时游戏会话或飞行安全状态已改变。");
76 now=DateTimeOffset.UtcNow;
77 if(now-frame.ObservedAt>TimeSpan.FromMilliseconds(300))throw new FlightTelemetryUnavailableException("收起设备的原生状态已过期。");
46 78 if(!StatusConfirmed(state,now))
47 79 {
48 80 clearSince=null;
@@ -53,7 +85,7 @@ public static class TravelPreparation
53 85 {
54 86 // Status reflects the physical deployment, independently of the
55 87 // desired input. Require a stable native confirmation as well.
56 clearSince??=now;if(now-clearSince>=TimeSpan.FromMilliseconds(80))return;
88 clearSince??=now;if(now-clearSince>=TimeSpan.FromMilliseconds(80))return true;
57 89 }
58 90 else clearSince=null;
59 91 var next=frame.Hardpoints?"hardpoints":frame.CargoScoop?"cargoScoop":frame.LandingGear?"landingGear":null;
Modified XFEEDToolBox.Test/FlightAutomationCoordinatorTests.cs +9 -1
@@ -26,6 +26,7 @@ public sealed class FlightAutomationCoordinatorTests
26 26 internal PlayerState Player=Initial;
27 27 internal readonly List<string> Calls=[];
28 28 internal Action? OnDepart;
29 internal Action? OnReady;
29 30 internal Func<CancellationToken,Task>? Departure;
30 31 internal Action? OnInspect;
31 32 internal bool Arrive=true,AutoLand=true;
@@ -34,7 +35,7 @@ public sealed class FlightAutomationCoordinatorTests
34 35 internal int Classifications;
35 36 public Task<Exception> ClassifyFailureAsync(Exception failure,CancellationToken token)
36 37 {token.ThrowIfCancellationRequested();Calls.Add("classify");Classifications++;return Task.FromResult(ClassifiedFailure??failure);}
37 public Task EnsureReadyAsync(CancellationToken token){Calls.Add("ready");return Task.CompletedTask;}
38 public Task EnsureReadyAsync(CancellationToken token){Calls.Add("ready");OnReady?.Invoke();return Task.CompletedTask;}
38 39 public async Task DepartAsync(CancellationToken token)
39 40 {
40 41 Calls.Add("depart");if(Departure is not null)await Departure(token);
@@ -85,6 +86,13 @@ public sealed class FlightAutomationCoordinatorTests
85 86 Check.Equal(1,driver.Calls.Count(value=>value=="depart"));Check.Equal(1,driver.Calls.Count(value=>value=="plan"));
86 87 Check.That(driver.Calls.IndexOf("release-galaxy")<driver.Calls.IndexOf("select"));Check.Equal("release-local",driver.Calls[^1]);
87 88 }
89 [Test]public async Task DockingAtTheDestinationWhileReadinessRecoversDoesNotLaunchAnotherDeparture()
90 {
91 var driver=new Journey();driver.Player=driver.Player with{Docked=false,SystemName="System 2",Navigation=driver.Player.Navigation with{CurrentSystemAddress=2}};
92 driver.OnReady=()=>driver.Player=driver.Player with{Docked=true,MarketId=20};
93 var result=await new FlightAutomationCoordinator(()=>driver.Player).TravelToStationAsync(Destination,driver);
94 Check.Equal(NavigationRunState.Completed,result.State);Check.Equal("ready",string.Join(',',driver.Calls));
95 }
88 96 [Test]public async Task BothStationEntriesClassifyAFailedLocalLegOnlyAfterItsInputsAreReleased()
89 97 {
90 98 foreach(var trade in new[]{false,true})
Modified XFEEDToolBox.Test/NativeDockingObservationTests.cs +13 -0
@@ -6,6 +6,19 @@ namespace XFEEDToolBox.Test;
6 6 [TestFixture]
7 7 public class NativeDockingObservationTests
8 8 {
9 [Test]public void PhysicalDockingAndAnOriginalDutyRemainObservationOnlyWhenTheGrantHasTransitioned()
10 {
11 using var complete=System.Text.Json.JsonDocument.Parse("{\"verified\":true,\"clearanceGranted\":false,\"nativeDutyActive\":false,\"nativeLaunchActive\":false,\"nativeLaunchRequested\":false}");
12 Check.That(NativeSystemFlightObservation.ObserveOriginalArrival(complete.RootElement,5));
13 Check.That(NativeSystemFlightObservation.ObserveOriginalArrival(complete.RootElement,7));
14 Check.That(!NativeSystemFlightObservation.ObserveOriginalArrival(complete.RootElement,3));
15 using var active=System.Text.Json.JsonDocument.Parse("{\"verified\":true,\"clearanceGranted\":false,\"nativeDutyActive\":true}");
16 Check.That(NativeSystemFlightObservation.ObserveOriginalArrival(active.RootElement,4));
17 using var unverified=System.Text.Json.JsonDocument.Parse("{\"verified\":false,\"nativeDutyActive\":true}");
18 Check.That(!NativeSystemFlightObservation.ObserveOriginalArrival(unverified.RootElement,5));
19 using var launch=System.Text.Json.JsonDocument.Parse("{\"verified\":true,\"nativeDutyActive\":true,\"nativeLaunchActive\":true}");
20 Check.That(!NativeSystemFlightObservation.ObserveOriginalArrival(launch.RootElement,5));
21 }
9 22 [Test] public void LaunchCancellationIncludesRequestsWhichHaveNotStartedTheirNativeDuty()
10 23 {
11 24 using var pending=System.Text.Json.JsonDocument.Parse("{\"nativeLaunchActive\":false,\"nativeLaunchRequested\":true}");
Modified XFEEDToolBox.Test/SystemNavigationRunnerTests.cs +22 -0
@@ -35,6 +35,28 @@ public class SystemNavigationRunnerTests
35 35 var driver=new Fake();var runner=new SystemNavigationRunner(()=>Player,()=>Now);await runner.RunAsync(Target,driver);
36 36 Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal("inspect,select,prepare,read,apply,release",string.Join(',',driver.Calls));
37 37 }
38 [Test]public async Task DockingAtTheSameMarketDuringPreparationCompletesWithoutReadingOrApplyingFlightInputs()
39 {
40 var player=Player;var driver=new Fake{OnPrepare=()=>player=player with{Docked=true,MarketId=20},Frame=_=>throw new InvalidOperationException("Docked physics need not keep updating.")};
41 var runner=new SystemNavigationRunner(()=>player,()=>Now);await runner.RunAsync(Target,driver);
42 Check.Equal(NavigationRunState.Completed,runner.State);Check.Equal("inspect,select,prepare,release",string.Join(',',driver.Calls));
43 }
44 [Test]public async Task ADockedArrivalNeverOverridesAChangedShipOrAnExternalCancellation()
45 {
46 foreach(var cancel in new[]{false,true})
47 {
48 using var stop=new CancellationTokenSource();var player=Player;var driver=new Fake{OnPrepare=()=>
49 {player=player with{Docked=true,MarketId=20};if(cancel)stop.Cancel();else player=player with{CurrentShipId=8};}};
50 var runner=new SystemNavigationRunner(()=>player,()=>Now);await runner.RunAsync(Target,driver,stop.Token);
51 Check.Equal(cancel?NavigationRunState.Stopped:NavigationRunState.Failed,runner.State);Check.Equal(0,driver.Calls.Count(call=>call=="read"||call=="apply"));
52 }
53 }
54 [Test]public async Task APreCancelledRequestAtTheDestinationDoesNotReportSuccessfulArrival()
55 {
56 using var stop=new CancellationTokenSource();stop.Cancel();var driver=new Fake();
57 var runner=new SystemNavigationRunner(()=>Player with{Docked=true,MarketId=20},()=>Now);await runner.RunAsync(Target,driver,stop.Token);
58 Check.Equal(NavigationRunState.Stopped,runner.State);Check.Equal("release",string.Join(',',driver.Calls));
59 }
38 60 [Test]public async Task RepeatedStillFreshPhysicsFrameWaitsWithoutReleasingControlOrReplayingInputs()
39 61 {
40 62 var at=Now;var driver=new Fake{Frame=n=>
Modified XFEEDToolBox.Test/TravelPreparationTests.cs +58 -1
@@ -13,11 +13,12 @@ public class TravelPreparationTests
13 13 public GameInputState Frame=new(1,42,DateTimeOffset.UtcNow,true,true,true);
14 14 public readonly List<string> Writes=[];
15 15 public Action? OnWrite;
16 public Action? OnRead;
16 17 public bool Refresh=true;
17 18 public int RefusedReads,Reads;
18 19 public string Refusal="Game input telemetry unavailable or stale";
19 20 public Task ConnectAsync(int thread,CancellationToken token)=>Task.CompletedTask;
20 public Task<GameInputState> ReadAsync(CancellationToken token){Reads++;if(RefusedReads-->0)throw new InvalidOperationException(Refusal);if(Refresh)Frame=Frame with{Sequence=Frame.Sequence+1,ObservedAt=DateTimeOffset.UtcNow};return Task.FromResult(Frame);}
21 public Task<GameInputState> ReadAsync(CancellationToken token){Reads++;OnRead?.Invoke();if(RefusedReads-->0)throw new InvalidOperationException(Refusal);if(Refresh)Frame=Frame with{Sequence=Frame.Sequence+1,ObservedAt=DateTimeOffset.UtcNow};return Task.FromResult(Frame);}
21 22 public Task<long> SetAsync(GameInputState frame,string input,bool value,CancellationToken token)
22 23 {
23 24 Check.That(!value);Writes.Add(input);OnWrite?.Invoke();
@@ -86,4 +87,60 @@ public class TravelPreparationTests
86 87 try{await TravelPreparation.EnsureRetractedAsync(c,()=>Player(1L<<6),CancellationToken.None,42);throw new Exception("Expected freshness check");}catch(FlightTelemetryUnavailableException){}
87 88 Check.Equal(0,c.Writes.Count);
88 89 }
90 private static readonly SystemNavigationTarget Arrival=new(1,10,"Elysian Fields",SystemTargetKind.Station,20);
91 private static SystemFlightFrame Landing=>new(){Sequence=1,ObservedAt=DateTimeOffset.UtcNow,InGame=true,GeometryVerified=true,ShipEntity=42,ReferenceWorld=10,
92 SelectedTarget=Arrival,SystemAddress=1,Mode=LocalFlightMode.Normal,Assists=new(false,true,false),DockingAssist=NativeAssistState.Engaged,
93 Clearance=new(20,DockingPermission.Granted,1,DateTimeOffset.UtcNow,DateTimeOffset.UtcNow.AddMilliseconds(300))};
94 [Test]public async Task ResumingAnOriginalLandingKeepsItsDeployedGearWithoutWritingAnyInput()
95 {
96 var channel=new Channel();var player=Player(4) with{CommanderId="F1",CurrentShipId=7,LoadGameAt=DateTimeOffset.UtcNow.AddHours(-1),Navigation=new(){CurrentSystemAddress=1}};
97 var retracted=await TravelPreparation.EnsureRetractedAsync(channel,()=>player,default,42,
98 _=>Task.FromResult(TravelPreparation.PreserveOriginalLanding(Landing,Arrival,DateTimeOffset.UtcNow)));
99 Check.That(!retracted);Check.Equal(0,channel.Reads);Check.Equal(0,channel.Writes.Count);
100 }
101 [Test]public async Task DockingOrOriginalHandoffDuringAnInputReadWinsBeforeAnyRetractionWrite()
102 {
103 foreach(var completes in new[]{false,true})
104 {
105 var player=Player(4) with{CommanderId="F1",CurrentShipId=7,LoadGameAt=DateTimeOffset.UtcNow.AddHours(-1),Navigation=new(){CurrentSystemAddress=1}};
106 var landing=false;var channel=new Channel{OnRead=()=>{landing=true;if(completes)player=player with{Docked=true,MarketId=20};}};
107 var retracted=await TravelPreparation.EnsureRetractedAsync(channel,()=>player,default,42,
108 _=>Task.FromResult(TravelPreparation.AtDockedDestination(player,Arrival)||landing&&TravelPreparation.PreserveOriginalLanding(Landing,Arrival,DateTimeOffset.UtcNow)));
109 Check.That(!retracted);Check.Equal(1,channel.Reads);Check.Equal(0,channel.Writes.Count);
110 }
111 }
112 [Test]public async Task ArrivalYieldCannotHideCancellationOrChangedShipHealthIdentity()
113 {
114 foreach(var cause in new[]{"cancel","commander","ship","session","hull","threat"})
115 {
116 using var stop=new CancellationTokenSource();var channel=new Channel();var player=Player(4) with{CommanderId="F1",CurrentShipId=7};
117 try
118 {
119 await TravelPreparation.EnsureRetractedAsync(channel,()=>player,stop.Token,42,async _=>
120 {
121 await Task.Delay(20);
122 if(cause=="cancel")stop.Cancel();
123 else if(cause=="commander")player=player with{CommanderId="other"};
124 else if(cause=="ship")player=player with{CurrentShipId=8};
125 else if(cause=="session")player=player with{LoadGameAt=DateTimeOffset.UtcNow};
126 else if(cause=="hull")player=player with{Ships=[new(){ShipId=7,HullHealth=0}]};
127 else player=player with{Navigation=new(){ExternalThreat=true}};
128 return true;
129 });
130 throw new Exception("An arrival must not override protection or user cancellation.");
131 }
132 catch(OperationCanceledException)when(cause=="cancel"){}
133 catch(InvalidOperationException)when(cause!="cancel"){}
134 Check.Equal(0,channel.Writes.Count);
135 }
136 }
137 [Test]public void OriginalLandingYieldRequiresFreshSameTargetClearanceAndNoManualOverride()
138 {
139 var frame=Landing;var now=DateTimeOffset.UtcNow;Check.That(TravelPreparation.PreserveOriginalLanding(frame,Arrival,now));
140 foreach(var invalid in new[]{frame with{PlayerOverride=true},frame with{ExternalThreat=true},frame with{DockingAssist=NativeAssistState.Ready},
141 frame with{ObservedAt=now.AddSeconds(-1)},frame with{SystemAddress=2},frame with{Clearance=frame.Clearance! with{MarketId=99}},
142 frame with{Clearance=frame.Clearance! with{ExpiresAt=now.AddMilliseconds(-1)}}})
143 Check.That(!TravelPreparation.PreserveOriginalLanding(invalid,Arrival,now));
144 Check.That(!TravelPreparation.AtDockedDestination(Player(4) with{Docked=true,MarketId=99,Navigation=new(){CurrentSystemAddress=1}},Arrival));
145 }
89 146 }