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 player, Func? configuredExecutable = null, Func? relogAvailable = null, Action? relogProgress = null, Func? ensureProtection = null) : IGameSessionDriver { public bool CanRelog => relogAvailable?.Invoke() == true && player().InGame && NativeRelogWorkflow.SupportsMode(player().GameMode); public async Task 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 FindLaunchTargetAsync(CancellationToken token) { token.ThrowIfCancellationRequested(); var executable = configuredExecutable?.Invoke().Trim() ?? ""; var games = Process.GetProcessesByName("EliteDangerous64"); try { var live=new List(); 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 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 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 FindReplacementAsync(GameSessionTarget previous, CancellationToken token) { token.ThrowIfCancellationRequested(); var games = Process.GetProcessesByName("EliteDangerous64"); try { var matches = new List(); 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); } }