using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Planning;
using XFEEDToolBox.Core.Presentation;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.NativeSupport;
using XFEEDToolBox.Views;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
public LiveCollection<SystemLocationLiveRow,SystemLocation,string> RemoteMapLocations{get;}=new(x=>x.Identity,x=>new(x));
[ObservableProperty] public partial string RemoteMapTitle{get;set;}="本地星系地图";
[ObservableProperty] public partial string RemoteMapStatus{get;set;}="选择星系后载入地点目录。";
[ObservableProperty] public partial string RemoteMapSearch{get;set;}="";
[ObservableProperty] public partial bool RemoteMapLoading{get;set;}
[ObservableProperty] public partial SystemLocationLiveRow? RemoteMapSelection{get;set;}
private SystemMapCatalogue? remoteMap;
private CancellationTokenSource? remoteMapLoadStop;
internal CurrentSystemSnapshot? RemoteMapSnapshot=>remoteMap?.Preview;
internal event Action? RemoteMapChanged;
public string RemoteMapSelectionName=>RemoteMapSelection?.Name??"选择空间站";
public string RemoteMapSelectionDetails=>RemoteMapSelection?.Record is not {} row?"在 3D 场景或左侧目录中选择目的站。":
row.KindText+" · 距主恒星 "+row.DistanceText;
public string RemoteMapActionHint=>RemoteMapSelection?.Record is not {} selected?"选择目的站后可一次启动跃迁与星系内导航。":
remoteMap?.Stations.TryGetValue(selected.Identity,out var station)!=true?"此地点没有已确认的停靠身份,仅供浏览。":
station!.IsPlanetary?"地表进近尚未完成验证,暂不能自动停靠。":
$"停机坪 {station.PadDisplay} · 开始后自动离站、跃迁并停靠;抵达后重新核对游戏中的站点。";
partial void OnRemoteMapSearchChanged(string value)=>FilterRemoteMap();
partial void OnRemoteMapSelectionChanged(SystemLocationLiveRow? value)
{
OnPropertyChanged(nameof(RemoteMapSelectionName));OnPropertyChanged(nameof(RemoteMapSelectionDetails));OnPropertyChanged(nameof(RemoteMapActionHint));
StartRemoteJourneyCommand.NotifyCanExecuteChanged();
}
private void FilterRemoteMap()
{
if(remoteMap is not {} map)return;var selected=RemoteMapSelection?.Record.Identity;
RemoteMapLocations.ApplyLocations(new SystemLocationFilter(Search:RemoteMapSearch).Apply(map.Preview));
RemoteMapSelection=RemoteMapLocations.FirstOrDefault(row=>row.Record.Identity==selected);RemoteMapChanged?.Invoke();
}
[RelayCommand] private async Task OpenSelectedSystemMapAsync()
{
if(GalaxySelection is not {} selected)return;
await OpenSystemMapAsync(selected);
}
internal async Task OpenSystemMapAsync(GalacticSystem selected)
{
remoteMapLoadStop?.Cancel();remoteMapLoadStop?.Dispose();remoteMapLoadStop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);
var stop=remoteMapLoadStop;RemoteMapTitle=selected.Name;RemoteMapSearch="";RemoteMapSelection=null;remoteMap=null;
RemoteMapLocations.Apply([]);RemoteMapChanged?.Invoke();RemoteMapLoading=true;RemoteMapStatus="正在载入所选星系的天体与空间站目录。";
AppShellPage.Current?.Navigate(typeof(SystemMapCataloguePage));
try
{
var map=await new SystemMapCatalogueClient(galaxyHttp).ReadAsync(selected,stop.Token);stop.Token.ThrowIfCancellationRequested();
if(!ReferenceEquals(stop,remoteMapLoadStop))return;
remoteMap=map;FilterRemoteMap();RemoteMapStatus=$"{SystemMapCatalogue.PreviewDescription} · {map.Stations.Count} 个空间站";
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){}
catch(Exception error){if(ReferenceEquals(stop,remoteMapLoadStop))RemoteMapStatus="本地目录载入未完成:"+error.Message;}
finally{if(ReferenceEquals(stop,remoteMapLoadStop))RemoteMapLoading=false;}
}
private bool CanStartRemoteJourney()=>!OfflinePreview&&!DemoMode&&!NavigationActive&&!SystemFlightActive&&!RunActive&&!SessionBusy&&RemoteMapSelection is {} row
&&remoteMap?.Stations.TryGetValue(row.Record.Identity,out var station)==true&&!station.IsPlanetary;
[RelayCommand(CanExecute=nameof(CanStartRemoteJourney))] private async Task StartRemoteJourneyAsync()
{
if(!CanStartRemoteJourney()||RemoteMapSelection is not {} selected||remoteMap?.Stations.TryGetValue(selected.Record.Identity,out var station)!=true)return;
await RunStationJourneyAsync(station!);
}
internal void SetSystemMapCatalogueForProbe(SystemMapCatalogue catalogue)
{
if(!OfflinePreview||!Environment.GetCommandLineArgs().Contains("--system-atlas-only"))throw new InvalidOperationException("仅允许隔离的目录界面核验。");
remoteMap=catalogue;RemoteMapTitle=catalogue.System.Name;RemoteMapStatus=SystemMapCatalogue.PreviewDescription;FilterRemoteMap();
}
internal async Task RunStationJourneyAsync(Station station)
{
if(OfflinePreview||DemoMode||!sessionAutomationAllowed)throw new InvalidOperationException("界面预览不能启动驾驶。");
if(runtime.FlightAutomation.IsActive||RunActive||SystemFlightActive||NavigationActive||SessionBusy)throw new InvalidOperationException("请先停止当前自动驾驶或任务。");
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);navigationStartStop=stop;NavigationActive=true;
StopNavigationCommand.NotifyCanExecuteChanged();
try
{
if(station.MarketId<=0||string.IsNullOrWhiteSpace(station.Name)||string.IsNullOrWhiteSpace(station.SystemName))throw new InvalidDataException("目的站身份不完整。");
if(station.IsPlanetary)throw new NotSupportedException("地表进近尚未完成验证,暂不能自动停靠。");
ShipPadPolicy.RequireFits(station,runtime.Player.Ship);
shipProtection?.RequireHealthyShip();
NavigationDestination=station.SystemName;LogNavigation("开始完整航程",station.DisplayName);
for(var recoveries=0;;recoveries++)
{
await ReadEnvironmentAsync(stop.Token);stop.Token.ThrowIfCancellationRequested();
await ReadPluginStatusAsync();
var requiresEntry=gameEnvironment?.ProcessId is null;
if(gameEnvironment?.ProcessId is {} existingPid)
{
using var existing=System.Diagnostics.Process.GetProcessById(existingPid);
var started=new DateTimeOffset(existing.StartTime.ToUniversalTime());
var menu=await new NativeRelogChannel(existingPid,NativeRelogChannel.SupportedGameSha256,entryOnly:true).ReadEntryStatusAsync(stop.Token);
var navigation=await new NavigationPipeClient(existingPid).CallAsync(new{op="describe"},stop.Token);
stop.Token.ThrowIfCancellationRequested();
requiresEntry=NativeSessionStartupReadiness.JourneyEntryRequired(runtime.Player,menu,navigation,existingPid,
gameEnvironment.ImageSha256??"",started,DateTimeOffset.UtcNow);
}
if(requiresEntry)
{
var launch=new Services.WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory){ResumeExistingCargo=true};
launch.Progress+=text=>dispatcher.TryEnqueue(()=>{NavigationStatus=text;LogNavigation("航程会话恢复",text);});
await Task.Run(()=>launch.UpgradeAndEnterAsync(stop.Token),stop.Token);
await ReadEnvironmentAsync(stop.Token);stop.Token.ThrowIfCancellationRequested();
await ReadPluginStatusAsync();
}
var pid=gameEnvironment?.ProcessId??throw new InvalidOperationException("当前游戏实例尚未连接。");
var hash=gameEnvironment?.ImageSha256??"";
var adapter=new NativeFlightJourneyDriver(pid,hash,()=>runtime.Player,NativeSupportPackage.Directory,()=>NavigationFuelReserve,
token=>EnsureNativeFlightProtectionAsync(pid,hash,token),cruiseDiagnostics:runtime.RecordScoDiagnostic);
adapter.Status+=text=>dispatcher.TryEnqueue(()=>NavigationStatus=text);
var result=await Task.Run(()=>runtime.FlightAutomation.NavigateStationAsync(station,adapter,stop.Token),stop.Token);
stop.Token.ThrowIfCancellationRequested();
if(result.Failure is StationDepartureRecoveryException&&recoveries<2)
{
await Task.Run(()=>Services.WindowsFlightRecovery.RecoverAsync(pid,hash,()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,
ReleaseProtectionForControlledExitAsync,text=>dispatcher.TryEnqueue(()=>{NavigationStatus=text;LogNavigation("离场恢复",text);}),stop.Token),stop.Token);
continue;
}
if(result.Failure is {} sceneError&&SavingSceneRecoveryRequiredException.Present(sceneError)&&recoveries<2)
{
var bootstrap=new Services.WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory){ResumeExistingCargo=true};
bootstrap.Progress+=text=>dispatcher.TryEnqueue(()=>{NavigationStatus=text;LogNavigation("场景恢复",text);});
await Task.Run(()=>bootstrap.UpgradeAndEnterAsync(stop.Token),stop.Token);
continue;
}
if(result.Failure is {} failure)throw failure;
NavigationStatus=result.Message??(result.State==NavigationRunState.Completed?"已停靠 "+station.DisplayName:"自动驾驶已停止。");
LogNavigation("完整航程结果",NavigationStatus);
break;
}
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){NavigationStatus="自动驾驶已停止。";}
catch(Exception error){NavigationStatus=error.Message;NavigationStatusSeverity=InfoBarSeverity.Error;runtime.Log("完整航程未完成",error.ToString());}
finally{navigationStartStop=null;NavigationActive=false;StopNavigationCommand.NotifyCanExecuteChanged();StartRemoteJourneyCommand.NotifyCanExecuteChanged();}
}
}
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Planning;
using XFEEDToolBox.Core.Presentation;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.NativeSupport;
using XFEEDToolBox.Views;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
public LiveCollection<SystemLocationLiveRow,SystemLocation,string> RemoteMapLocations{get;}=new(x=>x.Identity,x=>new(x));
[ObservableProperty] public partial string RemoteMapTitle{get;set;}="本地星系地图";
[ObservableProperty] public partial string RemoteMapStatus{get;set;}="选择星系后载入地点目录。";
[ObservableProperty] public partial string RemoteMapSearch{get;set;}="";
[ObservableProperty] public partial bool RemoteMapLoading{get;set;}
[ObservableProperty] public partial SystemLocationLiveRow? RemoteMapSelection{get;set;}
private SystemMapCatalogue? remoteMap;
private CancellationTokenSource? remoteMapLoadStop;
internal CurrentSystemSnapshot? RemoteMapSnapshot=>remoteMap?.Preview;
internal event Action? RemoteMapChanged;
public string RemoteMapSelectionName=>RemoteMapSelection?.Name??"选择空间站";
public string RemoteMapSelectionDetails=>RemoteMapSelection?.Record is not {} row?"在 3D 场景或左侧目录中选择目的站。":
row.KindText+" · 距主恒星 "+row.DistanceText;
public string RemoteMapActionHint=>RemoteMapSelection?.Record is not {} selected?"选择目的站后可一次启动跃迁与星系内导航。":
remoteMap?.Stations.TryGetValue(selected.Identity,out var station)!=true?"此地点没有已确认的停靠身份,仅供浏览。":
station!.IsPlanetary?"地表进近尚未完成验证,暂不能自动停靠。":
$"停机坪 {station.PadDisplay} · 开始后自动离站、跃迁并停靠;抵达后重新核对游戏中的站点。";
partial void OnRemoteMapSearchChanged(string value)=>FilterRemoteMap();
partial void OnRemoteMapSelectionChanged(SystemLocationLiveRow? value)
{
OnPropertyChanged(nameof(RemoteMapSelectionName));OnPropertyChanged(nameof(RemoteMapSelectionDetails));OnPropertyChanged(nameof(RemoteMapActionHint));
StartRemoteJourneyCommand.NotifyCanExecuteChanged();
}
private void FilterRemoteMap()
{
if(remoteMap is not {} map)return;var selected=RemoteMapSelection?.Record.Identity;
RemoteMapLocations.ApplyLocations(new SystemLocationFilter(Search:RemoteMapSearch).Apply(map.Preview));
RemoteMapSelection=RemoteMapLocations.FirstOrDefault(row=>row.Record.Identity==selected);RemoteMapChanged?.Invoke();
}
[RelayCommand] private async Task OpenSelectedSystemMapAsync()
{
if(GalaxySelection is not {} selected)return;
await OpenSystemMapAsync(selected);
}
internal async Task OpenSystemMapAsync(GalacticSystem selected)
{
remoteMapLoadStop?.Cancel();remoteMapLoadStop?.Dispose();remoteMapLoadStop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);
var stop=remoteMapLoadStop;RemoteMapTitle=selected.Name;RemoteMapSearch="";RemoteMapSelection=null;remoteMap=null;
RemoteMapLocations.Apply([]);RemoteMapChanged?.Invoke();RemoteMapLoading=true;RemoteMapStatus="正在载入所选星系的天体与空间站目录。";
AppShellPage.Current?.Navigate(typeof(SystemMapCataloguePage));
try
{
var map=await new SystemMapCatalogueClient(galaxyHttp).ReadAsync(selected,stop.Token);stop.Token.ThrowIfCancellationRequested();
if(!ReferenceEquals(stop,remoteMapLoadStop))return;
remoteMap=map;FilterRemoteMap();RemoteMapStatus=$"{SystemMapCatalogue.PreviewDescription} · {map.Stations.Count} 个空间站";
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){}
catch(Exception error){if(ReferenceEquals(stop,remoteMapLoadStop))RemoteMapStatus="本地目录载入未完成:"+error.Message;}
finally{if(ReferenceEquals(stop,remoteMapLoadStop))RemoteMapLoading=false;}
}
private bool CanStartRemoteJourney()=>!OfflinePreview&&!DemoMode&&!NavigationActive&&!SystemFlightActive&&!RunActive&&!SessionBusy&&RemoteMapSelection is {} row
&&remoteMap?.Stations.TryGetValue(row.Record.Identity,out var station)==true&&!station.IsPlanetary;
[RelayCommand(CanExecute=nameof(CanStartRemoteJourney))] private async Task StartRemoteJourneyAsync()
{
if(!CanStartRemoteJourney()||RemoteMapSelection is not {} selected||remoteMap?.Stations.TryGetValue(selected.Record.Identity,out var station)!=true)return;
await RunStationJourneyAsync(station!);
}
internal void SetSystemMapCatalogueForProbe(SystemMapCatalogue catalogue)
{
if(!OfflinePreview||!Environment.GetCommandLineArgs().Contains("--system-atlas-only"))throw new InvalidOperationException("仅允许隔离的目录界面核验。");
remoteMap=catalogue;RemoteMapTitle=catalogue.System.Name;RemoteMapStatus=SystemMapCatalogue.PreviewDescription;FilterRemoteMap();
}
internal async Task RunStationJourneyAsync(Station station)
{
if(OfflinePreview||DemoMode||!sessionAutomationAllowed)throw new InvalidOperationException("界面预览不能启动驾驶。");
if(runtime.FlightAutomation.IsActive||RunActive||SystemFlightActive||NavigationActive||SessionBusy)throw new InvalidOperationException("请先停止当前自动驾驶或任务。");
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);navigationStartStop=stop;NavigationActive=true;
StopNavigationCommand.NotifyCanExecuteChanged();
try
{
if(station.MarketId<=0||string.IsNullOrWhiteSpace(station.Name)||string.IsNullOrWhiteSpace(station.SystemName))throw new InvalidDataException("目的站身份不完整。");
if(station.IsPlanetary)throw new NotSupportedException("地表进近尚未完成验证,暂不能自动停靠。");
ShipPadPolicy.RequireFits(station,runtime.Player.Ship);
shipProtection?.RequireHealthyShip();
NavigationDestination=station.SystemName;LogNavigation("开始完整航程",station.DisplayName);
for(var recoveries=0;;recoveries++)
{
await ReadEnvironmentAsync(stop.Token);stop.Token.ThrowIfCancellationRequested();
await ReadPluginStatusAsync();
var requiresEntry=gameEnvironment?.ProcessId is null;
if(gameEnvironment?.ProcessId is {} existingPid)
{
using var existing=System.Diagnostics.Process.GetProcessById(existingPid);
var started=new DateTimeOffset(existing.StartTime.ToUniversalTime());
var menu=await new NativeRelogChannel(existingPid,NativeRelogChannel.SupportedGameSha256,entryOnly:true).ReadEntryStatusAsync(stop.Token);
var navigation=await new NavigationPipeClient(existingPid).CallAsync(new{op="describe"},stop.Token);
stop.Token.ThrowIfCancellationRequested();
requiresEntry=NativeSessionStartupReadiness.JourneyEntryRequired(runtime.Player,menu,navigation,existingPid,
gameEnvironment.ImageSha256??"",started,DateTimeOffset.UtcNow);
}
if(requiresEntry)
{
var launch=new Services.WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory){ResumeExistingCargo=true};
launch.Progress+=text=>dispatcher.TryEnqueue(()=>{NavigationStatus=text;LogNavigation("航程会话恢复",text);});
await Task.Run(()=>launch.UpgradeAndEnterAsync(stop.Token),stop.Token);
await ReadEnvironmentAsync(stop.Token);stop.Token.ThrowIfCancellationRequested();
await ReadPluginStatusAsync();
}
var pid=gameEnvironment?.ProcessId??throw new InvalidOperationException("当前游戏实例尚未连接。");
var hash=gameEnvironment?.ImageSha256??"";
var adapter=new NativeFlightJourneyDriver(pid,hash,()=>runtime.Player,NativeSupportPackage.Directory,()=>NavigationFuelReserve,
token=>EnsureNativeFlightProtectionAsync(pid,hash,token),cruiseDiagnostics:runtime.RecordScoDiagnostic);
adapter.Status+=text=>dispatcher.TryEnqueue(()=>NavigationStatus=text);
var result=await Task.Run(()=>runtime.FlightAutomation.NavigateStationAsync(station,adapter,stop.Token),stop.Token);
stop.Token.ThrowIfCancellationRequested();
if(result.Failure is StationDepartureRecoveryException&&recoveries<2)
{
await Task.Run(()=>Services.WindowsFlightRecovery.RecoverAsync(pid,hash,()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,
ReleaseProtectionForControlledExitAsync,text=>dispatcher.TryEnqueue(()=>{NavigationStatus=text;LogNavigation("离场恢复",text);}),stop.Token),stop.Token);
continue;
}
if(result.Failure is {} sceneError&&SavingSceneRecoveryRequiredException.Present(sceneError)&&recoveries<2)
{
var bootstrap=new Services.WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory){ResumeExistingCargo=true};
bootstrap.Progress+=text=>dispatcher.TryEnqueue(()=>{NavigationStatus=text;LogNavigation("场景恢复",text);});
await Task.Run(()=>bootstrap.UpgradeAndEnterAsync(stop.Token),stop.Token);
continue;
}
if(result.Failure is {} failure)throw failure;
NavigationStatus=result.Message??(result.State==NavigationRunState.Completed?"已停靠 "+station.DisplayName:"自动驾驶已停止。");
LogNavigation("完整航程结果",NavigationStatus);
break;
}
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){NavigationStatus="自动驾驶已停止。";}
catch(Exception error){NavigationStatus=error.Message;NavigationStatusSeverity=InfoBarSeverity.Error;runtime.Log("完整航程未完成",error.ToString());}
finally{navigationStartStop=null;NavigationActive=false;StopNavigationCommand.NotifyCanExecuteChanged();StartRemoteJourneyCommand.NotifyCanExecuteChanged();}
}
}