using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Navigation;
public interface ISystemFlightControlDriver
{
Task<SystemNavigationCapabilities> InspectAsync(CancellationToken token);
Task SelectAsync(SystemNavigationTarget target,CancellationToken token);
Task PrepareAsync(CancellationToken token);
Task<SystemFlightFrame> ReadSystemFlightAsync(CancellationToken token);
Task ApplySystemFlightAsync(SystemFlightDecision decision,SystemFlightFrame reference,SystemNavigationTarget target,DateTimeOffset expiresAt,CancellationToken token);
Task ReleaseSystemFlightAsync();
Task SuspendSystemFlightAsync()=>ReleaseSystemFlightAsync();
}
public sealed record SystemNavigationProgress(NavigationRunState State,SystemNavigationTarget Target,SystemNavigationPhase Phase,string Message)
{public string? Diagnostic{get;init;}}
public sealed class DockingHandoffException(string message,long marketId,Exception? inner=null):InvalidOperationException(message,inner)
{public long MarketId{get;}=marketId;}
public sealed class DockingRequestNotAppliedException():IOException("游戏尚未确认空管请求已执行。");
public sealed class DockingResetObservationPendingException(string requestId,long marketId,DateTimeOffset requestedAt,Exception inner)
:Exception("归零尚未确认,正在只读核对游戏自动降落接管,不重发请求。",inner)
{public string RequestId{get;}=requestId;public long MarketId{get;}=marketId;public DateTimeOffset RequestedAt{get;}=requestedAt;}
/// <summary>One local leg, with expiring inputs, actual target acknowledgements and bounded telemetry recovery.</summary>
public sealed class SystemNavigationRunner(Func<PlayerState> player,Func<DateTimeOffset>? clock=null)
{
private readonly Func<DateTimeOffset> now=clock??(()=>DateTimeOffset.UtcNow);
private readonly object gate=new();
private CancellationTokenSource? cancellation;
public NavigationRunState State{get;private set;}
public bool HandoffFailed{get;private set;}
public Exception? Failure{get;private set;}
public string LastMessage{get;private set;}="";
public event Action<SystemNavigationProgress>? Progress;
public void Stop(){lock(gate)cancellation?.Cancel();}
public async Task RunAsync(SystemNavigationTarget target,ISystemFlightControlDriver driver,CancellationToken token=default)
{
target.Validate();CancellationTokenSource stop;
lock(gate)
{
if(cancellation is not null)throw new InvalidOperationException("星系内导航已在运行。");
stop=CancellationTokenSource.CreateLinkedTokenSource(token);cancellation=stop;
}
HandoffFailed=false;Failure=null;var phase=SystemNavigationPhase.Preparing;var state=NavigationRunState.Running;var terminal="星系内导航已停止。";string? reportedMessage=null;
void Report(SystemNavigationPhase next,string message,string? diagnostic=null)
{
if(State==NavigationRunState.Running&&phase==next&&reportedMessage==message)return;
phase=next;reportedMessage=message;State=NavigationRunState.Running;Progress?.Invoke(new(State,target,next,message){Diagnostic=diagnostic});
}
try
{
stop.Token.ThrowIfCancellationRequested();
var initial=player();var initialShip=initial.CurrentShipId;var initialCommander=initial.CommanderId;var initialSession=initial.LoadGameAt;
if(!initial.InGame||initial.CurrentShipId is null||initial.Navigation.CurrentSystemAddress!=target.SystemAddress||NavigationSafety.UnhealthyHull(initial))
throw new InvalidOperationException("请先进入健康、安全的飞船驾驶状态。");
if(NavigationSafety.HasExternalThreat(initial))throw new FlightThreatPauseException();
stop.Token.ThrowIfCancellationRequested();
if(TravelPreparation.AtDockedDestination(initial,target)){state=NavigationRunState.Completed;terminal=$"已停靠 {target.Name}";return;}
var capabilities=await driver.InspectAsync(stop.Token);
if(!capabilities.Supports(target,NavigationAssists.From(initial)))throw new NotSupportedException("当前游戏接口尚未完成此类星系内驾驶的验证。");
await driver.SelectAsync(target,stop.Token);
if(!TravelPreparation.AtDockedDestination(player(),target))await driver.PrepareAsync(stop.Token);
var policy=new SystemFlightNavigator();long previous=0;ulong entity=0;string? loadout=null;DateTimeOffset? missingSince=null;
DockingResetObservationPendingException? pendingReset=null;var landingObserved=false;var requestFailures=0;
Report(phase,"正在核对星系内目标和驾驶状态。");
while(true)
{
stop.Token.ThrowIfCancellationRequested();var current=player();
if(!current.InGame||current.CurrentShipId!=initialShip||current.CommanderId!=initialCommander||current.LoadGameAt!=initialSession
||NavigationSafety.UnhealthyHull(current))throw new InvalidOperationException("飞船、会话或安全状态改变,已停止星系内导航。");
if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
if(TravelPreparation.AtDockedDestination(current,target)){state=NavigationRunState.Completed;terminal=$"已停靠 {target.Name}";break;}
SystemFlightFrame frame;
try
{
frame=await driver.ReadSystemFlightAsync(stop.Token);
if(frame.Sequence<previous||frame.ObservedAt>now()||now()-frame.ObservedAt>TimeSpan.FromMilliseconds(300)||!frame.GeometryVerified)
throw new FlightTelemetryUnavailableException("等待新飞行数据。");
if(frame.GameSha256!=capabilities.GameSha256||string.IsNullOrWhiteSpace(frame.LoadoutFingerprint))throw new InvalidOperationException("飞行数据构建或装配身份不匹配。");
var journalNow=GameJournalClock.Now(current,now());
var confirmedTransition=frame.ReplacesShipEntity==entity&&frame.SceneTransitionAt is {} transition&&transition<=journalNow&&journalNow-transition<TimeSpan.FromSeconds(30)
&&transition>=initialSession&&(transition==current.Navigation.SupercruiseEnteredAt||transition==current.Navigation.SupercruiseExitedAt);
if(entity!=0&&frame.ShipEntity!=entity&&!confirmedTransition||loadout is not null&&frame.LoadoutFingerprint!=loadout)throw new InvalidOperationException("受控飞船或装配改变,已停止导航。");
if(frame.Sequence==previous){await Task.Delay(40,stop.Token);continue;}
entity=frame.ShipEntity;loadout=frame.LoadoutFingerprint;previous=frame.Sequence;
var decision=policy.Decide(frame,target,now());
if(pendingReset is not null)
{
landingObserved|=frame.DockingAssist==NativeAssistState.Engaged;
if(!landingObserved&&decision.Phase!=SystemNavigationPhase.Stopped&&!decision.Complete
&&DockingHandoffPolicy.RecoveryRequired(frame.TargetDistance,pendingReset.RequestedAt,now()))
decision=new(SystemNavigationPhase.Stopped,DockingHandoffPolicy.RecoveryMessage(frame.TargetDistance),null,null,SystemNavigationAction.ExitForFailedHandoff);
else if(!landingObserved&&!decision.Complete&&now()-pendingReset.RequestedAt>TimeSpan.FromSeconds(30))
throw new InvalidOperationException("自动降落回执和接管状态仍未确认,原请求未重发。",pendingReset);
if(decision.Phase!=SystemNavigationPhase.Stopped&&!decision.Complete)
decision=decision with{Phase=SystemNavigationPhase.NativeLanding,Message=landingObserved?"自动降落已接管,正在核对实际停靠。":"正在核对归零回执与自动降落状态,不重发请求。"};
}
Report(decision.Phase,decision.Message);
if(decision.Phase==SystemNavigationPhase.Stopped)
{
if(decision.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff))
{
HandoffFailed=true;Failure=pendingReset;
await driver.ApplySystemFlightAsync(decision,frame,target,frame.ObservedAt.AddMilliseconds(250),stop.Token);
}
state=NavigationRunState.Stopped;terminal=decision.Message;break;
}
// A lost acknowledgement is not permission for a second
// arm or another axis lease. Observe the actual AI/dock;
// the same distance/ten-second handoff guard remains above.
if(pendingReset is null)
{
await driver.ApplySystemFlightAsync(decision,frame,target,frame.ObservedAt.AddMilliseconds(250),stop.Token);
if(decision.Actions.HasFlag(SystemNavigationAction.RequestDocking))requestFailures=0;
}
missingSince=null;
if(decision.Complete){state=NavigationRunState.Completed;terminal=decision.Message;break;}
}
catch(DockingRequestNotAppliedException)
{
requestFailures++;
try{await driver.SuspendSystemFlightAsync();}catch(Exception release)when(NavigationRecovery.CanRetry(release,stop.Token)){}
if(requestFailures>=3)
{
HandoffFailed=true;
throw new DockingHandoffException("连续 3 次空管请求未被游戏执行,正在恢复原会话后重试。",target.MarketId!.Value);
}
Report(SystemNavigationPhase.AwaitingClearance,$"游戏尚未执行空管请求,正在核对后重试({requestFailures}/3)。");
}
catch(DockingResetObservationPendingException error)
{
if(error.MarketId!=target.MarketId)throw new InvalidDataException("未确认的归零请求属于其他市场。",error);
pendingReset=error;Report(SystemNavigationPhase.NativeLanding,error.Message);
}
catch(Exception error)when(NavigationRecovery.CanRetry(error,stop.Token))
{
missingSince??=now();
if(pendingReset is null)
try{await driver.SuspendSystemFlightAsync();}catch(Exception release)when(NavigationRecovery.CanRetry(release,stop.Token)){}
if(now()-missingSince>TimeSpan.FromSeconds(30))throw new InvalidOperationException("星系内飞行数据恢复未成功,已停止并交还控制。",error);
Report(SystemNavigationPhase.Waiting,"正在重新连接飞行数据并自动重试。",error.ToString());
}
await Task.Delay(80,stop.Token);
}
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){state=NavigationRunState.Stopped;phase=SystemNavigationPhase.Stopped;}
catch(Exception error){Failure=error;state=NavigationRunState.Failed;phase=SystemNavigationPhase.Stopped;terminal=error.Message;}
finally
{
try{await driver.ReleaseSystemFlightAsync();}
catch(Exception error){Failure=Failure is null?error:new AggregateException(Failure,error);state=NavigationRunState.Failed;terminal="已停止导航,输入释放未获确认:"+error.Message;}
lock(gate){cancellation=null;stop.Dispose();State=state;}
LastMessage=terminal;Progress?.Invoke(new(state,target,phase,terminal));
}
}
}
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Navigation;
public interface ISystemFlightControlDriver
{
Task<SystemNavigationCapabilities> InspectAsync(CancellationToken token);
Task SelectAsync(SystemNavigationTarget target,CancellationToken token);
Task PrepareAsync(CancellationToken token);
Task<SystemFlightFrame> ReadSystemFlightAsync(CancellationToken token);
Task ApplySystemFlightAsync(SystemFlightDecision decision,SystemFlightFrame reference,SystemNavigationTarget target,DateTimeOffset expiresAt,CancellationToken token);
Task ReleaseSystemFlightAsync();
Task SuspendSystemFlightAsync()=>ReleaseSystemFlightAsync();
}
public sealed record SystemNavigationProgress(NavigationRunState State,SystemNavigationTarget Target,SystemNavigationPhase Phase,string Message)
{public string? Diagnostic{get;init;}}
public sealed class DockingHandoffException(string message,long marketId,Exception? inner=null):InvalidOperationException(message,inner)
{public long MarketId{get;}=marketId;}
public sealed class DockingRequestNotAppliedException():IOException("游戏尚未确认空管请求已执行。");
public sealed class DockingResetObservationPendingException(string requestId,long marketId,DateTimeOffset requestedAt,Exception inner)
:Exception("归零尚未确认,正在只读核对游戏自动降落接管,不重发请求。",inner)
{public string RequestId{get;}=requestId;public long MarketId{get;}=marketId;public DateTimeOffset RequestedAt{get;}=requestedAt;}
/// <summary>One local leg, with expiring inputs, actual target acknowledgements and bounded telemetry recovery.</summary>
public sealed class SystemNavigationRunner(Func<PlayerState> player,Func<DateTimeOffset>? clock=null)
{
private readonly Func<DateTimeOffset> now=clock??(()=>DateTimeOffset.UtcNow);
private readonly object gate=new();
private CancellationTokenSource? cancellation;
public NavigationRunState State{get;private set;}
public bool HandoffFailed{get;private set;}
public Exception? Failure{get;private set;}
public string LastMessage{get;private set;}="";
public event Action<SystemNavigationProgress>? Progress;
public void Stop(){lock(gate)cancellation?.Cancel();}
public async Task RunAsync(SystemNavigationTarget target,ISystemFlightControlDriver driver,CancellationToken token=default)
{
target.Validate();CancellationTokenSource stop;
lock(gate)
{
if(cancellation is not null)throw new InvalidOperationException("星系内导航已在运行。");
stop=CancellationTokenSource.CreateLinkedTokenSource(token);cancellation=stop;
}
HandoffFailed=false;Failure=null;var phase=SystemNavigationPhase.Preparing;var state=NavigationRunState.Running;var terminal="星系内导航已停止。";string? reportedMessage=null;
void Report(SystemNavigationPhase next,string message,string? diagnostic=null)
{
if(State==NavigationRunState.Running&&phase==next&&reportedMessage==message)return;
phase=next;reportedMessage=message;State=NavigationRunState.Running;Progress?.Invoke(new(State,target,next,message){Diagnostic=diagnostic});
}
try
{
stop.Token.ThrowIfCancellationRequested();
var initial=player();var initialShip=initial.CurrentShipId;var initialCommander=initial.CommanderId;var initialSession=initial.LoadGameAt;
if(!initial.InGame||initial.CurrentShipId is null||initial.Navigation.CurrentSystemAddress!=target.SystemAddress||NavigationSafety.UnhealthyHull(initial))
throw new InvalidOperationException("请先进入健康、安全的飞船驾驶状态。");
if(NavigationSafety.HasExternalThreat(initial))throw new FlightThreatPauseException();
stop.Token.ThrowIfCancellationRequested();
if(TravelPreparation.AtDockedDestination(initial,target)){state=NavigationRunState.Completed;terminal=$"已停靠 {target.Name}";return;}
var capabilities=await driver.InspectAsync(stop.Token);
if(!capabilities.Supports(target,NavigationAssists.From(initial)))throw new NotSupportedException("当前游戏接口尚未完成此类星系内驾驶的验证。");
await driver.SelectAsync(target,stop.Token);
if(!TravelPreparation.AtDockedDestination(player(),target))await driver.PrepareAsync(stop.Token);
var policy=new SystemFlightNavigator();long previous=0;ulong entity=0;string? loadout=null;DateTimeOffset? missingSince=null;
DockingResetObservationPendingException? pendingReset=null;var landingObserved=false;var requestFailures=0;
Report(phase,"正在核对星系内目标和驾驶状态。");
while(true)
{
stop.Token.ThrowIfCancellationRequested();var current=player();
if(!current.InGame||current.CurrentShipId!=initialShip||current.CommanderId!=initialCommander||current.LoadGameAt!=initialSession
||NavigationSafety.UnhealthyHull(current))throw new InvalidOperationException("飞船、会话或安全状态改变,已停止星系内导航。");
if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
if(TravelPreparation.AtDockedDestination(current,target)){state=NavigationRunState.Completed;terminal=$"已停靠 {target.Name}";break;}
SystemFlightFrame frame;
try
{
frame=await driver.ReadSystemFlightAsync(stop.Token);
if(frame.Sequence<previous||frame.ObservedAt>now()||now()-frame.ObservedAt>TimeSpan.FromMilliseconds(300)||!frame.GeometryVerified)
throw new FlightTelemetryUnavailableException("等待新飞行数据。");
if(frame.GameSha256!=capabilities.GameSha256||string.IsNullOrWhiteSpace(frame.LoadoutFingerprint))throw new InvalidOperationException("飞行数据构建或装配身份不匹配。");
var journalNow=GameJournalClock.Now(current,now());
var confirmedTransition=frame.ReplacesShipEntity==entity&&frame.SceneTransitionAt is {} transition&&transition<=journalNow&&journalNow-transition<TimeSpan.FromSeconds(30)
&&transition>=initialSession&&(transition==current.Navigation.SupercruiseEnteredAt||transition==current.Navigation.SupercruiseExitedAt);
if(entity!=0&&frame.ShipEntity!=entity&&!confirmedTransition||loadout is not null&&frame.LoadoutFingerprint!=loadout)throw new InvalidOperationException("受控飞船或装配改变,已停止导航。");
if(frame.Sequence==previous){await Task.Delay(40,stop.Token);continue;}
entity=frame.ShipEntity;loadout=frame.LoadoutFingerprint;previous=frame.Sequence;
var decision=policy.Decide(frame,target,now());
if(pendingReset is not null)
{
landingObserved|=frame.DockingAssist==NativeAssistState.Engaged;
if(!landingObserved&&decision.Phase!=SystemNavigationPhase.Stopped&&!decision.Complete
&&DockingHandoffPolicy.RecoveryRequired(frame.TargetDistance,pendingReset.RequestedAt,now()))
decision=new(SystemNavigationPhase.Stopped,DockingHandoffPolicy.RecoveryMessage(frame.TargetDistance),null,null,SystemNavigationAction.ExitForFailedHandoff);
else if(!landingObserved&&!decision.Complete&&now()-pendingReset.RequestedAt>TimeSpan.FromSeconds(30))
throw new InvalidOperationException("自动降落回执和接管状态仍未确认,原请求未重发。",pendingReset);
if(decision.Phase!=SystemNavigationPhase.Stopped&&!decision.Complete)
decision=decision with{Phase=SystemNavigationPhase.NativeLanding,Message=landingObserved?"自动降落已接管,正在核对实际停靠。":"正在核对归零回执与自动降落状态,不重发请求。"};
}
Report(decision.Phase,decision.Message);
if(decision.Phase==SystemNavigationPhase.Stopped)
{
if(decision.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff))
{
HandoffFailed=true;Failure=pendingReset;
await driver.ApplySystemFlightAsync(decision,frame,target,frame.ObservedAt.AddMilliseconds(250),stop.Token);
}
state=NavigationRunState.Stopped;terminal=decision.Message;break;
}
// A lost acknowledgement is not permission for a second
// arm or another axis lease. Observe the actual AI/dock;
// the same distance/ten-second handoff guard remains above.
if(pendingReset is null)
{
await driver.ApplySystemFlightAsync(decision,frame,target,frame.ObservedAt.AddMilliseconds(250),stop.Token);
if(decision.Actions.HasFlag(SystemNavigationAction.RequestDocking))requestFailures=0;
}
missingSince=null;
if(decision.Complete){state=NavigationRunState.Completed;terminal=decision.Message;break;}
}
catch(DockingRequestNotAppliedException)
{
requestFailures++;
try{await driver.SuspendSystemFlightAsync();}catch(Exception release)when(NavigationRecovery.CanRetry(release,stop.Token)){}
if(requestFailures>=3)
{
HandoffFailed=true;
throw new DockingHandoffException("连续 3 次空管请求未被游戏执行,正在恢复原会话后重试。",target.MarketId!.Value);
}
Report(SystemNavigationPhase.AwaitingClearance,$"游戏尚未执行空管请求,正在核对后重试({requestFailures}/3)。");
}
catch(DockingResetObservationPendingException error)
{
if(error.MarketId!=target.MarketId)throw new InvalidDataException("未确认的归零请求属于其他市场。",error);
pendingReset=error;Report(SystemNavigationPhase.NativeLanding,error.Message);
}
catch(Exception error)when(NavigationRecovery.CanRetry(error,stop.Token))
{
missingSince??=now();
if(pendingReset is null)
try{await driver.SuspendSystemFlightAsync();}catch(Exception release)when(NavigationRecovery.CanRetry(release,stop.Token)){}
if(now()-missingSince>TimeSpan.FromSeconds(30))throw new InvalidOperationException("星系内飞行数据恢复未成功,已停止并交还控制。",error);
Report(SystemNavigationPhase.Waiting,"正在重新连接飞行数据并自动重试。",error.ToString());
}
await Task.Delay(80,stop.Token);
}
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){state=NavigationRunState.Stopped;phase=SystemNavigationPhase.Stopped;}
catch(Exception error){Failure=error;state=NavigationRunState.Failed;phase=SystemNavigationPhase.Stopped;terminal=error.Message;}
finally
{
try{await driver.ReleaseSystemFlightAsync();}
catch(Exception error){Failure=Failure is null?error:new AggregateException(Failure,error);state=NavigationRunState.Failed;terminal="已停止导航,输入释放未获确认:"+error.Message;}
lock(gate){cancellation=null;stop.Dispose();State=state;}
LastMessage=terminal;Progress?.Invoke(new(state,target,phase,terminal));
}
}
}