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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Plugins;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Services;

internal static class GameSessionAcceptance
{
    internal static async Task LeaveAsync(int pid,string report)
    {
        if(File.Exists(report))throw new IOException("Use a new original-menu exit report.");
        var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
        await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync();var initial=journal.State;
        if(!initial.InGame||string.IsNullOrWhiteSpace(initial.CommanderId)||initial.CurrentShipId!=5||initial.CargoCount!=0||NavigationSafety.HasExternalThreat(initial))
            throw new InvalidOperationException("Expected the healthy, empty CG ship before updating its plugin.");
        using var process=Process.GetProcessById(pid);var started=process.StartTime.ToUniversalTime();
        var channel=new NativeRelogChannel(pid,NativeRelogChannel.SupportedGameSha256,token=>
        {
            token.ThrowIfCancellationRequested();process.Refresh();
            if(process.HasExited||process.StartTime.ToUniversalTime()!=started)throw new IOException("Game process changed during native menu exit.");
            return Task.CompletedTask;
        });
        var owner=Guid.NewGuid().ToString("N");var actions=new List<string>();string? failure=null;var passed=false;string? last=null;
        using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(100));
        try
        {
            await channel.BeginAsync(owner,timeout.Token);string? pending=null;var requested=DateTimeOffset.MinValue;
            while(true)
            {
                timeout.Token.ThrowIfCancellationRequested();var frame=await channel.ReadAsync(owner,timeout.Token);var now=DateTimeOffset.UtcNow;
                if(frame.State!=last){Console.WriteLine("原始返回菜单:"+frame.State);last=frame.State;}
                if(frame.Sequence<=0||now-frame.ObservedAt>TimeSpan.FromMilliseconds(350)||frame.ObservedAt>now.AddMilliseconds(100))
                {await Task.Delay(80,timeout.Token);continue;}
                if(frame.Stable&&frame.State=="MainMenu"){passed=true;break;}
                if(pending is not null)
                {
                    if(frame.LastRequestId==pending&&(frame.ActionApplied||frame.ActionFailed)||now-requested>TimeSpan.FromSeconds(3))pending=null;
                    else{await Task.Delay(80,timeout.Token);continue;}
                }
                var action=NativeRelogWorkflow.ChooseAction(frame,initial.GameMode,false);
                if(action is not null)
                {
                    pending=Guid.NewGuid().ToString("N");requested=now;
                    try{await channel.SubmitAsync(owner,pending,frame,action,timeout.Token);actions.Add(action);}
                    catch(NativeSessionStateChangedException){pending=null;}
                }
                await Task.Delay(80,timeout.Token);
            }
        }
        catch(Exception error){failure=error.ToString();}
        finally
        {
            await channel.EndAsync(owner);
            await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{passed,pid,lastState=last,actions,failure,screenUse=false,osInputsSent=0},ToolboxSettings.JsonOptions));
        }
        if(!passed)throw new InvalidOperationException("Original game menu exit did not complete; no process was terminated.");
    }
    internal static async Task<JsonElement> StatusAsync(int processId,CancellationToken token=default)
    {
        var pipe=new NavigationPipeClient(processId);var hello=await pipe.CallAsync(new{op="describe"},token);
        if(hello.TryGetProperty("nativeSessionStatus",out var enabled)&&enabled.ValueKind==JsonValueKind.True)
            return await pipe.CallAsync(new{op="session.status"},token);
        // Compatibility with 0.5.1: ending an unclaimed, freshly generated
        // owner returns the status without changing another workflow's lease.
        return await pipe.CallAsync(new{op="session.end",owner=Guid.NewGuid().ToString("N")},token);
    }
    internal static async Task RecoverDisconnectedAsync(int processId,string executable,string package,string report)
    {
        if(File.Exists(report))throw new IOException("Use a new recovery report path.");
        executable=Path.GetFullPath(executable);package=Path.GetFullPath(package);report=Path.GetFullPath(report);
        var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
        await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync();
        var driver=new WindowsGameSessionDriver(()=>journal.State,()=>executable);
        using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(5));var token=timeout.Token;
        var target=await driver.FindLaunchTargetAsync(token);
        if(target.ProcessId!=processId)throw new InvalidOperationException("The disconnected game process changed.");
        var pipe=new NavigationPipeClient(processId);var hello=await pipe.CallAsync(new{op="describe"},token);var status=await StatusAsync(processId,token);
        if(hello.GetProperty("gameSha256").GetString()!=NativeRelogChannel.SupportedGameSha256||hello.GetProperty("inGame").GetBoolean()
            ||status.GetProperty("state").GetString()!="FailedToConnect"||status.GetProperty("nextState").GetString()!="FailedToConnect")
            throw new InvalidOperationException("Only the verified disconnected game state may be restarted by this recovery.");
        if(!journal.State.Docked||string.IsNullOrWhiteSpace(journal.State.CommanderId)||journal.State.CurrentShipId!=5||journal.State.CargoCount!=1)
            throw new InvalidOperationException("The reserved one-ton ship or last docking identity changed.");
        var manager=new GamePluginManager(package);var previous=await manager.InspectAsync(executable,token);
        if(!previous.Installed)throw new InvalidOperationException("Recovery may update only the toolbox-owned plugin installation.");
        var bundle=manager.Package();
        var bundleHash=Convert.ToHexStringLower(System.Security.Cryptography.SHA256.HashData(await File.ReadAllBytesAsync(Path.Combine(package,bundle.Loader),token)));
        if(bundleHash!=bundle.Sha256||bundle.SupportedGameSha256!=NativeRelogChannel.SupportedGameSha256)
            throw new InvalidDataException("The replacement package failed validation; the disconnected game was left running.");
        var stages=new List<string>();string? failure=null;var installed=false;var entered=false;
        try
        {
            await driver.ValidateRestartAsync(target,token);await driver.TerminateAsync(target,token);stages.Add("verified-disconnected-process-terminated");
            var watch=Stopwatch.StartNew();while(!await driver.HasExitedAsync(target,token))
            {if(watch.Elapsed>TimeSpan.FromSeconds(20))throw new IOException("Old disconnected process did not exit.");await Task.Delay(100,token);}
            await manager.InstallAsync(executable,token);installed=true;stages.Add("managed-plugin-updated");
            await StartAsync(executable,report+".entry.json");entered=true;stages.Add("original-solo-entry-confirmed");
        }
        catch(Exception error){failure=error.ToString();}
        finally{await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{processId,initial=status,installed,entered,stages,failure,screenUse=false,flightInputsSent=0,maintenanceHoldReleased=false},ToolboxSettings.JsonOptions));}
        if(!entered)throw new InvalidOperationException("Disconnected-session recovery has not confirmed entry; maintenance hold remains active. "+failure);
    }
    public static async Task StartAsync(string executable,string report,int? existingProcessId=null)
    {
        if(File.Exists(report))throw new IOException("Use a new report path.");
        executable=Path.GetFullPath(executable);report=Path.GetFullPath(report);
        var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
        await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync();
        var initial=journal.State;
        if(!NativeRelogWorkflow.SupportsMode(initial.GameMode)||initial.CommanderId.Length==0)
            throw new InvalidOperationException("A known commander and previous Solo/Open mode are required.");
        using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(8));var token=timeout.Token;
        var driver=new WindowsGameSessionDriver(()=>journal.State,()=>executable);
        var sessions=new GameSessionController(driver,()=>false);
        sessions.Progress+=p=>{if(p.Message.Length>0)Console.WriteLine(p.Message);};
        if(existingProcessId is null)await sessions.RunAsync(SessionOperation.Start,token);
        var target=await driver.FindTargetAsync(token);
        if(existingProcessId is {} expectedPid&&target.ProcessId!=expectedPid)
            throw new InvalidOperationException("The selected existing game process changed; no replacement was started.");
        using var process=Process.GetProcessById(target.ProcessId);
        if(process.HasExited||!string.Equals(process.MainModule?.FileName,executable,StringComparison.OrdinalIgnoreCase))
            throw new InvalidOperationException("Official launch returned another game instance.");
        PluginHandshake? hello=null;
        while(hello is null)
        {
            token.ThrowIfCancellationRequested();
            try{hello=await GamePluginManager.StartNavigationObservationAsync(target.ProcessId,token);}
            catch(Exception error)when(error is IOException or OperationCanceledException&&!token.IsCancellationRequested)
            {await Task.Delay(250,token);}
        }
        if(!hello.Active||!hello.ObservationStarted)throw new InvalidOperationException("Managed plugin observation is not ready.");
        var channel=new NativeRelogChannel(target.ProcessId,NativeRelogChannel.SupportedGameSha256,t=>
        {
            t.ThrowIfCancellationRequested();process.Refresh();
            if(process.HasExited||process.StartTime.ToUniversalTime().Ticks!=target.StartedUtcTicks)throw new IOException("Game process changed during entry.");
            return Task.CompletedTask;
        },entryOnly:true);
        var owner=Guid.NewGuid().ToString("N");var actions=new List<string>();string? pending=null;
        var requested=DateTimeOffset.MinValue;string? lastState=null;var passed=false;string? entryFailure=null;
        try
        {
            while(true)
            {
                token.ThrowIfCancellationRequested();
                var connection=await StatusAsync(target.ProcessId,token);
                if(connection.GetProperty("state").GetString()=="FailedToConnect")throw new IOException("Original game connection failed; maintenance hold remains active.");
                try{await channel.BeginAsync(owner,token);break;}
                catch(InvalidOperationException error)when(error.Message.StartsWith("Wait for an idle game",StringComparison.Ordinal))
                {await Task.Delay(150,token);}
            }
            while(true)
            {
                if(File.Exists(report+".stop"))throw new OperationCanceledException("Entry stopped.");
                var frame=await channel.ReadAsync(owner,token);var now=DateTimeOffset.UtcNow;
                if(frame.State=="FailedToConnect")throw new IOException("Original game connection failed; maintenance hold remains active.");
                if(frame.State!=lastState){Console.WriteLine("原生菜单:"+frame.State);lastState=frame.State;}
                if(frame.Sequence<=0||now-frame.ObservedAt>TimeSpan.FromMilliseconds(350)||frame.ObservedAt>now.AddMilliseconds(100))
                {await Task.Delay(100,token);continue;}
                var current=journal.State;
                if(current.CommanderId!=initial.CommanderId)throw new InvalidOperationException("Commander changed during game entry.");
                if(frame.Stable&&frame.State=="InGame"&&current.InGame&&current.GameMode==initial.GameMode&&current.LoadGameAt>=initial.LoadGameAt)
                {passed=true;break;}
                if(pending is not null)
                {
                    if(frame.LastRequestId==pending&&(frame.ActionApplied||frame.ActionFailed))pending=null;
                    else if(now-requested>TimeSpan.FromSeconds(3))pending=null;
                    else{await Task.Delay(100,token);continue;}
                }
                var action=NativeRelogWorkflow.ChooseAction(frame,initial.GameMode,reachedMainMenu:true);
                if(action is not null)
                {
                    pending=Guid.NewGuid().ToString("N");requested=now;
                    try{await channel.SubmitAsync(owner,pending,frame,action,token);actions.Add(action);}
                    catch(NativeSessionStateChangedException){pending=null;}
                }
                await Task.Delay(100,token);
            }
        }
        catch(Exception error){entryFailure=error.ToString();throw;}
        finally
        {
            await channel.EndAsync(owner);
            await File.WriteAllTextAsync(report,JsonSerializer.Serialize(new{passed,failure=entryFailure,lastState,target.ProcessId,hello.Version,actions,
                ship=journal.State.Ship,shipId=journal.State.CurrentShipId,capacity=journal.State.CargoCapacity,
                system=journal.State.SystemName,station=journal.State.StationName,journal.State.Docked,screenUse=false,flightInputsSent=0},ToolboxSettings.JsonOptions));
        }
        Console.WriteLine(JsonSerializer.Serialize(new{passed,target.ProcessId,hello.Version,ship=journal.State.Ship,capacity=journal.State.CargoCapacity,screenUse=false}));
    }
}