using XFEEDToolBox.NativeSupport;
using System.Diagnostics;
using System.Security.Cryptography;
using Microsoft.Win32;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Session;
namespace XFEEDToolBox.Services;
public sealed class WindowsGameSessionDriver(Func<PlayerState> player, Func<string>? configuredExecutable = null,
Func<bool>? relogAvailable = null, Action<string>? relogProgress = null,
Func<int,string,CancellationToken,Task>? ensureProtection = null) : IGameSessionDriver
{
public bool CanRelog => relogAvailable?.Invoke() == true && player().InGame && NativeRelogWorkflow.SupportsMode(player().GameMode);
public async Task<GameSessionTarget> FindTargetAsync(CancellationToken token)
{
var environment = await GameEnvironmentReader.ReadAsync(player(), token: token);
if (environment.ProcessId is not { } pid || environment.ProfileMode != "standard") throw new InvalidOperationException("没有唯一识别到标准档案的游戏实例。");
using var process = Process.GetProcessById(pid);
return await InstallationAsync(environment.Executable,pid,process.StartTime.ToUniversalTime().Ticks,token);
}
public async Task<GameSessionTarget> FindLaunchTargetAsync(CancellationToken token)
{
token.ThrowIfCancellationRequested();
var executable = configuredExecutable?.Invoke().Trim() ?? "";
var games = Process.GetProcessesByName("EliteDangerous64");
try
{
var live=new List<GameSessionTarget>();
foreach(var game in games)
{
if(EnumeratedProcessExited(game))continue;
string runningPath;long started;
try
{
runningPath=game.MainModule?.FileName??throw new InvalidOperationException("无法核对正在运行的游戏路径。");
started=game.StartTime.ToUniversalTime().Ticks;
}
catch(ArgumentException){continue;}
catch(InvalidOperationException)when(EnumeratedProcessExited(game)){continue;}
if (executable.Length > 0 && !string.Equals(Path.GetFullPath(executable),runningPath,StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("游戏已在另一个安装目录运行,未启动第二个实例。");
live.Add(await InstallationAsync(runningPath,game.Id,started,token));
}
if(live.Count>1)throw new InvalidOperationException("检测到多个游戏实例,请先确认当前游戏状态。");
if(live.Count==1)return live[0];
}
finally { foreach (var game in games) game.Dispose(); }
if (executable.Length == 0) throw new InvalidOperationException("未找到游戏目录,请先在“设置 → 游戏插件”中选择 EliteDangerous64.exe。");
return await InstallationAsync(Path.GetFullPath(executable),0,0,token);
}
private static async Task<GameSessionTarget> InstallationAsync(string executable,int processId,long startedUtcTicks,CancellationToken token)
{
var root = Directory.GetParent(executable)?.Parent?.Parent?.FullName ?? "";
var launcher = Path.Combine(root, "EDLaunch.exe");
var manifest = Directory.GetParent(root)?.Parent is { Name: "steamapps" } apps ? Path.Combine(apps.FullName, "appmanifest_359320.acf") : "";
var steam = manifest.Length > 0 && File.Exists(manifest) && (await File.ReadAllTextAsync(manifest, token)).Contains("\"359320\"", StringComparison.Ordinal);
return new(processId, startedUtcTicks, executable, File.Exists(launcher) ? launcher : "", steam);
}
private static Process OpenVerified(GameSessionTarget target)
{
Process process;
try{process=Process.GetProcessById(target.ProcessId);}
catch(ArgumentException){throw new StartupClientExitedException(target.ProcessId);}
try
{
if(process.HasExited||process.StartTime.ToUniversalTime().Ticks!=target.StartedUtcTicks)throw new StartupClientExitedException(target.ProcessId);
if(!string.Equals(process.MainModule?.FileName,target.Executable,StringComparison.OrdinalIgnoreCase)||!process.ProcessName.Equals("EliteDangerous64",StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException("游戏进程身份已变化,已停止操作。");
return process;
}
catch(InvalidOperationException)when(OriginalProcessExited(target))
{process.Dispose();throw new StartupClientExitedException(target.ProcessId);}
catch { process.Dispose(); throw; }
}
private static bool EnumeratedProcessExited(Process process)
{
try{return process.HasExited;}
catch(Exception error)when(error is ArgumentException or InvalidOperationException){return true;}
}
private static bool OriginalProcessExited(GameSessionTarget target)
{
if(target.ProcessId==0)return true;
try{using var process=Process.GetProcessById(target.ProcessId);return process.HasExited||process.StartTime.ToUniversalTime().Ticks!=target.StartedUtcTicks;}
catch(Exception error)when(error is ArgumentException or InvalidOperationException){return true;}
}
public Task ValidateRestartAsync(GameSessionTarget target, CancellationToken token)
{
token.ThrowIfCancellationRequested(); using var p = OpenVerified(target);
return ValidateLaunchAsync(target,token);
}
public Task ValidateExitAsync(GameSessionTarget target,CancellationToken token)
{token.ThrowIfCancellationRequested();using var process=OpenVerified(target);return Task.CompletedTask;}
public Task ValidateLaunchAsync(GameSessionTarget target, CancellationToken token)
{
token.ThrowIfCancellationRequested();
if (!File.Exists(target.Executable) || !Path.GetFileName(target.Executable).Equals("EliteDangerous64.exe",StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("游戏主程序路径无效,请重新选择 EliteDangerous64.exe。");
var expectedLauncher = Path.Combine(Directory.GetParent(target.Executable)?.Parent?.Parent?.FullName ?? "","EDLaunch.exe");
if (!File.Exists(target.Launcher) || !string.Equals(Path.GetFullPath(target.Launcher),expectedLauncher,StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("未找到同一安装目录的官方启动器;请先确认游戏安装完整。");
if(target.Steam)_=SteamExecutable();
return Task.CompletedTask;
}
private static string SteamExecutable()
{
using var key=Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam");
var configured=key?.GetValue("SteamExe") as string;
if(string.IsNullOrWhiteSpace(configured)||!File.Exists(configured)||!Path.GetFileName(configured).Equals("steam.exe",StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("未找到 Steam 客户端;未结束游戏。请先从 Steam 正常启动一次。");
return Path.GetFullPath(configured);
}
public Task TerminateAsync(GameSessionTarget target, CancellationToken token)
{ token.ThrowIfCancellationRequested(); using var p = OpenVerified(target); p.Kill(entireProcessTree: false); return Task.CompletedTask; }
public Task<bool> HasExitedAsync(GameSessionTarget target, CancellationToken token)
{
token.ThrowIfCancellationRequested();
return Task.FromResult(OriginalProcessExited(target));
}
public async Task LaunchAsync(GameSessionTarget previous, CancellationToken token)
{
await ValidateLaunchAsync(previous,token);
if (!await HasExitedAsync(previous, token)) throw new InvalidOperationException("原进程尚未结束,不能启动第二个实例。");
EnsureNoRunningGame(token);
// The official launcher has a single-instance mutex. Its /autorun obtains fresh platform tickets.
var launchers = Process.GetProcessesByName("EDLaunch");
try
{
foreach (var launcher in launchers)
{
token.ThrowIfCancellationRequested();
if (!string.Equals(launcher.MainModule?.FileName, previous.Launcher, StringComparison.OrdinalIgnoreCase)) continue;
if (!launcher.CloseMainWindow()) throw new InvalidOperationException("官方启动器未接受关闭请求,已停止;请手动关闭后从 Steam 启动。");
using var closing = CancellationTokenSource.CreateLinkedTokenSource(token); closing.CancelAfter(TimeSpan.FromSeconds(15));
try { await launcher.WaitForExitAsync(closing.Token); }
catch (OperationCanceledException) when (!token.IsCancellationRequested) { throw new TimeoutException("官方启动器未关闭,已停止;未强杀启动器。"); }
}
}
catch (System.ComponentModel.Win32Exception error) when (error.NativeErrorCode == 5)
{
throw new InvalidOperationException("Windows 拒绝工具箱连接现有官方启动器。请关闭该启动器,再由工具箱以普通权限重新启动游戏。", error);
}
finally { foreach (var p in launchers) p.Dispose(); }
token.ThrowIfCancellationRequested();
// Let the real Steam client create its launcher with the official app
// context. Child-local app identifiers alone do not confirm Steam IPC
// or account authorisation, and must not be used to diagnose permissions.
// /novr is parsed by FORCManager and forwarded to the game explicitly.
var plan=OfficialGameLaunchPlan.Desktop(previous.Steam,previous.Executable.Contains("odyssey",StringComparison.OrdinalIgnoreCase));
var start=new ProcessStartInfo(previous.Steam?SteamExecutable():previous.Launcher){UseShellExecute=false,WorkingDirectory=Path.GetDirectoryName(previous.Launcher)!,WindowStyle=ProcessWindowStyle.Hidden};
if(previous.Steam){start.ArgumentList.Add("-applaunch");start.ArgumentList.Add("359320");}
foreach(var argument in plan.Arguments)
if(!previous.Steam||argument!="/Steam")start.ArgumentList.Add(argument);
if(!previous.Steam)foreach(var pair in plan.Environment)start.Environment[pair.Key]=pair.Value;
EnsureNoRunningGame(token);
using var launched = Process.Start(start) ?? throw new InvalidOperationException("无法启动官方启动器。");
}
private static void EnsureNoRunningGame(CancellationToken token)
{
token.ThrowIfCancellationRequested();
var games = Process.GetProcessesByName("EliteDangerous64");
try { if (games.Any(game => !game.HasExited)) throw new InvalidOperationException("游戏已经在运行,未重复启动。"); }
finally { foreach (var game in games) game.Dispose(); }
}
public Task<GameSessionTarget?> FindReplacementAsync(GameSessionTarget previous, CancellationToken token)
{
token.ThrowIfCancellationRequested(); var games = Process.GetProcessesByName("EliteDangerous64");
try
{
var matches = new List<GameSessionTarget>();
foreach (var p in games)
if (!p.HasExited && string.Equals(p.MainModule?.FileName, previous.Executable, StringComparison.OrdinalIgnoreCase) && p.StartTime.ToUniversalTime().Ticks > previous.StartedUtcTicks)
matches.Add(previous with { ProcessId = p.Id, StartedUtcTicks = p.StartTime.ToUniversalTime().Ticks });
return Task.FromResult(matches.Count == 1 ? matches[0] : null);
}
finally { foreach (var p in games) p.Dispose(); }
}
public async Task RelogAsync(GameSessionTarget target, CancellationToken token)
{
using var process = OpenVerified(target);
string hash;
await using (var image = File.OpenRead(target.Executable))
hash = Convert.ToHexStringLower(await SHA256.HashDataAsync(image, token));
if (hash != NativeRelogChannel.SupportedGameSha256)
throw new NotSupportedException("当前游戏构建尚未匹配原生重进接口,未执行游戏操作。");
if(ensureProtection is not null)await ensureProtection(target.ProcessId,hash,token);
var channel = new NativeSessionRecoveryChannel(target.ProcessId, hash, Path.Combine(NativeSupportPackage.Directory,"DockingControl"), cancellation =>
{
cancellation.ThrowIfCancellationRequested();
using var verified = OpenVerified(target);
return Task.CompletedTask;
});
channel.Progress+=message=>relogProgress?.Invoke(message);
var relog = new NativeRelogWorkflow(channel, player){Timeout=TimeSpan.FromMinutes(8)};
relog.Progress += message => relogProgress?.Invoke(message);
await relog.RunAsync(token);
}
}
using XFEEDToolBox.NativeSupport;
using System.Diagnostics;
using System.Security.Cryptography;
using Microsoft.Win32;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Session;
namespace XFEEDToolBox.Services;
public sealed class WindowsGameSessionDriver(Func<PlayerState> player, Func<string>? configuredExecutable = null,
Func<bool>? relogAvailable = null, Action<string>? relogProgress = null,
Func<int,string,CancellationToken,Task>? ensureProtection = null) : IGameSessionDriver
{
public bool CanRelog => relogAvailable?.Invoke() == true && player().InGame && NativeRelogWorkflow.SupportsMode(player().GameMode);
public async Task<GameSessionTarget> FindTargetAsync(CancellationToken token)
{
var environment = await GameEnvironmentReader.ReadAsync(player(), token: token);
if (environment.ProcessId is not { } pid || environment.ProfileMode != "standard") throw new InvalidOperationException("没有唯一识别到标准档案的游戏实例。");
using var process = Process.GetProcessById(pid);
return await InstallationAsync(environment.Executable,pid,process.StartTime.ToUniversalTime().Ticks,token);
}
public async Task<GameSessionTarget> FindLaunchTargetAsync(CancellationToken token)
{
token.ThrowIfCancellationRequested();
var executable = configuredExecutable?.Invoke().Trim() ?? "";
var games = Process.GetProcessesByName("EliteDangerous64");
try
{
var live=new List<GameSessionTarget>();
foreach(var game in games)
{
if(EnumeratedProcessExited(game))continue;
string runningPath;long started;
try
{
runningPath=game.MainModule?.FileName??throw new InvalidOperationException("无法核对正在运行的游戏路径。");
started=game.StartTime.ToUniversalTime().Ticks;
}
catch(ArgumentException){continue;}
catch(InvalidOperationException)when(EnumeratedProcessExited(game)){continue;}
if (executable.Length > 0 && !string.Equals(Path.GetFullPath(executable),runningPath,StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("游戏已在另一个安装目录运行,未启动第二个实例。");
live.Add(await InstallationAsync(runningPath,game.Id,started,token));
}
if(live.Count>1)throw new InvalidOperationException("检测到多个游戏实例,请先确认当前游戏状态。");
if(live.Count==1)return live[0];
}
finally { foreach (var game in games) game.Dispose(); }
if (executable.Length == 0) throw new InvalidOperationException("未找到游戏目录,请先在“设置 → 游戏插件”中选择 EliteDangerous64.exe。");
return await InstallationAsync(Path.GetFullPath(executable),0,0,token);
}
private static async Task<GameSessionTarget> InstallationAsync(string executable,int processId,long startedUtcTicks,CancellationToken token)
{
var root = Directory.GetParent(executable)?.Parent?.Parent?.FullName ?? "";
var launcher = Path.Combine(root, "EDLaunch.exe");
var manifest = Directory.GetParent(root)?.Parent is { Name: "steamapps" } apps ? Path.Combine(apps.FullName, "appmanifest_359320.acf") : "";
var steam = manifest.Length > 0 && File.Exists(manifest) && (await File.ReadAllTextAsync(manifest, token)).Contains("\"359320\"", StringComparison.Ordinal);
return new(processId, startedUtcTicks, executable, File.Exists(launcher) ? launcher : "", steam);
}
private static Process OpenVerified(GameSessionTarget target)
{
Process process;
try{process=Process.GetProcessById(target.ProcessId);}
catch(ArgumentException){throw new StartupClientExitedException(target.ProcessId);}
try
{
if(process.HasExited||process.StartTime.ToUniversalTime().Ticks!=target.StartedUtcTicks)throw new StartupClientExitedException(target.ProcessId);
if(!string.Equals(process.MainModule?.FileName,target.Executable,StringComparison.OrdinalIgnoreCase)||!process.ProcessName.Equals("EliteDangerous64",StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException("游戏进程身份已变化,已停止操作。");
return process;
}
catch(InvalidOperationException)when(OriginalProcessExited(target))
{process.Dispose();throw new StartupClientExitedException(target.ProcessId);}
catch { process.Dispose(); throw; }
}
private static bool EnumeratedProcessExited(Process process)
{
try{return process.HasExited;}
catch(Exception error)when(error is ArgumentException or InvalidOperationException){return true;}
}
private static bool OriginalProcessExited(GameSessionTarget target)
{
if(target.ProcessId==0)return true;
try{using var process=Process.GetProcessById(target.ProcessId);return process.HasExited||process.StartTime.ToUniversalTime().Ticks!=target.StartedUtcTicks;}
catch(Exception error)when(error is ArgumentException or InvalidOperationException){return true;}
}
public Task ValidateRestartAsync(GameSessionTarget target, CancellationToken token)
{
token.ThrowIfCancellationRequested(); using var p = OpenVerified(target);
return ValidateLaunchAsync(target,token);
}
public Task ValidateExitAsync(GameSessionTarget target,CancellationToken token)
{token.ThrowIfCancellationRequested();using var process=OpenVerified(target);return Task.CompletedTask;}
public Task ValidateLaunchAsync(GameSessionTarget target, CancellationToken token)
{
token.ThrowIfCancellationRequested();
if (!File.Exists(target.Executable) || !Path.GetFileName(target.Executable).Equals("EliteDangerous64.exe",StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("游戏主程序路径无效,请重新选择 EliteDangerous64.exe。");
var expectedLauncher = Path.Combine(Directory.GetParent(target.Executable)?.Parent?.Parent?.FullName ?? "","EDLaunch.exe");
if (!File.Exists(target.Launcher) || !string.Equals(Path.GetFullPath(target.Launcher),expectedLauncher,StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("未找到同一安装目录的官方启动器;请先确认游戏安装完整。");
if(target.Steam)_=SteamExecutable();
return Task.CompletedTask;
}
private static string SteamExecutable()
{
using var key=Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam");
var configured=key?.GetValue("SteamExe") as string;
if(string.IsNullOrWhiteSpace(configured)||!File.Exists(configured)||!Path.GetFileName(configured).Equals("steam.exe",StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("未找到 Steam 客户端;未结束游戏。请先从 Steam 正常启动一次。");
return Path.GetFullPath(configured);
}
public Task TerminateAsync(GameSessionTarget target, CancellationToken token)
{ token.ThrowIfCancellationRequested(); using var p = OpenVerified(target); p.Kill(entireProcessTree: false); return Task.CompletedTask; }
public Task<bool> HasExitedAsync(GameSessionTarget target, CancellationToken token)
{
token.ThrowIfCancellationRequested();
return Task.FromResult(OriginalProcessExited(target));
}
public async Task LaunchAsync(GameSessionTarget previous, CancellationToken token)
{
await ValidateLaunchAsync(previous,token);
if (!await HasExitedAsync(previous, token)) throw new InvalidOperationException("原进程尚未结束,不能启动第二个实例。");
EnsureNoRunningGame(token);
// The official launcher has a single-instance mutex. Its /autorun obtains fresh platform tickets.
var launchers = Process.GetProcessesByName("EDLaunch");
try
{
foreach (var launcher in launchers)
{
token.ThrowIfCancellationRequested();
if (!string.Equals(launcher.MainModule?.FileName, previous.Launcher, StringComparison.OrdinalIgnoreCase)) continue;
if (!launcher.CloseMainWindow()) throw new InvalidOperationException("官方启动器未接受关闭请求,已停止;请手动关闭后从 Steam 启动。");
using var closing = CancellationTokenSource.CreateLinkedTokenSource(token); closing.CancelAfter(TimeSpan.FromSeconds(15));
try { await launcher.WaitForExitAsync(closing.Token); }
catch (OperationCanceledException) when (!token.IsCancellationRequested) { throw new TimeoutException("官方启动器未关闭,已停止;未强杀启动器。"); }
}
}
catch (System.ComponentModel.Win32Exception error) when (error.NativeErrorCode == 5)
{
throw new InvalidOperationException("Windows 拒绝工具箱连接现有官方启动器。请关闭该启动器,再由工具箱以普通权限重新启动游戏。", error);
}
finally { foreach (var p in launchers) p.Dispose(); }
token.ThrowIfCancellationRequested();
// Let the real Steam client create its launcher with the official app
// context. Child-local app identifiers alone do not confirm Steam IPC
// or account authorisation, and must not be used to diagnose permissions.
// /novr is parsed by FORCManager and forwarded to the game explicitly.
var plan=OfficialGameLaunchPlan.Desktop(previous.Steam,previous.Executable.Contains("odyssey",StringComparison.OrdinalIgnoreCase));
var start=new ProcessStartInfo(previous.Steam?SteamExecutable():previous.Launcher){UseShellExecute=false,WorkingDirectory=Path.GetDirectoryName(previous.Launcher)!,WindowStyle=ProcessWindowStyle.Hidden};
if(previous.Steam){start.ArgumentList.Add("-applaunch");start.ArgumentList.Add("359320");}
foreach(var argument in plan.Arguments)
if(!previous.Steam||argument!="/Steam")start.ArgumentList.Add(argument);
if(!previous.Steam)foreach(var pair in plan.Environment)start.Environment[pair.Key]=pair.Value;
EnsureNoRunningGame(token);
using var launched = Process.Start(start) ?? throw new InvalidOperationException("无法启动官方启动器。");
}
private static void EnsureNoRunningGame(CancellationToken token)
{
token.ThrowIfCancellationRequested();
var games = Process.GetProcessesByName("EliteDangerous64");
try { if (games.Any(game => !game.HasExited)) throw new InvalidOperationException("游戏已经在运行,未重复启动。"); }
finally { foreach (var game in games) game.Dispose(); }
}
public Task<GameSessionTarget?> FindReplacementAsync(GameSessionTarget previous, CancellationToken token)
{
token.ThrowIfCancellationRequested(); var games = Process.GetProcessesByName("EliteDangerous64");
try
{
var matches = new List<GameSessionTarget>();
foreach (var p in games)
if (!p.HasExited && string.Equals(p.MainModule?.FileName, previous.Executable, StringComparison.OrdinalIgnoreCase) && p.StartTime.ToUniversalTime().Ticks > previous.StartedUtcTicks)
matches.Add(previous with { ProcessId = p.Id, StartedUtcTicks = p.StartTime.ToUniversalTime().Ticks });
return Task.FromResult(matches.Count == 1 ? matches[0] : null);
}
finally { foreach (var p in games) p.Dispose(); }
}
public async Task RelogAsync(GameSessionTarget target, CancellationToken token)
{
using var process = OpenVerified(target);
string hash;
await using (var image = File.OpenRead(target.Executable))
hash = Convert.ToHexStringLower(await SHA256.HashDataAsync(image, token));
if (hash != NativeRelogChannel.SupportedGameSha256)
throw new NotSupportedException("当前游戏构建尚未匹配原生重进接口,未执行游戏操作。");
if(ensureProtection is not null)await ensureProtection(target.ProcessId,hash,token);
var channel = new NativeSessionRecoveryChannel(target.ProcessId, hash, Path.Combine(NativeSupportPackage.Directory,"DockingControl"), cancellation =>
{
cancellation.ThrowIfCancellationRequested();
using var verified = OpenVerified(target);
return Task.CompletedTask;
});
channel.Progress+=message=>relogProgress?.Invoke(message);
var relog = new NativeRelogWorkflow(channel, player){Timeout=TimeSpan.FromMinutes(8)};
relog.Progress += message => relogProgress?.Invoke(message);
await relog.RunAsync(token);
}
}