using System.Diagnostics; using System.Text.Json; using XFEEDToolBox.Core.Models; namespace XFEEDToolBox.Core.Session; public interface INativeSessionEntryStatus { Task ReadEntryStatusAsync(CancellationToken token); } public sealed class NativeSessionConnectionFailedException(NativeSessionFrame frame,GameSessionTarget? client=null) :IOException("原版客户端已持续确认连接失败,尚未进入可驾驶会话。") { public NativeSessionFrame Frame{get;}=frame; public GameSessionTarget? Client{get;}=client; } public sealed class StartupConnectionRecoveryExhaustedException(NativeSessionConnectionFailedException failure,int recoveries) :IOException($"原版连接失败已完成 {recoveries} 次有界恢复,保留客户端等待下一轮连接。",failure) { public NativeSessionConnectionFailedException Failure{get;}=failure; } /// Repeated publications cannot turn a loading phase into a connection failure. public sealed class NativeConnectionFailureObserver { public TimeSpan ConfirmationPeriod{get;init;}=TimeSpan.FromSeconds(2); private NativeSessionFrame? first,last; public bool Observe(NativeSessionFrame frame,DateTimeOffset now) { if(!frame.Available||!NativeMenuExitSafety.Fresh(frame,now)||!frame.Stable||frame.State!="FailedToConnect") {first=last=null;return false;} if(last is {} previous) { if(frame.Sequence==previous.Sequence&&frame.ObservedAt==previous.ObservedAt)return false; if(frame.Sequence<=previous.Sequence||frame.ObservedAt<=previous.ObservedAt||frame.ObservedAt-previous.ObservedAt>TimeSpan.FromMilliseconds(500))first=null; } first??=frame;last=frame; return frame.ObservedAt-first.ObservedAt>=ConfirmationPeriod; } } public static class NativeSessionStartupReadiness { public static bool JourneyEntryRequired(PlayerState current,NativeSessionFrame menu,JsonElement navigation,int processId,string hash,DateTimeOffset started,DateTimeOffset now) { bool Flag(string name,bool expected)=>navigation.TryGetProperty(name,out var value)&&value.ValueKind==(expected?JsonValueKind.True:JsonValueKind.False); if(navigation.GetProperty("schema").GetInt32()!=1||navigation.GetProperty("name").GetString()!="XFEEDToolBox.Navigation" ||navigation.GetProperty("processId").GetInt32()!=processId||navigation.GetProperty("gameSha256").GetString()!=hash||!Flag("verified",true)) throw new InvalidDataException("完整航程的原生实例或构建身份不匹配。"); if(Ready(current,menu,now,started)&&Flag("inGame",true))return false; if(!menu.Available||!NativeMenuExitSafety.Fresh(menu,now)||!menu.Stable ||!(NativeMenuExitSafety.MenuOrLoading(menu.State)||menu.State=="FailedToConnect") ||!(NativeMenuExitSafety.MenuOrLoading(menu.NextState)||menu.NextState=="FailedToConnect") ||!Flag("inGame",false)||!Flag("continuousPose",false)||!Flag("continuousHeat",false)) throw new InvalidDataException("尚未确认原版主菜单或载入状态,已保留当前实例;未自动重启或发送驾驶输入。"); if(current.LoadGameAt>=started&&Navigation.NavigationSafety.HasExternalThreat(current))throw new Navigation.FlightThreatPauseException(); return true; } public static bool Ready(PlayerState current,NativeSessionFrame frame,DateTimeOffset now,DateTimeOffset? processStarted=null) =>frame.Available&&NativeMenuExitSafety.Fresh(frame,now)&&frame.Stable&&frame.State=="InGame"&¤t.InGame &¤t.LoadGameAt is {} loaded&&(processStarted is null||loaded>=processStarted) &¤t.LocationAt>=loaded&¤t.CargoReliable&¤t.CargoAt>=loaded &¤t.Navigation.CurrentSystemAddress is >0&¤t.SystemName.Length>0; public static bool FailedWithoutLiveFlight(JsonElement menu,JsonElement navigation,int processId,string hash,DateTimeOffset now) { static bool Flag(JsonElement row,string key,bool expected)=>row.TryGetProperty(key,out var value) &&value.ValueKind==(expected?JsonValueKind.True:JsonValueKind.False); try { return processId>0&&menu.GetProperty("schema").GetInt32()==1&&menu.GetProperty("name").GetString()=="XFEEDToolBox.Session" &&menu.GetProperty("processId").GetInt32()==processId&&menu.GetProperty("gameSha256").GetString()==hash&&Flag(menu,"available",true) &&menu.GetProperty("sequence").GetInt64()>0&&menu.GetProperty("state").GetString()=="FailedToConnect"&&menu.GetProperty("nextState").GetString()=="FailedToConnect" &&now.ToUnixTimeMilliseconds()-menu.GetProperty("observedAtUnixMilliseconds").GetInt64() is >=-100 and <=350 &&navigation.GetProperty("schema").GetInt32()==1&&navigation.GetProperty("name").GetString()=="XFEEDToolBox.Navigation" &&navigation.GetProperty("processId").GetInt32()==processId&&navigation.GetProperty("gameSha256").GetString()==hash&&Flag(navigation,"verified",true) // ready describes the initialized service, not a live cockpit. &&Flag(navigation,"inGame",false)&&Flag(navigation,"continuousPose",false)&&Flag(navigation,"continuousHeat",false); } catch(Exception error)when(error is KeyNotFoundException or InvalidOperationException or FormatException or OverflowException){return false;} } } /// Only a typed original-menu failure can retry. It never replays a trade. public static class StartupConnectionRecovery { /// Disk/report work must finish before this final live witness; never kill from its earlier snapshot. public static async Task TerminateWithFreshEvidenceAsync(Func> verify, Func terminate,CancellationToken token) { token.ThrowIfCancellationRequested(); var confirmed=await verify(token).ConfigureAwait(false); token.ThrowIfCancellationRequested(); if(!confirmed)throw new InvalidDataException("结束客户端前原版连接失败或无驾驶场景证据已改变,当前实例已保留。"); await terminate(token).ConfigureAwait(false); } public static async Task RunAsync(Func,CancellationToken,Task> attempt, Func recover,CancellationToken token, bool allowVerifiedExisting=false,int maximumRecoveries=2,TimeSpan? totalTimeout=null,Func? retryDelay=null) { if(maximumRecoveries is <0 or >3)throw new ArgumentOutOfRangeException(nameof(maximumRecoveries)); var limit=totalTimeout??TimeSpan.FromMinutes(20);if(limit<=TimeSpan.Zero)throw new ArgumentOutOfRangeException(nameof(totalTimeout)); using var budget=CancellationTokenSource.CreateLinkedTokenSource(token);budget.CancelAfter(limit); try { for(var count=0;;count++) { GameSessionTarget? owned=null; try{budget.Token.ThrowIfCancellationRequested();await attempt(client=>owned=client,budget.Token).ConfigureAwait(false);return;} catch(NativeSessionConnectionFailedException failure)when(!budget.IsCancellationRequested&&count=maximumRecoveries&&failure.Client is not null &&(allowVerifiedExisting||owned==failure.Client)) {throw new StartupConnectionRecoveryExhaustedException(failure,count);} } } catch(OperationCanceledException)when(!token.IsCancellationRequested&&budget.IsCancellationRequested) {throw new TimeoutException("原版连接失败恢复已达到总期限,客户端和原订单已保留。");} } public static async Task SuperviseAsync(Func enter, Func wait,CancellationToken token) { var rounds=0; while(true) { token.ThrowIfCancellationRequested(); // The waiting callback can update the invoking UI's run status. // Keep that context; the startup operation itself owns its worker. try{await enter(token);return;} catch(StartupConnectionRecoveryExhaustedException failure)when(!token.IsCancellationRequested) {await wait(failure,++rounds,token);} } } }