namespace XFEEDToolBox.Core.Navigation;
public enum FlightRecovery { RefreshTelemetry, ReadNextFrame, ReselectTarget, RefreshDriver }
/// <summary>Only known timing/transport refusals are retried. Protocol, ownership and build failures remain terminal.</summary>
public static class NavigationRecovery
{
public static Task<T> ReadWithRecoveryAsync<T>(Func<CancellationToken,Task<T>> read,Action? recovering,CancellationToken token)
=>ReadWithRecoveryAsync(read,recovering,token,TimeSpan.FromSeconds(30));
public static async Task<T> ReadWithRecoveryAsync<T>(Func<CancellationToken,Task<T>> read,Action? recovering,CancellationToken token,TimeSpan maximumWait)
{
var watch=System.Diagnostics.Stopwatch.StartNew();var attempts=0;var limit=maximumWait;
if(limit<TimeSpan.Zero&&limit!=Timeout.InfiniteTimeSpan)throw new ArgumentOutOfRangeException(nameof(maximumWait));
while(true)
{
token.ThrowIfCancellationRequested();
try{return await read(token);}
catch(Exception error)when(CanRetry(error,token)&&(limit==Timeout.InfiniteTimeSpan||watch.Elapsed<limit))
{recovering?.Invoke();attempts=Math.Min(10,attempts+1);await Task.Delay(100*attempts,token);}
}
}
public static async Task<FlightDriverCapabilities> PreflightWithRecoveryAsync(IFlightControlDriver driver,Action? recovering,CancellationToken token)
{
var attempts=0;
while(true)
{
token.ThrowIfCancellationRequested();
try
{
var capabilities=await driver.PreflightAsync(token);
RequireReady(capabilities);
return capabilities;
}
catch(Exception error)when(CanRetry(error,token))
{
recovering?.Invoke();
await Task.Delay(Math.Min(1000,100*++attempts),token);
}
}
}
public static void RequireReady(FlightDriverCapabilities? capabilities)
{
if(capabilities is null)throw new FlightTelemetryUnavailableException("正在重新连接原生导航接口。",recovery:FlightRecovery.RefreshDriver);
if(!capabilities.InterfacesVerified)throw new InvalidOperationException("原生恒星包络、转向或充能联锁接口未通过构建验证,不能发送飞行控制。");
if(!capabilities.Ready)throw new FlightTelemetryUnavailableException("正在等待最新姿态和热量,恢复后自动继续原航线。",recovery:FlightRecovery.RefreshDriver);
}
public static bool HasInvalidDataCause(Exception error)=>error is InvalidDataException
||error is AggregateException aggregate&&aggregate.InnerExceptions.Any(HasInvalidDataCause)
||error.InnerException is {} inner&&HasInvalidDataCause(inner);
public static bool CanRetry(Exception error,CancellationToken stop)=>!stop.IsCancellationRequested&&!HasInvalidDataCause(error)&&
(error is FlightTelemetryUnavailableException or OperationCanceledException or TimeoutException
||error is IOException
||error is InvalidOperationException&&NativeRefusal(error.Message) is not null);
public static FlightRecovery Kind(Exception? error)=>error is FlightTelemetryUnavailableException transient?transient.Recovery
:error is InvalidOperationException?NativeRefusal(error.Message)??FlightRecovery.RefreshTelemetry:FlightRecovery.RefreshTelemetry;
private static FlightRecovery? NativeRefusal(string message)=>message switch
{
"Navigation reference frame expired; read fresh telemetry and retry" or
"FSD calibration frame or deadline is invalid" or
"Invalid or expired game input request" or
"Native jump commit requires full charge, alignment and full throttle" or
"Native thermal or stellar clearance gate refused FSD charging" or
"Supercruise entry requires verified outward full-speed departure"=>FlightRecovery.ReadNextFrame,
"Fresh healthy player ship required for local navigation" or
"Stop flight control before selecting a local destination" or
"Local control frame expired or selection changed"=>FlightRecovery.ReadNextFrame,
"Native target or primary-star envelope changed"=>FlightRecovery.ReselectTarget,
"Native player telemetry unavailable or stale" or
"Native main-ship health unavailable or stale" or
"Local target telemetry unavailable or stale" or
"Game input telemetry unavailable or stale" or
"A fresh active ship and system target are required"=>FlightRecovery.RefreshTelemetry,
_=>null
};
}
namespace XFEEDToolBox.Core.Navigation;
public enum FlightRecovery { RefreshTelemetry, ReadNextFrame, ReselectTarget, RefreshDriver }
/// <summary>Only known timing/transport refusals are retried. Protocol, ownership and build failures remain terminal.</summary>
public static class NavigationRecovery
{
public static Task<T> ReadWithRecoveryAsync<T>(Func<CancellationToken,Task<T>> read,Action? recovering,CancellationToken token)
=>ReadWithRecoveryAsync(read,recovering,token,TimeSpan.FromSeconds(30));
public static async Task<T> ReadWithRecoveryAsync<T>(Func<CancellationToken,Task<T>> read,Action? recovering,CancellationToken token,TimeSpan maximumWait)
{
var watch=System.Diagnostics.Stopwatch.StartNew();var attempts=0;var limit=maximumWait;
if(limit<TimeSpan.Zero&&limit!=Timeout.InfiniteTimeSpan)throw new ArgumentOutOfRangeException(nameof(maximumWait));
while(true)
{
token.ThrowIfCancellationRequested();
try{return await read(token);}
catch(Exception error)when(CanRetry(error,token)&&(limit==Timeout.InfiniteTimeSpan||watch.Elapsed<limit))
{recovering?.Invoke();attempts=Math.Min(10,attempts+1);await Task.Delay(100*attempts,token);}
}
}
public static async Task<FlightDriverCapabilities> PreflightWithRecoveryAsync(IFlightControlDriver driver,Action? recovering,CancellationToken token)
{
var attempts=0;
while(true)
{
token.ThrowIfCancellationRequested();
try
{
var capabilities=await driver.PreflightAsync(token);
RequireReady(capabilities);
return capabilities;
}
catch(Exception error)when(CanRetry(error,token))
{
recovering?.Invoke();
await Task.Delay(Math.Min(1000,100*++attempts),token);
}
}
}
public static void RequireReady(FlightDriverCapabilities? capabilities)
{
if(capabilities is null)throw new FlightTelemetryUnavailableException("正在重新连接原生导航接口。",recovery:FlightRecovery.RefreshDriver);
if(!capabilities.InterfacesVerified)throw new InvalidOperationException("原生恒星包络、转向或充能联锁接口未通过构建验证,不能发送飞行控制。");
if(!capabilities.Ready)throw new FlightTelemetryUnavailableException("正在等待最新姿态和热量,恢复后自动继续原航线。",recovery:FlightRecovery.RefreshDriver);
}
public static bool HasInvalidDataCause(Exception error)=>error is InvalidDataException
||error is AggregateException aggregate&&aggregate.InnerExceptions.Any(HasInvalidDataCause)
||error.InnerException is {} inner&&HasInvalidDataCause(inner);
public static bool CanRetry(Exception error,CancellationToken stop)=>!stop.IsCancellationRequested&&!HasInvalidDataCause(error)&&
(error is FlightTelemetryUnavailableException or OperationCanceledException or TimeoutException
||error is IOException
||error is InvalidOperationException&&NativeRefusal(error.Message) is not null);
public static FlightRecovery Kind(Exception? error)=>error is FlightTelemetryUnavailableException transient?transient.Recovery
:error is InvalidOperationException?NativeRefusal(error.Message)??FlightRecovery.RefreshTelemetry:FlightRecovery.RefreshTelemetry;
private static FlightRecovery? NativeRefusal(string message)=>message switch
{
"Navigation reference frame expired; read fresh telemetry and retry" or
"FSD calibration frame or deadline is invalid" or
"Invalid or expired game input request" or
"Native jump commit requires full charge, alignment and full throttle" or
"Native thermal or stellar clearance gate refused FSD charging" or
"Supercruise entry requires verified outward full-speed departure"=>FlightRecovery.ReadNextFrame,
"Fresh healthy player ship required for local navigation" or
"Stop flight control before selecting a local destination" or
"Local control frame expired or selection changed"=>FlightRecovery.ReadNextFrame,
"Native target or primary-star envelope changed"=>FlightRecovery.ReselectTarget,
"Native player telemetry unavailable or stale" or
"Native main-ship health unavailable or stale" or
"Local target telemetry unavailable or stale" or
"Game input telemetry unavailable or stale" or
"A fresh active ship and system target are required"=>FlightRecovery.RefreshTelemetry,
_=>null
};
}