using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Presentation;
using XFEEDToolBox.Core.Galaxy;
using System.Text.Json;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
[ObservableProperty] public partial string NavigationDestination { get; set; }="";
[ObservableProperty] public partial double NavigationFuelReserve { get; set; }=.5;
[ObservableProperty] public partial GameRoutePlotOptions NavigationRouteOptions {get;set;}=new();
[ObservableProperty] public partial string NavigationRouteSummary { get; set; }="选择目标后,长按星图中的恒星,由游戏计算完整航路。";
[ObservableProperty] public partial string NavigationNext { get; set; }="—";
[ObservableProperty] public partial string NavigationFuel { get; set; }="—";
[ObservableProperty] public partial string NavigationFuelBudget { get; set; }="等待航线与完整飞船配置";
[ObservableProperty] public partial string NavigationShip { get; set; }="等待当前飞船配置";
[ObservableProperty] public partial string NavigationScoop { get; set; }="油勺状态未知";
[ObservableProperty] public partial string NavigationStatus { get; set; }="待命 · 设定目标后核对航行计划";
[ObservableProperty] public partial string NavigationStartHint { get; set; }="正在读取目标与飞船配置…";
[ObservableProperty] public partial InfoBarSeverity NavigationStatusSeverity { get; set; }=InfoBarSeverity.Informational;
[ObservableProperty] public partial string NavigationNativeStatus { get; set; }="安装游戏插件并进入飞行状态后,连接原生导航驱动。";
[ObservableProperty] public partial bool NavigationActive { get; set; }
[ObservableProperty] public partial bool NavigationPlanning { get; set; }
[ObservableProperty] public partial bool NavigationCancelling { get; set; }
[ObservableProperty] public partial long NavigationPlottedDestinationAddress { get; set; }
[ObservableProperty] public partial string NavigationPlottedDestination { get; set; }="";
public bool NavigationRouteBusy=>NavigationPlanning||NavigationCancelling;
public bool CanCancelPlottedRoute=>NavigationPlottedDestinationAddress>0&&!NavigationRouteBusy&&!RunActive&&!SessionBusy;
public string NavigationHoldHint=>NavigationPlottedDestinationAddress>0?"已同步游戏航线 · 长按目的地取消航线\n点击开始即可导航;Esc 或空白处取消选择":"点击选择 · 双击放大 · 长按恒星规划航线\nEsc 或点击空白处取消选择";
[ObservableProperty] public partial IReadOnlyList<NavWaypoint> NavigationMapRoute { get; set; }=[];
[ObservableProperty] public partial NavigationFuelOverlay NavigationFuelMap { get; set; }=NavigationFuelOverlay.Empty;
public bool NavigationTargetLocked=>NavigationActive||NavigationRouteBusy;
public bool NavigationTargetEditable=>!NavigationTargetLocked;
public event Action? NavigationPlanned;
[ObservableProperty] public partial bool NavigationRouteMatches { get; set; }
[ObservableProperty] public partial bool PluginNavigationReady { get; set; }
public LiveCollection<NavigationLegLiveRow,NavigationLegPreview,int> NavigationLegs { get; }=new(r=>r.Number,r=>new(r));
[ObservableProperty] public partial bool HasNoNavigationLegs {get;set;}=true;
private readonly NavigationItinerary navigationItinerary=new();
private string navigationItineraryOwner="",navigationItineraryDestination="";
private IReadOnlyList<NavWaypoint> navigationDisplayRoute=[];
private readonly GameRouteMirror gameRouteMirror=new();
private bool synchronizingGameRoute;
partial void OnNavigationStatusChanged(string value)=>RefreshNavigationLegPresentation();
partial void OnNavigationStatusSeverityChanged(InfoBarSeverity value)=>RefreshNavigationLegPresentation();
private void RefreshNavigationLegPresentation()
{
var state=NavigationActive?NavigationRunState.Running:NavigationStatusSeverity switch{InfoBarSeverity.Error=>NavigationRunState.Failed,InfoBarSeverity.Warning=>NavigationRunState.Stopped,_=>NavigationRunState.Idle};
NavigationLegs.Apply(navigationItinerary.Present(state,NavigationStatus,NavigationNext));HasNoNavigationLegs=NavigationLegs.Count==0;
}
private CancellationTokenSource? navigationStartStop;
internal GameRoutePlan? LastConfirmedGameRoute { get; private set; }
private DateTimeOffset nextNavigationUiUpdate;
private string lastNavigationUiState="";
private async Task ResolveNavigationDestinationAsync(CancellationToken token=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,token);
stop.Token.ThrowIfCancellationRequested();
var name=NavigationDestination.Trim();
if(name.Length is <1 or >128||galaxyCache.GetValueOrDefault(name)?.SystemAddress is >0)return;
if(CurrentPlayer.Navigation.Route.FirstOrDefault(w=>w.Name.Equals(name,StringComparison.OrdinalIgnoreCase)) is {} waypoint)
{galaxyCache[name]=new(waypoint.Name,waypoint.Position,waypoint.SystemAddress,waypoint.StarClass);return;}
NavigationStatus="正在读取目标星系的真实坐标与地址…";
var found=await FindGalacticSystemAsync(name,stop.Token);
if(!found.Name.Equals(name,StringComparison.OrdinalIgnoreCase)||found.SystemAddress is not >0)throw new InvalidOperationException("目标星系目录缺少有效的游戏地址。");
galaxyCache[found.Name]=found;PublishGalaxy();
await File.WriteAllTextAsync(Path.Combine(runtime.DataDirectory,"galaxy-cache.json"),JsonSerializer.Serialize(galaxyCache.Values,ToolboxSettings.JsonOptions),stop.Token);
}
private void InitializeNavigation()
{
NavigationDestination=runtime.Settings.NavigationDestination;NavigationFuelReserve=runtime.Settings.NavigationFuelReserve;
runtime.Navigator.Progress+=OnNavigationProgress;
runtime.FlightAutomation.Progress+=OnFlightAutomationProgress;
}
private void OnFlightAutomationProgress(FlightAutomationProgress progress)=>dispatcher.TryEnqueue(()=>
{
var running=progress.Stage is not (FlightAutomationStage.Completed or FlightAutomationStage.Stopped or FlightAutomationStage.Failed);
if(progress.Entry==FlightAutomationEntry.Galaxy)
{
NavigationActive=running;NavigationStatus=progress.Message;
NavigationStatusSeverity=progress.Stage==FlightAutomationStage.Failed?InfoBarSeverity.Error:progress.Stage==FlightAutomationStage.Completed?InfoBarSeverity.Success:InfoBarSeverity.Informational;
}
else if(progress.Entry==FlightAutomationEntry.CurrentSystem){SystemFlightActive=running;SystemFlightStatus=progress.Message;}
RefreshNavigationStartHint();StartNavigationCommand.NotifyCanExecuteChanged();StopNavigationCommand.NotifyCanExecuteChanged();
StartRemoteJourneyCommand.NotifyCanExecuteChanged();
StartSystemFlightCommand.NotifyCanExecuteChanged();RefreshTradePlanCommands();
StartGameCommand.NotifyCanExecuteChanged();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();
});
private void OnNavigationProgress(NavigationProgress progress)
{
if(runtime.FlightAutomation.ActiveEntry is {} entry&&entry!=FlightAutomationEntry.Galaxy)return;
var state=progress.Decision?.State??progress.Message;var now=DateTimeOffset.UtcNow;
if(progress.State==NavigationRunState.Running&&state==lastNavigationUiState&&now<nextNavigationUiUpdate)return;
lastNavigationUiState=state;nextNavigationUiUpdate=now.AddMilliseconds(200);
dispatcher.TryEnqueue(()=>
{
NavigationActive=runtime.FlightAutomation.ActiveEntry==FlightAutomationEntry.Galaxy||progress.State==NavigationRunState.Running;NavigationStatus=progress.Message;NavigationNext=progress.NextSystem;
NavigationStatusSeverity=progress.State switch{NavigationRunState.Failed=>InfoBarSeverity.Error,NavigationRunState.Completed=>InfoBarSeverity.Success,NavigationRunState.Stopped=>InfoBarSeverity.Warning,_=>InfoBarSeverity.Informational};
if(progress.State!=NavigationRunState.Running)LogNavigation("导航结果",progress.Message);
RefreshNavigationStartHint();
StartNavigationCommand.NotifyCanExecuteChanged();StopNavigationCommand.NotifyCanExecuteChanged();
StartGameCommand.NotifyCanExecuteChanged();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();
RefreshTradePlanCommands();
});
}
partial void OnNavigationDestinationChanged(string value){if(initialized&&!synchronizingGameRoute)RefreshNavigation(CurrentPlayer);}
partial void OnNavigationFuelReserveChanged(double value){if(initialized)RefreshNavigation(CurrentPlayer);}
private void RefreshNavigation(PlayerState player)
{
var mirrored=gameRouteMirror.Observe(player,NavigationRouteBusy);
var plotted=player.Navigation.Route.Count>=2&&player.Navigation.Route[^1].SystemAddress!=player.Navigation.CurrentSystemAddress?player.Navigation.Route[^1]:null;
var oldPlotted=NavigationPlottedDestinationAddress;
NavigationPlottedDestinationAddress=plotted?.SystemAddress??0;NavigationPlottedDestination=plotted?.Name??"";
OnPropertyChanged(nameof(CanCancelPlottedRoute));OnPropertyChanged(nameof(NavigationHoldHint));
if(mirrored.Changed)
{
if(mirrored.Cleared)
{
if(NavigationActive)StopForObservedRouteChange("游戏航线已清空,已释放原导航输入。");
navigationItinerary.Reset();navigationDisplayRoute=[];LastConfirmedGameRoute=null;
synchronizingGameRoute=true;try{NavigationDestination="";}finally{synchronizingGameRoute=false;}
NavigationStatus="游戏航线已取消。";NavigationStatusSeverity=InfoBarSeverity.Informational;
}
else if(mirrored.Destination is {} gameDestination)
{
if(mirrored.Replaced&&NavigationActive)StopForObservedRouteChange("游戏航线终点已改变,已释放原导航输入。");
synchronizingGameRoute=true;try{NavigationDestination=gameDestination.Name;}finally{synchronizingGameRoute=false;}
var system=new GalacticSystem(gameDestination.Name,gameDestination.Position,gameDestination.SystemAddress,gameDestination.StarClass,"游戏航线");
galaxyCache[system.Name]=system;
if(oldPlotted!=gameDestination.SystemAddress){GalaxySelection=system;NavigationPlanned?.Invoke();}
}
}
var destination=(plotted?.Name??NavigationDestination).Trim();
if(navigationItineraryOwner!=player.CommanderId||!navigationItineraryDestination.Equals(destination,StringComparison.OrdinalIgnoreCase))
{navigationItinerary.Reset();navigationDisplayRoute=[];navigationItineraryOwner=player.CommanderId;navigationItineraryDestination=destination;}
ObserveNativeSession(player);
var ship=player.Ships.FirstOrDefault(s=>s.ShipId==player.CurrentShipId);
NavigationShip=ship?.DisplayName??"等待当前飞船配置";
NavigationScoop=ship?.ConfigurationAt is null?"装配未读取":ship.ConfigurationChanged?"装配变化,等待新快照":NavigationPreviewBuilder.HasScoop(ship)?"已安装并启用油勺":"未检测到启用的油勺";
var current=player.Navigation.FuelAt>player.StatusAt?player.Navigation.MainFuel:player.Status?.FuelMain??player.Navigation.MainFuel;
NavigationFuel=current is {} main?$"{main:F2} t":"等待燃油状态";
try
{
var preview=NavigationPreviewBuilder.Build(player,plotted?.Name??NavigationDestination,NavigationFuelReserve);
navigationItinerary.UpdateRoute(preview.Legs);NavigationNext=preview.NextSystem;NavigationFuelBudget=preview.Message;
navigationDisplayRoute=player.Navigation.Route;
NavigationRouteSummary=preview.RemainingJumps==0?"已抵达目标星系":$"{player.SystemName} → {plotted?.Name??NavigationDestination.Trim()} · 剩余 {preview.RemainingJumps} 跳";
NavigationRouteMatches=preview.RemainingJumps>0;
}
catch(InvalidOperationException e)
{
NavigationRouteMatches=false;NavigationRouteSummary=e.Message;NavigationNext=player.Navigation.TargetSystem.Length>0?player.Navigation.TargetSystem:"—";
NavigationFuelBudget="等待游戏生成航路后,按当前装配、货重和燃油估算。";
if(player.Navigation.Route.Count==0)NavigationRouteSummary="游戏尚未生成航路,请长按星图中的目标恒星。";
}
if(player.Navigation.CurrentSystemAddress is >0&&destination.Length>0
&&player.SystemName.Equals(destination,StringComparison.OrdinalIgnoreCase))
{
NavigationRouteMatches=false;NavigationNext="已抵达";
NavigationRouteSummary=$"已抵达 {player.SystemName}";NavigationFuelBudget="当前已在目标星系。";
if(!NavigationActive){NavigationStatus=NavigationRouteSummary;NavigationStatusSeverity=InfoBarSeverity.Success;}
}
navigationItinerary.ConfirmPosition(player.Navigation.CurrentSystemAddress??0);RefreshNavigationLegPresentation();
NavigationMapRoute=StableSequence(NavigationMapRoute,navigationDisplayRoute);
var fuelMap=NavigationRouteMatches?NavigationFuelOverlay.Build(player,NavigationFuelReserve):NavigationFuelOverlay.Empty;
if(!NavigationFuelMap.DashedLegs.SequenceEqual(fuelMap.DashedLegs)||!NavigationFuelMap.SupplySites.SequenceEqual(fuelMap.SupplySites)||NavigationFuelMap.RecommendedSupply!=fuelMap.RecommendedSupply)NavigationFuelMap=fuelMap;
RefreshNavigationStartHint();StartNavigationCommand.NotifyCanExecuteChanged();
RefreshDashboardRoute();
}
private bool CanPlanNavigation()=>!NavigationActive&&!NavigationRouteBusy&&!RunActive&&!SessionBusy;
[RelayCommand(CanExecute=nameof(CanPlanNavigation))] private async Task PlanNavigationAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
if(!CanPlanNavigation())return;
var destination=NavigationDestination.Trim();var reserve=NavigationFuelReserve;
void RequireSameTarget()
{
token.ThrowIfCancellationRequested();
if(!destination.Equals(NavigationDestination.Trim(),StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("规划目标已改变,请长按所选目标重新规划。");
}
NavigationPlanning=true;
navigationItinerary.Reset();navigationDisplayRoute=[];RefreshNavigationLegPresentation();
LastConfirmedGameRoute=null;
try
{
if(NavigationActive)throw new InvalidOperationException("请先停止导航再更改目标。");
if(NavigationDestination.Trim().Length is <1 or >128)throw new InvalidOperationException("请输入有效的目标星系名称。");
if(!double.IsFinite(NavigationFuelReserve)||NavigationFuelReserve<0||NavigationFuelReserve>100)throw new InvalidOperationException("燃油余量需在 0–100 t 之间。");
await ResolveNavigationDestinationAsync(token);
RequireSameTarget();
var target=galaxyCache.GetValueOrDefault(destination);
if(target?.SystemAddress is not >0||!target.Name.Equals(destination,StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("目标星系缺少匹配的游戏地址。");
var routeOptions=NavigationRouteOptions;routeOptions.Validate(CurrentPlayer.CargoCapacity);
await runtime.SaveSettingsAsync(runtime.Settings with{NavigationDestination=destination,NavigationFuelReserve=reserve,NavigationRouteOptions=routeOptions});
await ReadPluginStatusAsync();
RequireSameTarget();
if(runtime.FlightDriver is not IGameRoutePlanningDriver planner)throw new InvalidOperationException("请连接支持游戏航线规划的原生插件。");
NavigationStatus="正在将目标提交给游戏航线规划器…";NavigationStatusSeverity=InfoBarSeverity.Informational;
LogNavigation("规划航线",$"提交游戏规划:{target.Name}");
var result=await new GameRoutePlanner(()=>runtime.Player).PlanAsync(new(target.SystemAddress.Value,target.Name,target.Position,target.StarClass),planner,message=>NavigationStatus=message,token,routeOptions);
RequireSameTarget();
LastConfirmedGameRoute=result;
RefreshNavigation(CurrentPlayer);
NavigationStatus=$"游戏航线已确认,共 {result.Route.Count-1} 跳;下一跳 {result.Next.Name} 已锁定。";
NavigationStatusSeverity=InfoBarSeverity.Success;LogNavigation("航线确认",NavigationStatus);NavigationPlanned?.Invoke();
}
catch(OperationCanceledException){NavigationStatus="航线规划已取消。";NavigationStatusSeverity=InfoBarSeverity.Informational;}
catch(Exception e){NavigationStatus=e.Message;NavigationStatusSeverity=InfoBarSeverity.Error;}
finally{NavigationPlanning=false;PlanNavigationCommand.NotifyCanExecuteChanged();RefreshNavigation(CurrentPlayer);}
}
internal Task ChooseNavigationTargetAsync(GalacticSystem target)
{
if(NavigationTargetLocked)return Task.CompletedTask;
galaxyCache[target.Name]=target;GalaxySelection=target;NavigationDestination=target.Name;
NavigationStatus=$"已选择 {target.Name},长按星图中的恒星规划航线。";NavigationStatusSeverity=InfoBarSeverity.Informational;
return Task.CompletedTask;
}
partial void OnNavigationPlanningChanged(bool value)
{
RefreshRelogAvailability();
OnPropertyChanged(nameof(NavigationRouteBusy));OnPropertyChanged(nameof(CanCancelPlottedRoute));
OnPropertyChanged(nameof(NavigationTargetLocked));RefreshNavigationStartHint();StartNavigationCommand.NotifyCanExecuteChanged();PlanNavigationCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(NavigationTargetEditable));
}
partial void OnNavigationCancellingChanged(bool value)
{
RefreshRelogAvailability();
OnPropertyChanged(nameof(NavigationRouteBusy));OnPropertyChanged(nameof(NavigationTargetLocked));OnPropertyChanged(nameof(NavigationTargetEditable));OnPropertyChanged(nameof(CanCancelPlottedRoute));
StartNavigationCommand.NotifyCanExecuteChanged();PlanNavigationCommand.NotifyCanExecuteChanged();
}
internal bool IsPlottedDestination(GalacticSystem target)=>NavigationPlottedDestinationAddress>0&&target.SystemAddress==NavigationPlottedDestinationAddress;
[RelayCommand] private async Task CancelPlottedRouteAsync(CancellationToken token=default)
{
if(!CanCancelPlottedRoute)return;
var expected=NavigationPlottedDestinationAddress;NavigationCancelling=true;
try
{
if(NavigationActive){StopNavigation();var until=DateTimeOffset.UtcNow.AddSeconds(5);while(NavigationActive&&DateTimeOffset.UtcNow<until)await Task.Delay(50,token);if(NavigationActive)throw new InvalidOperationException("正在停止导航,请稍后再次长按取消航线。");}
if(runtime.FlightDriver is not IGameRouteCancellationDriver driver)throw new InvalidOperationException("请等待原生导航连接完成后取消游戏航线。");
await new GameRouteCancellation(()=>runtime.Player).CancelAsync(expected,driver,message=>NavigationStatus=message,token);
NavigationStatus="游戏航线已取消。";NavigationStatusSeverity=InfoBarSeverity.Success;
}
catch(OperationCanceledException){NavigationStatus="未收到游戏的完整取消确认,正在继续同步实际航线。";NavigationStatusSeverity=InfoBarSeverity.Warning;}
catch(Exception error){NavigationStatus=error.Message;NavigationStatusSeverity=InfoBarSeverity.Error;}
finally{NavigationCancelling=false;RefreshNavigation(CurrentPlayer);}
}
[RelayCommand] private async Task CheckNavigationAsync()
{
if(NavigationActive)return;
try{await ValidateNavigationStartAsync();}
catch(Exception e){NavigationStatus=e.Message;NavigationStatusSeverity=InfoBarSeverity.Error;}
}
private string NavigationStartBlocker()
{
if(RunActive)return "请先停止正在执行的贸易路线。";
if(SessionBusy)return "正在处理游戏会话,请等待完成。";
if(NavigationDestination.Trim().Length is <1 or >128)return "请输入有效的目标星系名称。";
if(!double.IsFinite(NavigationFuelReserve)||NavigationFuelReserve<0||NavigationFuelReserve>100)return "燃油余量需在 0–100 t 之间。";
var player=CurrentPlayer;
if(ProtectionBlocksTesting||NavigationSafety.UnhealthyHull(player))return "飞船安全保护正在等待稳定健康确认,请先处理实际受损或失效状态。";
if(!player.InGame)return "请先进入游戏中的飞船驾驶舱。";
if(player.Docked&&!NavigationAssists.From(player).AdvancedDocking)return "自动离站需要已启用的高级自动起降模块。";
if(runtime.FlightAutomation.IsActive&&runtime.FlightAutomation.ActiveEntry!=FlightAutomationEntry.Galaxy)return "已有自动驾驶正在运行,请先停止当前航程。";
if(NavigationSafety.MustStop(player,(runtime.FlightDriver as NativeFlightControlDriver)?.LastObservation?.Frame,DateTimeOffset.UtcNow))return "检测到战斗、拦截或未确认的危险状态,请先解除危险。";
if(!NavigationRouteMatches)return NavigationRouteSummary;
if(!PluginNavigationReady||runtime.FlightDriver is null)return NavigationNativeStatus;
return "";
}
private void RefreshNavigationStartHint()
{
var blocker=NavigationStartBlocker();
NavigationStartHint=NavigationActive?"自动导航正在核对或执行,可随时停止。":NavigationPlanning?"正在规划航线…":blocker.Length>0?blocker:CurrentPlayer.Docked?"已就绪 · 开始后自动离站并接续航路。":"已就绪 · 点击开始即可自动飞行。";
}
// Shared by the start button and the read-only diagnostic; this method sends no flight inputs.
internal async Task ValidateNavigationStartAsync(CancellationToken token=default)
{
NavigationStatus="正在核对目标、飞船与原生导航驱动…";NavigationStatusSeverity=InfoBarSeverity.Informational;
if(CurrentPlayer.Navigation.Route.Count>=2&&CurrentPlayer.Navigation.Route[^1] is {} gameDestination)NavigationDestination=gameDestination.Name;
if(NavigationDestination.Trim().Length is <1 or >128)throw new InvalidOperationException("请输入有效的目标星系名称。");
if(!double.IsFinite(NavigationFuelReserve)||NavigationFuelReserve<0||NavigationFuelReserve>100)throw new InvalidOperationException("燃油余量需在 0–100 t 之间。");
await ResolveNavigationDestinationAsync(token);
await ReadPluginStatusAsync();
token.ThrowIfCancellationRequested();
RefreshNavigation(CurrentPlayer);
var driver=runtime.FlightDriver??throw new InvalidOperationException(NavigationNativeStatus);
await driver.BeginNavigationSessionAsync(token);
void Recovering()
{
if(!CurrentPlayer.InGame||NavigationSafety.UnhealthyHull(CurrentPlayer)||NavigationSafety.HasExternalThreat(CurrentPlayer))
throw new InvalidOperationException("游戏会话或飞船安全状态已改变,已停止导航连接恢复。");
NavigationStatus="正在恢复导航连接,原航线保留并自动继续…";
}
await NavigationRecovery.PreflightWithRecoveryAsync(driver,Recovering,token);
if(CurrentPlayer.Docked&&driver is NativeFlightControlDriver dockedDriver)
{
// Docked ships need a live, identified cockpit observation, not a
// target vector for a flight phase that has not started yet.
await NavigationRecovery.ReadWithRecoveryAsync(dockedDriver.ReadShipTelemetryAsync,Recovering,token);
}
else await NavigationRecovery.ReadWithRecoveryAsync(async readToken=>
{
try{return await driver.ReadAsync(readToken);}
catch(FlightTelemetryUnavailableException error)when(error.Recovery==FlightRecovery.RefreshDriver)
{
await NavigationRecovery.PreflightWithRecoveryAsync(driver,Recovering,readToken);
return await driver.ReadAsync(readToken);
}
},Recovering,token);
var blocker=NavigationStartBlocker();
if(blocker.Length>0)throw new InvalidOperationException(blocker);
// Validate current reference geometry before the start button can report
// readiness. This performs read/describe only, with no target or inputs.
NavigationStatus="航线已就绪,等待启动。";NavigationStatusSeverity=InfoBarSeverity.Success;
}
private void LogNavigation(string kind,string message)
{
runtime.Log(kind,message);
try
{
var path=Path.Combine(runtime.DataDirectory,"navigation.log");
if(File.Exists(path)&&new FileInfo(path).Length>1024*1024)File.Move(path,path+".previous",true);
File.AppendAllText(path,$"{DateTimeOffset.Now:o}\t{kind}\t{message.Replace('\r',' ').Replace('\n',' ')}{Environment.NewLine}");
}
catch(Exception e)when(e is IOException or UnauthorizedAccessException){runtime.Log("导航日志",e.Message);}
}
private bool CanStartNavigation()=>!NavigationActive&&!NavigationRouteBusy&&!RunActive&&!SessionBusy&&!runtime.FlightAutomation.IsActive;
[RelayCommand(CanExecute=nameof(CanStartNavigation))] private async Task StartNavigationAsync()
{
if(!CanStartNavigation())return;
using var startStop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);navigationStartStop=startStop;
NavigationActive=true;RefreshNavigationStartHint();StopNavigationCommand.NotifyCanExecuteChanged();
StartGameCommand.NotifyCanExecuteChanged();RefreshTradePlanCommands();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();
LogNavigation("启动导航",$"收到启动请求:{NavigationDestination.Trim()}");
try
{
await ValidateNavigationStartAsync(startStop.Token);
if(runtime.Settings.NavigationDestination!=NavigationDestination.Trim()||runtime.Settings.NavigationFuelReserve!=NavigationFuelReserve)
await runtime.SaveSettingsAsync(runtime.Settings with{NavigationDestination=NavigationDestination.Trim(),NavigationFuelReserve=NavigationFuelReserve});
var destination=NavigationDestination;var driver=runtime.FlightDriver as NativeFlightControlDriver??throw new InvalidOperationException("原生导航驱动尚未就绪。");
var departure=new NativeFlightJourneyDriver(driver.ProcessId,Core.Native.NativeSystemLocationReader.GameSha256,()=>runtime.Player,
XFEEDToolBox.NativeSupport.NativeSupportPackage.Directory,()=>NavigationFuelReserve,
cancellation=>EnsureNativeFlightProtectionAsync(driver.ProcessId,Core.Native.NativeSystemLocationReader.GameSha256,cancellation));
startStop.Token.ThrowIfCancellationRequested();
var result=await Task.Run(()=>runtime.FlightAutomation.NavigateGalaxyAsync(destination,driver,departure,startStop.Token),startStop.Token);
if(result.Failure is {} failure)throw failure;
}
catch(OperationCanceledException)when(lifetime.IsCancellationRequested){}
catch(OperationCanceledException)when(startStop.IsCancellationRequested){NavigationStatus="自动导航已停止。";NavigationStatusSeverity=InfoBarSeverity.Warning;LogNavigation("导航取消",NavigationStatus);}
catch(Exception e){NavigationStatus=e.Message;NavigationStatusSeverity=InfoBarSeverity.Error;LogNavigation("导航未启动",e.Message);}
finally{navigationStartStop=null;NavigationActive=false;RefreshNavigationStartHint();StartGameCommand.NotifyCanExecuteChanged();StartNavigationCommand.NotifyCanExecuteChanged();StopNavigationCommand.NotifyCanExecuteChanged();RefreshTradePlanCommands();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();}
}
private bool CanStopNavigation()=>NavigationActive;
private int navigationUserStopVersion;
internal int NavigationUserStopVersion=>Volatile.Read(ref navigationUserStopVersion);
private void StopOwnedNavigation(){navigationStartStop?.Cancel();runtime.FlightAutomation.Stop();runtime.Navigator.Stop();}
private void StopForObservedRouteChange(string reason)
{
runtime.FlightAutomation.PauseForMonitoring(new InvalidOperationException(reason));
runtime.Navigator.Stop();
}
internal void StopNavigationForScoAcceptance()
{
if(!Environment.GetCommandLineArgs().Contains("--accept-sco"))throw new InvalidOperationException("SCO QA stop requires its explicit launch entry.");
StopOwnedNavigation();
}
[RelayCommand(CanExecute=nameof(CanStopNavigation))] private void StopNavigation(){Interlocked.Increment(ref navigationUserStopVersion);StopOwnedNavigation();}
partial void OnPluginNavigationReadyChanged(bool value){RefreshNavigationStartHint();StartNavigationCommand.NotifyCanExecuteChanged();}
}
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Presentation;
using XFEEDToolBox.Core.Galaxy;
using System.Text.Json;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
[ObservableProperty] public partial string NavigationDestination { get; set; }="";
[ObservableProperty] public partial double NavigationFuelReserve { get; set; }=.5;
[ObservableProperty] public partial GameRoutePlotOptions NavigationRouteOptions {get;set;}=new();
[ObservableProperty] public partial string NavigationRouteSummary { get; set; }="选择目标后,长按星图中的恒星,由游戏计算完整航路。";
[ObservableProperty] public partial string NavigationNext { get; set; }="—";
[ObservableProperty] public partial string NavigationFuel { get; set; }="—";
[ObservableProperty] public partial string NavigationFuelBudget { get; set; }="等待航线与完整飞船配置";
[ObservableProperty] public partial string NavigationShip { get; set; }="等待当前飞船配置";
[ObservableProperty] public partial string NavigationScoop { get; set; }="油勺状态未知";
[ObservableProperty] public partial string NavigationStatus { get; set; }="待命 · 设定目标后核对航行计划";
[ObservableProperty] public partial string NavigationStartHint { get; set; }="正在读取目标与飞船配置…";
[ObservableProperty] public partial InfoBarSeverity NavigationStatusSeverity { get; set; }=InfoBarSeverity.Informational;
[ObservableProperty] public partial string NavigationNativeStatus { get; set; }="安装游戏插件并进入飞行状态后,连接原生导航驱动。";
[ObservableProperty] public partial bool NavigationActive { get; set; }
[ObservableProperty] public partial bool NavigationPlanning { get; set; }
[ObservableProperty] public partial bool NavigationCancelling { get; set; }
[ObservableProperty] public partial long NavigationPlottedDestinationAddress { get; set; }
[ObservableProperty] public partial string NavigationPlottedDestination { get; set; }="";
public bool NavigationRouteBusy=>NavigationPlanning||NavigationCancelling;
public bool CanCancelPlottedRoute=>NavigationPlottedDestinationAddress>0&&!NavigationRouteBusy&&!RunActive&&!SessionBusy;
public string NavigationHoldHint=>NavigationPlottedDestinationAddress>0?"已同步游戏航线 · 长按目的地取消航线\n点击开始即可导航;Esc 或空白处取消选择":"点击选择 · 双击放大 · 长按恒星规划航线\nEsc 或点击空白处取消选择";
[ObservableProperty] public partial IReadOnlyList<NavWaypoint> NavigationMapRoute { get; set; }=[];
[ObservableProperty] public partial NavigationFuelOverlay NavigationFuelMap { get; set; }=NavigationFuelOverlay.Empty;
public bool NavigationTargetLocked=>NavigationActive||NavigationRouteBusy;
public bool NavigationTargetEditable=>!NavigationTargetLocked;
public event Action? NavigationPlanned;
[ObservableProperty] public partial bool NavigationRouteMatches { get; set; }
[ObservableProperty] public partial bool PluginNavigationReady { get; set; }
public LiveCollection<NavigationLegLiveRow,NavigationLegPreview,int> NavigationLegs { get; }=new(r=>r.Number,r=>new(r));
[ObservableProperty] public partial bool HasNoNavigationLegs {get;set;}=true;
private readonly NavigationItinerary navigationItinerary=new();
private string navigationItineraryOwner="",navigationItineraryDestination="";
private IReadOnlyList<NavWaypoint> navigationDisplayRoute=[];
private readonly GameRouteMirror gameRouteMirror=new();
private bool synchronizingGameRoute;
partial void OnNavigationStatusChanged(string value)=>RefreshNavigationLegPresentation();
partial void OnNavigationStatusSeverityChanged(InfoBarSeverity value)=>RefreshNavigationLegPresentation();
private void RefreshNavigationLegPresentation()
{
var state=NavigationActive?NavigationRunState.Running:NavigationStatusSeverity switch{InfoBarSeverity.Error=>NavigationRunState.Failed,InfoBarSeverity.Warning=>NavigationRunState.Stopped,_=>NavigationRunState.Idle};
NavigationLegs.Apply(navigationItinerary.Present(state,NavigationStatus,NavigationNext));HasNoNavigationLegs=NavigationLegs.Count==0;
}
private CancellationTokenSource? navigationStartStop;
internal GameRoutePlan? LastConfirmedGameRoute { get; private set; }
private DateTimeOffset nextNavigationUiUpdate;
private string lastNavigationUiState="";
private async Task ResolveNavigationDestinationAsync(CancellationToken token=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,token);
stop.Token.ThrowIfCancellationRequested();
var name=NavigationDestination.Trim();
if(name.Length is <1 or >128||galaxyCache.GetValueOrDefault(name)?.SystemAddress is >0)return;
if(CurrentPlayer.Navigation.Route.FirstOrDefault(w=>w.Name.Equals(name,StringComparison.OrdinalIgnoreCase)) is {} waypoint)
{galaxyCache[name]=new(waypoint.Name,waypoint.Position,waypoint.SystemAddress,waypoint.StarClass);return;}
NavigationStatus="正在读取目标星系的真实坐标与地址…";
var found=await FindGalacticSystemAsync(name,stop.Token);
if(!found.Name.Equals(name,StringComparison.OrdinalIgnoreCase)||found.SystemAddress is not >0)throw new InvalidOperationException("目标星系目录缺少有效的游戏地址。");
galaxyCache[found.Name]=found;PublishGalaxy();
await File.WriteAllTextAsync(Path.Combine(runtime.DataDirectory,"galaxy-cache.json"),JsonSerializer.Serialize(galaxyCache.Values,ToolboxSettings.JsonOptions),stop.Token);
}
private void InitializeNavigation()
{
NavigationDestination=runtime.Settings.NavigationDestination;NavigationFuelReserve=runtime.Settings.NavigationFuelReserve;
runtime.Navigator.Progress+=OnNavigationProgress;
runtime.FlightAutomation.Progress+=OnFlightAutomationProgress;
}
private void OnFlightAutomationProgress(FlightAutomationProgress progress)=>dispatcher.TryEnqueue(()=>
{
var running=progress.Stage is not (FlightAutomationStage.Completed or FlightAutomationStage.Stopped or FlightAutomationStage.Failed);
if(progress.Entry==FlightAutomationEntry.Galaxy)
{
NavigationActive=running;NavigationStatus=progress.Message;
NavigationStatusSeverity=progress.Stage==FlightAutomationStage.Failed?InfoBarSeverity.Error:progress.Stage==FlightAutomationStage.Completed?InfoBarSeverity.Success:InfoBarSeverity.Informational;
}
else if(progress.Entry==FlightAutomationEntry.CurrentSystem){SystemFlightActive=running;SystemFlightStatus=progress.Message;}
RefreshNavigationStartHint();StartNavigationCommand.NotifyCanExecuteChanged();StopNavigationCommand.NotifyCanExecuteChanged();
StartRemoteJourneyCommand.NotifyCanExecuteChanged();
StartSystemFlightCommand.NotifyCanExecuteChanged();RefreshTradePlanCommands();
StartGameCommand.NotifyCanExecuteChanged();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();
});
private void OnNavigationProgress(NavigationProgress progress)
{
if(runtime.FlightAutomation.ActiveEntry is {} entry&&entry!=FlightAutomationEntry.Galaxy)return;
var state=progress.Decision?.State??progress.Message;var now=DateTimeOffset.UtcNow;
if(progress.State==NavigationRunState.Running&&state==lastNavigationUiState&&now<nextNavigationUiUpdate)return;
lastNavigationUiState=state;nextNavigationUiUpdate=now.AddMilliseconds(200);
dispatcher.TryEnqueue(()=>
{
NavigationActive=runtime.FlightAutomation.ActiveEntry==FlightAutomationEntry.Galaxy||progress.State==NavigationRunState.Running;NavigationStatus=progress.Message;NavigationNext=progress.NextSystem;
NavigationStatusSeverity=progress.State switch{NavigationRunState.Failed=>InfoBarSeverity.Error,NavigationRunState.Completed=>InfoBarSeverity.Success,NavigationRunState.Stopped=>InfoBarSeverity.Warning,_=>InfoBarSeverity.Informational};
if(progress.State!=NavigationRunState.Running)LogNavigation("导航结果",progress.Message);
RefreshNavigationStartHint();
StartNavigationCommand.NotifyCanExecuteChanged();StopNavigationCommand.NotifyCanExecuteChanged();
StartGameCommand.NotifyCanExecuteChanged();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();
RefreshTradePlanCommands();
});
}
partial void OnNavigationDestinationChanged(string value){if(initialized&&!synchronizingGameRoute)RefreshNavigation(CurrentPlayer);}
partial void OnNavigationFuelReserveChanged(double value){if(initialized)RefreshNavigation(CurrentPlayer);}
private void RefreshNavigation(PlayerState player)
{
var mirrored=gameRouteMirror.Observe(player,NavigationRouteBusy);
var plotted=player.Navigation.Route.Count>=2&&player.Navigation.Route[^1].SystemAddress!=player.Navigation.CurrentSystemAddress?player.Navigation.Route[^1]:null;
var oldPlotted=NavigationPlottedDestinationAddress;
NavigationPlottedDestinationAddress=plotted?.SystemAddress??0;NavigationPlottedDestination=plotted?.Name??"";
OnPropertyChanged(nameof(CanCancelPlottedRoute));OnPropertyChanged(nameof(NavigationHoldHint));
if(mirrored.Changed)
{
if(mirrored.Cleared)
{
if(NavigationActive)StopForObservedRouteChange("游戏航线已清空,已释放原导航输入。");
navigationItinerary.Reset();navigationDisplayRoute=[];LastConfirmedGameRoute=null;
synchronizingGameRoute=true;try{NavigationDestination="";}finally{synchronizingGameRoute=false;}
NavigationStatus="游戏航线已取消。";NavigationStatusSeverity=InfoBarSeverity.Informational;
}
else if(mirrored.Destination is {} gameDestination)
{
if(mirrored.Replaced&&NavigationActive)StopForObservedRouteChange("游戏航线终点已改变,已释放原导航输入。");
synchronizingGameRoute=true;try{NavigationDestination=gameDestination.Name;}finally{synchronizingGameRoute=false;}
var system=new GalacticSystem(gameDestination.Name,gameDestination.Position,gameDestination.SystemAddress,gameDestination.StarClass,"游戏航线");
galaxyCache[system.Name]=system;
if(oldPlotted!=gameDestination.SystemAddress){GalaxySelection=system;NavigationPlanned?.Invoke();}
}
}
var destination=(plotted?.Name??NavigationDestination).Trim();
if(navigationItineraryOwner!=player.CommanderId||!navigationItineraryDestination.Equals(destination,StringComparison.OrdinalIgnoreCase))
{navigationItinerary.Reset();navigationDisplayRoute=[];navigationItineraryOwner=player.CommanderId;navigationItineraryDestination=destination;}
ObserveNativeSession(player);
var ship=player.Ships.FirstOrDefault(s=>s.ShipId==player.CurrentShipId);
NavigationShip=ship?.DisplayName??"等待当前飞船配置";
NavigationScoop=ship?.ConfigurationAt is null?"装配未读取":ship.ConfigurationChanged?"装配变化,等待新快照":NavigationPreviewBuilder.HasScoop(ship)?"已安装并启用油勺":"未检测到启用的油勺";
var current=player.Navigation.FuelAt>player.StatusAt?player.Navigation.MainFuel:player.Status?.FuelMain??player.Navigation.MainFuel;
NavigationFuel=current is {} main?$"{main:F2} t":"等待燃油状态";
try
{
var preview=NavigationPreviewBuilder.Build(player,plotted?.Name??NavigationDestination,NavigationFuelReserve);
navigationItinerary.UpdateRoute(preview.Legs);NavigationNext=preview.NextSystem;NavigationFuelBudget=preview.Message;
navigationDisplayRoute=player.Navigation.Route;
NavigationRouteSummary=preview.RemainingJumps==0?"已抵达目标星系":$"{player.SystemName} → {plotted?.Name??NavigationDestination.Trim()} · 剩余 {preview.RemainingJumps} 跳";
NavigationRouteMatches=preview.RemainingJumps>0;
}
catch(InvalidOperationException e)
{
NavigationRouteMatches=false;NavigationRouteSummary=e.Message;NavigationNext=player.Navigation.TargetSystem.Length>0?player.Navigation.TargetSystem:"—";
NavigationFuelBudget="等待游戏生成航路后,按当前装配、货重和燃油估算。";
if(player.Navigation.Route.Count==0)NavigationRouteSummary="游戏尚未生成航路,请长按星图中的目标恒星。";
}
if(player.Navigation.CurrentSystemAddress is >0&&destination.Length>0
&&player.SystemName.Equals(destination,StringComparison.OrdinalIgnoreCase))
{
NavigationRouteMatches=false;NavigationNext="已抵达";
NavigationRouteSummary=$"已抵达 {player.SystemName}";NavigationFuelBudget="当前已在目标星系。";
if(!NavigationActive){NavigationStatus=NavigationRouteSummary;NavigationStatusSeverity=InfoBarSeverity.Success;}
}
navigationItinerary.ConfirmPosition(player.Navigation.CurrentSystemAddress??0);RefreshNavigationLegPresentation();
NavigationMapRoute=StableSequence(NavigationMapRoute,navigationDisplayRoute);
var fuelMap=NavigationRouteMatches?NavigationFuelOverlay.Build(player,NavigationFuelReserve):NavigationFuelOverlay.Empty;
if(!NavigationFuelMap.DashedLegs.SequenceEqual(fuelMap.DashedLegs)||!NavigationFuelMap.SupplySites.SequenceEqual(fuelMap.SupplySites)||NavigationFuelMap.RecommendedSupply!=fuelMap.RecommendedSupply)NavigationFuelMap=fuelMap;
RefreshNavigationStartHint();StartNavigationCommand.NotifyCanExecuteChanged();
RefreshDashboardRoute();
}
private bool CanPlanNavigation()=>!NavigationActive&&!NavigationRouteBusy&&!RunActive&&!SessionBusy;
[RelayCommand(CanExecute=nameof(CanPlanNavigation))] private async Task PlanNavigationAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
if(!CanPlanNavigation())return;
var destination=NavigationDestination.Trim();var reserve=NavigationFuelReserve;
void RequireSameTarget()
{
token.ThrowIfCancellationRequested();
if(!destination.Equals(NavigationDestination.Trim(),StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("规划目标已改变,请长按所选目标重新规划。");
}
NavigationPlanning=true;
navigationItinerary.Reset();navigationDisplayRoute=[];RefreshNavigationLegPresentation();
LastConfirmedGameRoute=null;
try
{
if(NavigationActive)throw new InvalidOperationException("请先停止导航再更改目标。");
if(NavigationDestination.Trim().Length is <1 or >128)throw new InvalidOperationException("请输入有效的目标星系名称。");
if(!double.IsFinite(NavigationFuelReserve)||NavigationFuelReserve<0||NavigationFuelReserve>100)throw new InvalidOperationException("燃油余量需在 0–100 t 之间。");
await ResolveNavigationDestinationAsync(token);
RequireSameTarget();
var target=galaxyCache.GetValueOrDefault(destination);
if(target?.SystemAddress is not >0||!target.Name.Equals(destination,StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("目标星系缺少匹配的游戏地址。");
var routeOptions=NavigationRouteOptions;routeOptions.Validate(CurrentPlayer.CargoCapacity);
await runtime.SaveSettingsAsync(runtime.Settings with{NavigationDestination=destination,NavigationFuelReserve=reserve,NavigationRouteOptions=routeOptions});
await ReadPluginStatusAsync();
RequireSameTarget();
if(runtime.FlightDriver is not IGameRoutePlanningDriver planner)throw new InvalidOperationException("请连接支持游戏航线规划的原生插件。");
NavigationStatus="正在将目标提交给游戏航线规划器…";NavigationStatusSeverity=InfoBarSeverity.Informational;
LogNavigation("规划航线",$"提交游戏规划:{target.Name}");
var result=await new GameRoutePlanner(()=>runtime.Player).PlanAsync(new(target.SystemAddress.Value,target.Name,target.Position,target.StarClass),planner,message=>NavigationStatus=message,token,routeOptions);
RequireSameTarget();
LastConfirmedGameRoute=result;
RefreshNavigation(CurrentPlayer);
NavigationStatus=$"游戏航线已确认,共 {result.Route.Count-1} 跳;下一跳 {result.Next.Name} 已锁定。";
NavigationStatusSeverity=InfoBarSeverity.Success;LogNavigation("航线确认",NavigationStatus);NavigationPlanned?.Invoke();
}
catch(OperationCanceledException){NavigationStatus="航线规划已取消。";NavigationStatusSeverity=InfoBarSeverity.Informational;}
catch(Exception e){NavigationStatus=e.Message;NavigationStatusSeverity=InfoBarSeverity.Error;}
finally{NavigationPlanning=false;PlanNavigationCommand.NotifyCanExecuteChanged();RefreshNavigation(CurrentPlayer);}
}
internal Task ChooseNavigationTargetAsync(GalacticSystem target)
{
if(NavigationTargetLocked)return Task.CompletedTask;
galaxyCache[target.Name]=target;GalaxySelection=target;NavigationDestination=target.Name;
NavigationStatus=$"已选择 {target.Name},长按星图中的恒星规划航线。";NavigationStatusSeverity=InfoBarSeverity.Informational;
return Task.CompletedTask;
}
partial void OnNavigationPlanningChanged(bool value)
{
RefreshRelogAvailability();
OnPropertyChanged(nameof(NavigationRouteBusy));OnPropertyChanged(nameof(CanCancelPlottedRoute));
OnPropertyChanged(nameof(NavigationTargetLocked));RefreshNavigationStartHint();StartNavigationCommand.NotifyCanExecuteChanged();PlanNavigationCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(NavigationTargetEditable));
}
partial void OnNavigationCancellingChanged(bool value)
{
RefreshRelogAvailability();
OnPropertyChanged(nameof(NavigationRouteBusy));OnPropertyChanged(nameof(NavigationTargetLocked));OnPropertyChanged(nameof(NavigationTargetEditable));OnPropertyChanged(nameof(CanCancelPlottedRoute));
StartNavigationCommand.NotifyCanExecuteChanged();PlanNavigationCommand.NotifyCanExecuteChanged();
}
internal bool IsPlottedDestination(GalacticSystem target)=>NavigationPlottedDestinationAddress>0&&target.SystemAddress==NavigationPlottedDestinationAddress;
[RelayCommand] private async Task CancelPlottedRouteAsync(CancellationToken token=default)
{
if(!CanCancelPlottedRoute)return;
var expected=NavigationPlottedDestinationAddress;NavigationCancelling=true;
try
{
if(NavigationActive){StopNavigation();var until=DateTimeOffset.UtcNow.AddSeconds(5);while(NavigationActive&&DateTimeOffset.UtcNow<until)await Task.Delay(50,token);if(NavigationActive)throw new InvalidOperationException("正在停止导航,请稍后再次长按取消航线。");}
if(runtime.FlightDriver is not IGameRouteCancellationDriver driver)throw new InvalidOperationException("请等待原生导航连接完成后取消游戏航线。");
await new GameRouteCancellation(()=>runtime.Player).CancelAsync(expected,driver,message=>NavigationStatus=message,token);
NavigationStatus="游戏航线已取消。";NavigationStatusSeverity=InfoBarSeverity.Success;
}
catch(OperationCanceledException){NavigationStatus="未收到游戏的完整取消确认,正在继续同步实际航线。";NavigationStatusSeverity=InfoBarSeverity.Warning;}
catch(Exception error){NavigationStatus=error.Message;NavigationStatusSeverity=InfoBarSeverity.Error;}
finally{NavigationCancelling=false;RefreshNavigation(CurrentPlayer);}
}
[RelayCommand] private async Task CheckNavigationAsync()
{
if(NavigationActive)return;
try{await ValidateNavigationStartAsync();}
catch(Exception e){NavigationStatus=e.Message;NavigationStatusSeverity=InfoBarSeverity.Error;}
}
private string NavigationStartBlocker()
{
if(RunActive)return "请先停止正在执行的贸易路线。";
if(SessionBusy)return "正在处理游戏会话,请等待完成。";
if(NavigationDestination.Trim().Length is <1 or >128)return "请输入有效的目标星系名称。";
if(!double.IsFinite(NavigationFuelReserve)||NavigationFuelReserve<0||NavigationFuelReserve>100)return "燃油余量需在 0–100 t 之间。";
var player=CurrentPlayer;
if(ProtectionBlocksTesting||NavigationSafety.UnhealthyHull(player))return "飞船安全保护正在等待稳定健康确认,请先处理实际受损或失效状态。";
if(!player.InGame)return "请先进入游戏中的飞船驾驶舱。";
if(player.Docked&&!NavigationAssists.From(player).AdvancedDocking)return "自动离站需要已启用的高级自动起降模块。";
if(runtime.FlightAutomation.IsActive&&runtime.FlightAutomation.ActiveEntry!=FlightAutomationEntry.Galaxy)return "已有自动驾驶正在运行,请先停止当前航程。";
if(NavigationSafety.MustStop(player,(runtime.FlightDriver as NativeFlightControlDriver)?.LastObservation?.Frame,DateTimeOffset.UtcNow))return "检测到战斗、拦截或未确认的危险状态,请先解除危险。";
if(!NavigationRouteMatches)return NavigationRouteSummary;
if(!PluginNavigationReady||runtime.FlightDriver is null)return NavigationNativeStatus;
return "";
}
private void RefreshNavigationStartHint()
{
var blocker=NavigationStartBlocker();
NavigationStartHint=NavigationActive?"自动导航正在核对或执行,可随时停止。":NavigationPlanning?"正在规划航线…":blocker.Length>0?blocker:CurrentPlayer.Docked?"已就绪 · 开始后自动离站并接续航路。":"已就绪 · 点击开始即可自动飞行。";
}
// Shared by the start button and the read-only diagnostic; this method sends no flight inputs.
internal async Task ValidateNavigationStartAsync(CancellationToken token=default)
{
NavigationStatus="正在核对目标、飞船与原生导航驱动…";NavigationStatusSeverity=InfoBarSeverity.Informational;
if(CurrentPlayer.Navigation.Route.Count>=2&&CurrentPlayer.Navigation.Route[^1] is {} gameDestination)NavigationDestination=gameDestination.Name;
if(NavigationDestination.Trim().Length is <1 or >128)throw new InvalidOperationException("请输入有效的目标星系名称。");
if(!double.IsFinite(NavigationFuelReserve)||NavigationFuelReserve<0||NavigationFuelReserve>100)throw new InvalidOperationException("燃油余量需在 0–100 t 之间。");
await ResolveNavigationDestinationAsync(token);
await ReadPluginStatusAsync();
token.ThrowIfCancellationRequested();
RefreshNavigation(CurrentPlayer);
var driver=runtime.FlightDriver??throw new InvalidOperationException(NavigationNativeStatus);
await driver.BeginNavigationSessionAsync(token);
void Recovering()
{
if(!CurrentPlayer.InGame||NavigationSafety.UnhealthyHull(CurrentPlayer)||NavigationSafety.HasExternalThreat(CurrentPlayer))
throw new InvalidOperationException("游戏会话或飞船安全状态已改变,已停止导航连接恢复。");
NavigationStatus="正在恢复导航连接,原航线保留并自动继续…";
}
await NavigationRecovery.PreflightWithRecoveryAsync(driver,Recovering,token);
if(CurrentPlayer.Docked&&driver is NativeFlightControlDriver dockedDriver)
{
// Docked ships need a live, identified cockpit observation, not a
// target vector for a flight phase that has not started yet.
await NavigationRecovery.ReadWithRecoveryAsync(dockedDriver.ReadShipTelemetryAsync,Recovering,token);
}
else await NavigationRecovery.ReadWithRecoveryAsync(async readToken=>
{
try{return await driver.ReadAsync(readToken);}
catch(FlightTelemetryUnavailableException error)when(error.Recovery==FlightRecovery.RefreshDriver)
{
await NavigationRecovery.PreflightWithRecoveryAsync(driver,Recovering,readToken);
return await driver.ReadAsync(readToken);
}
},Recovering,token);
var blocker=NavigationStartBlocker();
if(blocker.Length>0)throw new InvalidOperationException(blocker);
// Validate current reference geometry before the start button can report
// readiness. This performs read/describe only, with no target or inputs.
NavigationStatus="航线已就绪,等待启动。";NavigationStatusSeverity=InfoBarSeverity.Success;
}
private void LogNavigation(string kind,string message)
{
runtime.Log(kind,message);
try
{
var path=Path.Combine(runtime.DataDirectory,"navigation.log");
if(File.Exists(path)&&new FileInfo(path).Length>1024*1024)File.Move(path,path+".previous",true);
File.AppendAllText(path,$"{DateTimeOffset.Now:o}\t{kind}\t{message.Replace('\r',' ').Replace('\n',' ')}{Environment.NewLine}");
}
catch(Exception e)when(e is IOException or UnauthorizedAccessException){runtime.Log("导航日志",e.Message);}
}
private bool CanStartNavigation()=>!NavigationActive&&!NavigationRouteBusy&&!RunActive&&!SessionBusy&&!runtime.FlightAutomation.IsActive;
[RelayCommand(CanExecute=nameof(CanStartNavigation))] private async Task StartNavigationAsync()
{
if(!CanStartNavigation())return;
using var startStop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);navigationStartStop=startStop;
NavigationActive=true;RefreshNavigationStartHint();StopNavigationCommand.NotifyCanExecuteChanged();
StartGameCommand.NotifyCanExecuteChanged();RefreshTradePlanCommands();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();
LogNavigation("启动导航",$"收到启动请求:{NavigationDestination.Trim()}");
try
{
await ValidateNavigationStartAsync(startStop.Token);
if(runtime.Settings.NavigationDestination!=NavigationDestination.Trim()||runtime.Settings.NavigationFuelReserve!=NavigationFuelReserve)
await runtime.SaveSettingsAsync(runtime.Settings with{NavigationDestination=NavigationDestination.Trim(),NavigationFuelReserve=NavigationFuelReserve});
var destination=NavigationDestination;var driver=runtime.FlightDriver as NativeFlightControlDriver??throw new InvalidOperationException("原生导航驱动尚未就绪。");
var departure=new NativeFlightJourneyDriver(driver.ProcessId,Core.Native.NativeSystemLocationReader.GameSha256,()=>runtime.Player,
XFEEDToolBox.NativeSupport.NativeSupportPackage.Directory,()=>NavigationFuelReserve,
cancellation=>EnsureNativeFlightProtectionAsync(driver.ProcessId,Core.Native.NativeSystemLocationReader.GameSha256,cancellation));
startStop.Token.ThrowIfCancellationRequested();
var result=await Task.Run(()=>runtime.FlightAutomation.NavigateGalaxyAsync(destination,driver,departure,startStop.Token),startStop.Token);
if(result.Failure is {} failure)throw failure;
}
catch(OperationCanceledException)when(lifetime.IsCancellationRequested){}
catch(OperationCanceledException)when(startStop.IsCancellationRequested){NavigationStatus="自动导航已停止。";NavigationStatusSeverity=InfoBarSeverity.Warning;LogNavigation("导航取消",NavigationStatus);}
catch(Exception e){NavigationStatus=e.Message;NavigationStatusSeverity=InfoBarSeverity.Error;LogNavigation("导航未启动",e.Message);}
finally{navigationStartStop=null;NavigationActive=false;RefreshNavigationStartHint();StartGameCommand.NotifyCanExecuteChanged();StartNavigationCommand.NotifyCanExecuteChanged();StopNavigationCommand.NotifyCanExecuteChanged();RefreshTradePlanCommands();RelogGameCommand.NotifyCanExecuteChanged();RestartGameCommand.NotifyCanExecuteChanged();}
}
private bool CanStopNavigation()=>NavigationActive;
private int navigationUserStopVersion;
internal int NavigationUserStopVersion=>Volatile.Read(ref navigationUserStopVersion);
private void StopOwnedNavigation(){navigationStartStop?.Cancel();runtime.FlightAutomation.Stop();runtime.Navigator.Stop();}
private void StopForObservedRouteChange(string reason)
{
runtime.FlightAutomation.PauseForMonitoring(new InvalidOperationException(reason));
runtime.Navigator.Stop();
}
internal void StopNavigationForScoAcceptance()
{
if(!Environment.GetCommandLineArgs().Contains("--accept-sco"))throw new InvalidOperationException("SCO QA stop requires its explicit launch entry.");
StopOwnedNavigation();
}
[RelayCommand(CanExecute=nameof(CanStopNavigation))] private void StopNavigation(){Interlocked.Increment(ref navigationUserStopVersion);StopOwnedNavigation();}
partial void OnPluginNavigationReadyChanged(bool value){RefreshNavigationStartHint();StartNavigationCommand.NotifyCanExecuteChanged();}
}