using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Session;
namespace XFEEDToolBox.Core.Navigation;
public sealed record FlightConnectionBinding(int ProcessId,long StartedUtcTicks,string GameSha256,string CommanderId,long ShipId,string Mode,DateTimeOffset SessionStartedAt,int CargoCount);
/// <summary>A positive original-menu connection failure, not a request to retry flight inputs or replay commerce.</summary>
public sealed class FlightConnectionRecoveryRequiredException(FlightConnectionBinding binding,JsonElement menu,JsonElement navigation,Exception original)
:InvalidOperationException("原版飞行会话已持续确认连接失败且无实时驾驶场景;保留原订单,等待官方连接恢复。",original)
{
public FlightConnectionBinding Binding{get;}=binding;
public JsonElement Menu{get;}=menu.Clone();
public JsonElement Navigation{get;}=navigation.Clone();
public static FlightConnectionRecoveryRequiredException? Find(Exception error)
{
if(FlightJourneyFailure.HasProtectedCause(error)||FlightConnectionRecovery.HasCancellationCause(error))return null;
if(error is FlightConnectionRecoveryRequiredException confirmed)return confirmed;
if(error is AggregateException aggregate)
{
// A distinct cancelled, ownership or other unknown cleanup cause
// cannot be discarded just because another branch has this proof.
if(aggregate.InnerExceptions.Any(inner=>Find(inner) is null
&&(inner is OperationCanceledException||!FlightJourneyFailure.HasTransientCause(inner,default))))return null;
var matches=aggregate.InnerExceptions.Select(Find).Where(value=>value is not null).ToArray();
return matches.Length==1?matches[0]:null;
}
return error.InnerException is {} inner?Find(inner):null;
}
}
public static class FlightConnectionRecovery
{
internal static bool HasCancellationCause(Exception error)=>error is OperationCanceledException
||error is AggregateException aggregate&&aggregate.InnerExceptions.Any(HasCancellationCause)
||error.InnerException is {} inner&&HasCancellationCause(inner);
public static void RequireCurrentIdentity(FlightConnectionBinding binding,PlayerState current,CancellationToken token)
{
token.ThrowIfCancellationRequested();
if(current.CommanderId!=binding.CommanderId||current.CurrentShipId!=binding.ShipId||current.GameMode!=binding.Mode
||current.LoadGameAt!=binding.SessionStartedAt||current.CargoCount!=binding.CargoCount)
throw new InvalidDataException("原飞行会话的身份、模式或实际货物已改变,未自动恢复连接。");
if(NavigationSafety.UnhealthyHull(current))throw new FlightHealthPauseException();
if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
}
public static void RequireOriginalClient(FlightConnectionBinding binding,GameSessionTarget current,string configuredExecutable)
{
if(current.ProcessId!=binding.ProcessId||current.StartedUtcTicks!=binding.StartedUtcTicks
||!string.Equals(Path.GetFullPath(current.Executable),Path.GetFullPath(configuredExecutable),StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException("已确认连接失败的游戏实例或安装目录已改变,未接管其他客户端。");
}
public static async Task<Exception> ClassifyAsync(Exception failure,FlightConnectionBinding binding,
Func<CancellationToken,Task<(JsonElement Menu,JsonElement Navigation)>> read,Func<CancellationToken,Task> verify,CancellationToken token,
TimeSpan? confirmation=null,TimeSpan? timeout=null,TimeSpan? pollInterval=null)
{
token.ThrowIfCancellationRequested();
if(HasCancellationCause(failure)||FlightConnectionRecoveryRequiredException.Find(failure) is not null||!FlightJourneyFailure.HasTransientCause(failure,token))return failure;
if(binding.ProcessId<=0||binding.StartedUtcTicks<=0||binding.GameSha256.Length!=64||binding.CommanderId.Length==0
||binding.ShipId<0||binding.CargoCount<0||binding.SessionStartedAt==default||!NativeRelogWorkflow.SupportsMode(binding.Mode))throw new InvalidDataException("连接故障缺少原飞行实例或身份绑定,未授权重启。");
using var budget=CancellationTokenSource.CreateLinkedTokenSource(token);budget.CancelAfter(timeout??TimeSpan.FromSeconds(4));
var observer=new NativeConnectionFailureObserver{ConfirmationPeriod=confirmation??TimeSpan.FromSeconds(2)};
try
{
while(true)
{
budget.Token.ThrowIfCancellationRequested();await verify(budget.Token);budget.Token.ThrowIfCancellationRequested();
var observed=await read(budget.Token);await verify(budget.Token);budget.Token.ThrowIfCancellationRequested();
var now=DateTimeOffset.UtcNow;
if(!NativeSessionStartupReadiness.FailedWithoutLiveFlight(observed.Menu,observed.Navigation,binding.ProcessId,binding.GameSha256,now))return failure;
if(observer.Observe(NativeRelogChannel.Parse(observed.Menu),now))
return new FlightConnectionRecoveryRequiredException(binding,observed.Menu,observed.Navigation,failure);
await Task.Delay(pollInterval??TimeSpan.FromMilliseconds(100),budget.Token);
}
}
catch(OperationCanceledException)when(!token.IsCancellationRequested&&budget.IsCancellationRequested){return failure;}
catch(Exception error)when(!token.IsCancellationRequested&&NavigationRecovery.CanRetry(error,token)){return failure;}
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Session;
namespace XFEEDToolBox.Core.Navigation;
public sealed record FlightConnectionBinding(int ProcessId,long StartedUtcTicks,string GameSha256,string CommanderId,long ShipId,string Mode,DateTimeOffset SessionStartedAt,int CargoCount);
/// <summary>A positive original-menu connection failure, not a request to retry flight inputs or replay commerce.</summary>
public sealed class FlightConnectionRecoveryRequiredException(FlightConnectionBinding binding,JsonElement menu,JsonElement navigation,Exception original)
:InvalidOperationException("原版飞行会话已持续确认连接失败且无实时驾驶场景;保留原订单,等待官方连接恢复。",original)
{
public FlightConnectionBinding Binding{get;}=binding;
public JsonElement Menu{get;}=menu.Clone();
public JsonElement Navigation{get;}=navigation.Clone();
public static FlightConnectionRecoveryRequiredException? Find(Exception error)
{
if(FlightJourneyFailure.HasProtectedCause(error)||FlightConnectionRecovery.HasCancellationCause(error))return null;
if(error is FlightConnectionRecoveryRequiredException confirmed)return confirmed;
if(error is AggregateException aggregate)
{
// A distinct cancelled, ownership or other unknown cleanup cause
// cannot be discarded just because another branch has this proof.
if(aggregate.InnerExceptions.Any(inner=>Find(inner) is null
&&(inner is OperationCanceledException||!FlightJourneyFailure.HasTransientCause(inner,default))))return null;
var matches=aggregate.InnerExceptions.Select(Find).Where(value=>value is not null).ToArray();
return matches.Length==1?matches[0]:null;
}
return error.InnerException is {} inner?Find(inner):null;
}
}
public static class FlightConnectionRecovery
{
internal static bool HasCancellationCause(Exception error)=>error is OperationCanceledException
||error is AggregateException aggregate&&aggregate.InnerExceptions.Any(HasCancellationCause)
||error.InnerException is {} inner&&HasCancellationCause(inner);
public static void RequireCurrentIdentity(FlightConnectionBinding binding,PlayerState current,CancellationToken token)
{
token.ThrowIfCancellationRequested();
if(current.CommanderId!=binding.CommanderId||current.CurrentShipId!=binding.ShipId||current.GameMode!=binding.Mode
||current.LoadGameAt!=binding.SessionStartedAt||current.CargoCount!=binding.CargoCount)
throw new InvalidDataException("原飞行会话的身份、模式或实际货物已改变,未自动恢复连接。");
if(NavigationSafety.UnhealthyHull(current))throw new FlightHealthPauseException();
if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
}
public static void RequireOriginalClient(FlightConnectionBinding binding,GameSessionTarget current,string configuredExecutable)
{
if(current.ProcessId!=binding.ProcessId||current.StartedUtcTicks!=binding.StartedUtcTicks
||!string.Equals(Path.GetFullPath(current.Executable),Path.GetFullPath(configuredExecutable),StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException("已确认连接失败的游戏实例或安装目录已改变,未接管其他客户端。");
}
public static async Task<Exception> ClassifyAsync(Exception failure,FlightConnectionBinding binding,
Func<CancellationToken,Task<(JsonElement Menu,JsonElement Navigation)>> read,Func<CancellationToken,Task> verify,CancellationToken token,
TimeSpan? confirmation=null,TimeSpan? timeout=null,TimeSpan? pollInterval=null)
{
token.ThrowIfCancellationRequested();
if(HasCancellationCause(failure)||FlightConnectionRecoveryRequiredException.Find(failure) is not null||!FlightJourneyFailure.HasTransientCause(failure,token))return failure;
if(binding.ProcessId<=0||binding.StartedUtcTicks<=0||binding.GameSha256.Length!=64||binding.CommanderId.Length==0
||binding.ShipId<0||binding.CargoCount<0||binding.SessionStartedAt==default||!NativeRelogWorkflow.SupportsMode(binding.Mode))throw new InvalidDataException("连接故障缺少原飞行实例或身份绑定,未授权重启。");
using var budget=CancellationTokenSource.CreateLinkedTokenSource(token);budget.CancelAfter(timeout??TimeSpan.FromSeconds(4));
var observer=new NativeConnectionFailureObserver{ConfirmationPeriod=confirmation??TimeSpan.FromSeconds(2)};
try
{
while(true)
{
budget.Token.ThrowIfCancellationRequested();await verify(budget.Token);budget.Token.ThrowIfCancellationRequested();
var observed=await read(budget.Token);await verify(budget.Token);budget.Token.ThrowIfCancellationRequested();
var now=DateTimeOffset.UtcNow;
if(!NativeSessionStartupReadiness.FailedWithoutLiveFlight(observed.Menu,observed.Navigation,binding.ProcessId,binding.GameSha256,now))return failure;
if(observer.Observe(NativeRelogChannel.Parse(observed.Menu),now))
return new FlightConnectionRecoveryRequiredException(binding,observed.Menu,observed.Navigation,failure);
await Task.Delay(pollInterval??TimeSpan.FromMilliseconds(100),budget.Token);
}
}
catch(OperationCanceledException)when(!token.IsCancellationRequested&&budget.IsCancellationRequested){return failure;}
catch(Exception error)when(!token.IsCancellationRequested&&NavigationRecovery.CanRetry(error,token)){return failure;}
}
}