XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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);
    }
}