using XFEEDToolBox.NativeSupport;
using XFEEDToolBox.Core.Execution;
using System.Diagnostics;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
private readonly GamePluginManager pluginManager=new(NativeSupportPackage.Directory);
[ObservableProperty] public partial string PluginGameExecutable { get; set; } = "";
[ObservableProperty] public partial string PluginStatusText { get; set; } = "插件未安装,自动贸易不可启动。";
[ObservableProperty] public partial bool PluginBusy { get; set; }
[ObservableProperty] public partial bool PluginInstalled { get; set; }
[ObservableProperty] public partial bool PluginTradeReady { get; set; }
private DateTimeOffset? nativeSessionSeen;
private bool nativeStatusChecking;
private Task? nativeStatusRead;
private DateTimeOffset nativeStatusRetryAt;
private bool nativeStatusConnected;
private void ObserveNativeSession(PlayerState player)
{
if(OfflinePreview)return;
if(!player.InGame){PluginNavigationReady=false;PluginRelogReady=false;nativeSessionSeen=null;nativeStatusConnected=false;runtime.UnbindFlightDriver();return;}
if(nativeStatusChecking||player.LoadGameAt is null||nativeSessionSeen==player.LoadGameAt&&(nativeStatusConnected||DateTimeOffset.UtcNow<nativeStatusRetryAt))return;
if(NavigationActive||PluginBusy)return;
nativeSessionSeen=player.LoadGameAt;
nativeStatusRetryAt=DateTimeOffset.UtcNow.AddSeconds(3);
_=ReadPluginStatusAsync();
}
private void InitializePluginSettings()
{ PluginGameExecutable=runtime.Settings.PluginGameExecutable;if(!OfflinePreview&&PluginGameExecutable.Length==0)PluginGameExecutable=FindSteamGame(); }
private static string FindSteamGame()
{
var steam=Microsoft.Win32.Registry.GetValue(@"HKEY_CURRENT_USER\Software\Valve\Steam","SteamPath",null) as string;if(steam is null)return "";
var libraries=new List<string>{steam};var vdf=Path.Combine(steam,"steamapps","libraryfolders.vdf");
if(File.Exists(vdf))foreach(Match match in Regex.Matches(File.ReadAllText(vdf),"\"path\"\\s+\"([^\"]+)\""))libraries.Add(match.Groups[1].Value.Replace("\\\\","\\"));
return libraries.Select(l=>Path.Combine(l,"steamapps","common","Elite Dangerous","Products","elite-dangerous-odyssey-64","EliteDangerous64.exe")).FirstOrDefault(File.Exists)??"";
}
[RelayCommand] private async Task ReadPluginStatusAsync()
{
if(OfflinePreview)return;
if(Environment.GetCommandLineArgs().Contains("--smoke-interface"))return;
if(nativeStatusRead is {} pending){await pending;return;}
var task=ReadPluginStatusCoreAsync();nativeStatusRead=task;
try{await task;}finally{nativeStatusRead=null;}
}
private async Task ReadPluginStatusCoreAsync()
{
nativeStatusChecking=true;
nativeStatusConnected=false;RelogUnavailableReason="正在连接当前游戏的重进接口";RefreshRelogAvailability();
PluginTradeReady=false;PluginNavigationReady=false;PluginRelogReady=false;
NavigationNativeStatus="正在检查游戏插件与导航连接…";
var session=CurrentPlayer.LoadGameAt;var inGame=CurrentPlayer.InGame;
try
{
var connection=await ReadPluginConnectionAsync(PluginGameExecutable,gameEnvironment?.Executable??"",inGame,lifetime.Token);
pluginQueryApplyThread=Environment.CurrentManagedThreadId;
PluginGameExecutable=connection.Executable;PluginInstalled=connection.Status?.Installed==true;
if(connection.Package is {} availablePackage&&connection.Handshake is {} loadedPlugin&&availablePackage.Version!=loadedPlugin.Version)AutomaticPluginUpdateRequired=true;
if(connection.Status is {} status)PluginStatusText=status.Message;
if(connection.Waiting.Length!=0){NavigationNativeStatus=PluginStatusText=connection.Waiting;RelogUnavailableReason=connection.Waiting;return;}
if(CurrentPlayer.LoadGameAt!=session||CurrentPlayer.InGame!=inGame){nativeSessionSeen=null;return;}
var state=connection.Status!;var package=connection.Package!;var hello=connection.Handshake!;
nativeStatusConnected=connection.RelogReady||!inGame;
PluginRelogReady=connection.RelogReady;RelogUnavailableReason=connection.RelogIssue;
PluginTradeReady=connection.TradeReady;
if(connection.TradeIssue.Length!=0)runtime.Log("交易接口",connection.TradeIssue);
if(inGame&&!connection.RelogReady&&connection.RelogIssue.Length!=0)runtime.Log("重进接口",connection.RelogIssue);
PluginNavigationReady=inGame&&!state.PendingRemoval&&hello.Loaded&&hello.Active&&hello.SupportsNavigation&&package.SupportsNavigation
&&runtime.FlightDriver is NativeFlightControlDriver driver&&driver.ProcessId==connection.ProcessId;
PluginStatusText=$"插件已加载 · PID {connection.ProcessId} · {hello.Version}\n"+(hello.Active?"基础通信可用。\n"+(PluginNavigationReady?"自动导航驱动已就绪。":"等待进入飞行状态并完成原生遥测握手。")+"\n"+(PluginTradeReady?"自动贸易接口已就绪。":"自动贸易接口尚未就绪。")+"\n"+(PluginRelogReady?"原生重进接口已连接。":RelogUnavailableReason):"插件已停用,加载文件等待游戏退出后清理。");
NavigationNativeStatus=PluginNavigationReady?"原生导航已连接:姿态、实时热量、恒星边界、目标锁定和充能等待联锁。":hello.Active?"插件已加载,等待飞行状态和原生遥测。":"插件已停用。";
if(PluginNavigationReady&&runtime.FlightDriver is NativeFlightControlDriver{SupportsReferenceGeometry:false})
{
NavigationNativeStatus="已连接旧版导航插件;空间站与行星参考系需要更新插件后重启游戏。";
PluginStatusText+=$"\n可更新至 {package.Version},补齐行星与空间站参考系。请退出游戏后点击安装 / 更新。";
}
else if(PluginNavigationReady&&runtime.FlightDriver is NativeFlightControlDriver{SupportsAdaptiveFlightControl:false})
{
NavigationNativeStatus="当前加载的是旧版导航插件;更新并重启游戏后启用快速对齐与高功率补给。";
PluginStatusText+=$"\n可更新至 {package.Version},启用新的转向、热量预测和补给控制。";
}
else if(PluginNavigationReady&&runtime.FlightDriver is NativeFlightControlDriver{SupportsApplicationSteeringAxes:false})
{
PluginNavigationReady=false;
NavigationNativeStatus="当前交互接口尚未支持绕星三轴姿态;请在结束游玩后更新游戏插件。";
PluginStatusText+=$"\n可更新至 {package.Version},提供软件控制的偏航、俯仰和滚转输入。";
}
if(PluginNavigationReady)await EnsureGalaxyDisplayAsync();
}
catch(OperationCanceledException)when(lifetime.IsCancellationRequested){}
catch(Exception e){nativeStatusConnected=false;PluginRelogReady=false;RelogUnavailableReason="重进连接未就绪:"+e.Message;PluginTradeReady=false;PluginStatusText=(PluginInstalled?"插件已安装,实时握手未就绪:":"插件检查失败:")+e.Message;NavigationNativeStatus=PluginStatusText;}
finally{nativeStatusChecking=false;RefreshNavigationStartHint();RefreshRelogAvailability();}
}
[RelayCommand] private async Task InstallPluginAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
if(PluginBusy)return;PluginBusy=true;
try{PluginStatusText=(await pluginManager.InstallAsync(PluginGameExecutable,token)).Message;await runtime.SaveSettingsAsync(runtime.Settings with{PluginGameExecutable=PluginGameExecutable});await ReadPluginStatusAsync();runtime.Log("插件",PluginStatusText);}
catch(OperationCanceledException){PluginStatusText="插件操作已取消;已完成的步骤保留。";}
catch(Exception e){PluginStatusText=e.Message;}finally{PluginBusy=false;}
}
[RelayCommand] private async Task UninstallPluginAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
if(PluginBusy)return;PluginBusy=true;runtime.Runner.Stop();runtime.FlightAutomation.Stop();runtime.Navigator.Stop();PluginTradeReady=false;PluginNavigationReady=false;
try
{
var games=Process.GetProcessesByName("EliteDangerous64");
try{foreach(var game in games)if(string.Equals(game.MainModule?.FileName,PluginGameExecutable,StringComparison.OrdinalIgnoreCase))try{await GamePluginManager.ProbeAsync(game.Id,true,token);}catch(OperationCanceledException)when(!token.IsCancellationRequested){}catch(IOException){}}
finally{foreach(var game in games)game.Dispose();}
var status=await pluginManager.UninstallAsync(PluginGameExecutable,token);PluginInstalled=status.Installed;PluginStatusText=status.Message;runtime.Log("插件",PluginStatusText);
}
catch(OperationCanceledException){PluginStatusText="插件清理已取消;已完成的步骤保留。";}
catch(Exception e){PluginStatusText=e.Message;}finally{PluginBusy=false;}
}
[RelayCommand] private async Task ChoosePluginGameAsync()
{
var picker=new Windows.Storage.Pickers.FileOpenPicker();picker.FileTypeFilter.Add(".exe");InitializePicker(picker);
if(await picker.PickSingleFileAsync() is { } file){PluginGameExecutable=file.Path;await ReadPluginStatusAsync();}
}
private async Task RequirePluginTradeAsync()
{
await ReadPluginStatusAsync();
if(!PluginInstalled)throw new InvalidOperationException("自动贸易需要先在设置 → 游戏插件中安装插件。");
if(!PluginNavigationReady||runtime.FlightDriver is not NativeFlightControlDriver driver)
throw new InvalidOperationException("插件导航接口尚未就绪,工具箱暂不发出贸易或飞行指令。");
var hello=await new NavigationPipeClient(driver.ProcessId).CallAsync(new{op="describe"},lifetime.Token);
if(!NativeTradeCapabilities.SupportsDockedTrade(hello))
throw new NotSupportedException("当前插件尚未提供经过验证的停靠交易接口。");
PluginTradeReady=true;
}
}
using XFEEDToolBox.NativeSupport;
using XFEEDToolBox.Core.Execution;
using System.Diagnostics;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
private readonly GamePluginManager pluginManager=new(NativeSupportPackage.Directory);
[ObservableProperty] public partial string PluginGameExecutable { get; set; } = "";
[ObservableProperty] public partial string PluginStatusText { get; set; } = "插件未安装,自动贸易不可启动。";
[ObservableProperty] public partial bool PluginBusy { get; set; }
[ObservableProperty] public partial bool PluginInstalled { get; set; }
[ObservableProperty] public partial bool PluginTradeReady { get; set; }
private DateTimeOffset? nativeSessionSeen;
private bool nativeStatusChecking;
private Task? nativeStatusRead;
private DateTimeOffset nativeStatusRetryAt;
private bool nativeStatusConnected;
private void ObserveNativeSession(PlayerState player)
{
if(OfflinePreview)return;
if(!player.InGame){PluginNavigationReady=false;PluginRelogReady=false;nativeSessionSeen=null;nativeStatusConnected=false;runtime.UnbindFlightDriver();return;}
if(nativeStatusChecking||player.LoadGameAt is null||nativeSessionSeen==player.LoadGameAt&&(nativeStatusConnected||DateTimeOffset.UtcNow<nativeStatusRetryAt))return;
if(NavigationActive||PluginBusy)return;
nativeSessionSeen=player.LoadGameAt;
nativeStatusRetryAt=DateTimeOffset.UtcNow.AddSeconds(3);
_=ReadPluginStatusAsync();
}
private void InitializePluginSettings()
{ PluginGameExecutable=runtime.Settings.PluginGameExecutable;if(!OfflinePreview&&PluginGameExecutable.Length==0)PluginGameExecutable=FindSteamGame(); }
private static string FindSteamGame()
{
var steam=Microsoft.Win32.Registry.GetValue(@"HKEY_CURRENT_USER\Software\Valve\Steam","SteamPath",null) as string;if(steam is null)return "";
var libraries=new List<string>{steam};var vdf=Path.Combine(steam,"steamapps","libraryfolders.vdf");
if(File.Exists(vdf))foreach(Match match in Regex.Matches(File.ReadAllText(vdf),"\"path\"\\s+\"([^\"]+)\""))libraries.Add(match.Groups[1].Value.Replace("\\\\","\\"));
return libraries.Select(l=>Path.Combine(l,"steamapps","common","Elite Dangerous","Products","elite-dangerous-odyssey-64","EliteDangerous64.exe")).FirstOrDefault(File.Exists)??"";
}
[RelayCommand] private async Task ReadPluginStatusAsync()
{
if(OfflinePreview)return;
if(Environment.GetCommandLineArgs().Contains("--smoke-interface"))return;
if(nativeStatusRead is {} pending){await pending;return;}
var task=ReadPluginStatusCoreAsync();nativeStatusRead=task;
try{await task;}finally{nativeStatusRead=null;}
}
private async Task ReadPluginStatusCoreAsync()
{
nativeStatusChecking=true;
nativeStatusConnected=false;RelogUnavailableReason="正在连接当前游戏的重进接口";RefreshRelogAvailability();
PluginTradeReady=false;PluginNavigationReady=false;PluginRelogReady=false;
NavigationNativeStatus="正在检查游戏插件与导航连接…";
var session=CurrentPlayer.LoadGameAt;var inGame=CurrentPlayer.InGame;
try
{
var connection=await ReadPluginConnectionAsync(PluginGameExecutable,gameEnvironment?.Executable??"",inGame,lifetime.Token);
pluginQueryApplyThread=Environment.CurrentManagedThreadId;
PluginGameExecutable=connection.Executable;PluginInstalled=connection.Status?.Installed==true;
if(connection.Package is {} availablePackage&&connection.Handshake is {} loadedPlugin&&availablePackage.Version!=loadedPlugin.Version)AutomaticPluginUpdateRequired=true;
if(connection.Status is {} status)PluginStatusText=status.Message;
if(connection.Waiting.Length!=0){NavigationNativeStatus=PluginStatusText=connection.Waiting;RelogUnavailableReason=connection.Waiting;return;}
if(CurrentPlayer.LoadGameAt!=session||CurrentPlayer.InGame!=inGame){nativeSessionSeen=null;return;}
var state=connection.Status!;var package=connection.Package!;var hello=connection.Handshake!;
nativeStatusConnected=connection.RelogReady||!inGame;
PluginRelogReady=connection.RelogReady;RelogUnavailableReason=connection.RelogIssue;
PluginTradeReady=connection.TradeReady;
if(connection.TradeIssue.Length!=0)runtime.Log("交易接口",connection.TradeIssue);
if(inGame&&!connection.RelogReady&&connection.RelogIssue.Length!=0)runtime.Log("重进接口",connection.RelogIssue);
PluginNavigationReady=inGame&&!state.PendingRemoval&&hello.Loaded&&hello.Active&&hello.SupportsNavigation&&package.SupportsNavigation
&&runtime.FlightDriver is NativeFlightControlDriver driver&&driver.ProcessId==connection.ProcessId;
PluginStatusText=$"插件已加载 · PID {connection.ProcessId} · {hello.Version}\n"+(hello.Active?"基础通信可用。\n"+(PluginNavigationReady?"自动导航驱动已就绪。":"等待进入飞行状态并完成原生遥测握手。")+"\n"+(PluginTradeReady?"自动贸易接口已就绪。":"自动贸易接口尚未就绪。")+"\n"+(PluginRelogReady?"原生重进接口已连接。":RelogUnavailableReason):"插件已停用,加载文件等待游戏退出后清理。");
NavigationNativeStatus=PluginNavigationReady?"原生导航已连接:姿态、实时热量、恒星边界、目标锁定和充能等待联锁。":hello.Active?"插件已加载,等待飞行状态和原生遥测。":"插件已停用。";
if(PluginNavigationReady&&runtime.FlightDriver is NativeFlightControlDriver{SupportsReferenceGeometry:false})
{
NavigationNativeStatus="已连接旧版导航插件;空间站与行星参考系需要更新插件后重启游戏。";
PluginStatusText+=$"\n可更新至 {package.Version},补齐行星与空间站参考系。请退出游戏后点击安装 / 更新。";
}
else if(PluginNavigationReady&&runtime.FlightDriver is NativeFlightControlDriver{SupportsAdaptiveFlightControl:false})
{
NavigationNativeStatus="当前加载的是旧版导航插件;更新并重启游戏后启用快速对齐与高功率补给。";
PluginStatusText+=$"\n可更新至 {package.Version},启用新的转向、热量预测和补给控制。";
}
else if(PluginNavigationReady&&runtime.FlightDriver is NativeFlightControlDriver{SupportsApplicationSteeringAxes:false})
{
PluginNavigationReady=false;
NavigationNativeStatus="当前交互接口尚未支持绕星三轴姿态;请在结束游玩后更新游戏插件。";
PluginStatusText+=$"\n可更新至 {package.Version},提供软件控制的偏航、俯仰和滚转输入。";
}
if(PluginNavigationReady)await EnsureGalaxyDisplayAsync();
}
catch(OperationCanceledException)when(lifetime.IsCancellationRequested){}
catch(Exception e){nativeStatusConnected=false;PluginRelogReady=false;RelogUnavailableReason="重进连接未就绪:"+e.Message;PluginTradeReady=false;PluginStatusText=(PluginInstalled?"插件已安装,实时握手未就绪:":"插件检查失败:")+e.Message;NavigationNativeStatus=PluginStatusText;}
finally{nativeStatusChecking=false;RefreshNavigationStartHint();RefreshRelogAvailability();}
}
[RelayCommand] private async Task InstallPluginAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
if(PluginBusy)return;PluginBusy=true;
try{PluginStatusText=(await pluginManager.InstallAsync(PluginGameExecutable,token)).Message;await runtime.SaveSettingsAsync(runtime.Settings with{PluginGameExecutable=PluginGameExecutable});await ReadPluginStatusAsync();runtime.Log("插件",PluginStatusText);}
catch(OperationCanceledException){PluginStatusText="插件操作已取消;已完成的步骤保留。";}
catch(Exception e){PluginStatusText=e.Message;}finally{PluginBusy=false;}
}
[RelayCommand] private async Task UninstallPluginAsync(CancellationToken cancellationToken=default)
{
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token;
if(PluginBusy)return;PluginBusy=true;runtime.Runner.Stop();runtime.FlightAutomation.Stop();runtime.Navigator.Stop();PluginTradeReady=false;PluginNavigationReady=false;
try
{
var games=Process.GetProcessesByName("EliteDangerous64");
try{foreach(var game in games)if(string.Equals(game.MainModule?.FileName,PluginGameExecutable,StringComparison.OrdinalIgnoreCase))try{await GamePluginManager.ProbeAsync(game.Id,true,token);}catch(OperationCanceledException)when(!token.IsCancellationRequested){}catch(IOException){}}
finally{foreach(var game in games)game.Dispose();}
var status=await pluginManager.UninstallAsync(PluginGameExecutable,token);PluginInstalled=status.Installed;PluginStatusText=status.Message;runtime.Log("插件",PluginStatusText);
}
catch(OperationCanceledException){PluginStatusText="插件清理已取消;已完成的步骤保留。";}
catch(Exception e){PluginStatusText=e.Message;}finally{PluginBusy=false;}
}
[RelayCommand] private async Task ChoosePluginGameAsync()
{
var picker=new Windows.Storage.Pickers.FileOpenPicker();picker.FileTypeFilter.Add(".exe");InitializePicker(picker);
if(await picker.PickSingleFileAsync() is { } file){PluginGameExecutable=file.Path;await ReadPluginStatusAsync();}
}
private async Task RequirePluginTradeAsync()
{
await ReadPluginStatusAsync();
if(!PluginInstalled)throw new InvalidOperationException("自动贸易需要先在设置 → 游戏插件中安装插件。");
if(!PluginNavigationReady||runtime.FlightDriver is not NativeFlightControlDriver driver)
throw new InvalidOperationException("插件导航接口尚未就绪,工具箱暂不发出贸易或飞行指令。");
var hello=await new NavigationPipeClient(driver.ProcessId).CallAsync(new{op="describe"},lifetime.Token);
if(!NativeTradeCapabilities.SupportsDockedTrade(hello))
throw new NotSupportedException("当前插件尚未提供经过验证的停靠交易接口。");
PluginTradeReady=true;
}
}