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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

internal static class DockingFixtureWait
{
    // A continuously running, explicitly launched test workflow. No scheduler,
    // new game process, process termination, OS input, or arbitrary mode change.
    internal static async Task RunAsync(int pid,long started,int observerPid,long observerStarted,string root,string report)
    {
        root=Path.GetFullPath(root);report=Path.GetFullPath(report);
        if(Path.GetDirectoryName(report)!=root||File.Exists(report))throw new IOException("Use a fresh report in the existing trade work directory.");
        Directory.CreateDirectory(root);var stopFile=report+".stop";var fixtureReport=report+".fixture.json";
        using var exclusive=new FileStream(Path.Combine(root,"docking-fixture-wait.lock"),FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.None);
        using var stop=new CancellationTokenSource();var token=stop.Token;
        await using var log=new StreamWriter(new FileStream(report+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.ReadWrite|FileShare.Delete));
        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 known=journal.State;
        if(known.CommanderId.Length==0||known.CurrentShipId!=5||known.CargoCount!=0||known.CargoCapacity!=192||known.GameMode!="Solo")throw new InvalidDataException("Known empty Solo Corsair identity required for the dedicated test.");
        var pipe=new NavigationPipeClient(pid);var began=DateTimeOffset.UtcNow;var phase="waiting-original-session";string? reason=null,lastLog=null,lastState=null;
        JsonElement? native=null;var attemptedEntry=false;var testStarted=false;long reads=0;bool? passed=null;DateTimeOffset lastWritten=default;
        void Verify()
        {
            using var game=Process.GetProcessById(pid);using var observer=Process.GetProcessById(observerPid);
            if(game.HasExited||game.StartTime.ToUniversalTime().Ticks!=started||game.ProcessName!="EliteDangerous64"
                ||!string.Equals(game.MainModule?.FileName,settings.PluginGameExecutable,StringComparison.OrdinalIgnoreCase)
                ||observer.HasExited||observer.StartTime.ToUniversalTime().Ticks!=observerStarted||observer.ProcessName!="NativeAcceptance")
                throw new InvalidOperationException("Bound game or independent observer changed; test workflow stopped without retargeting.");
        }
        static JsonDocument SharedJson(string path)
        {using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);return JsonDocument.Parse(stream);}
        bool IdentityReady()
        {
            var value=journal.State;
            return value.InGame&&value.GameMode=="Solo"&&value.CommanderId==known.CommanderId&&value.CurrentShipId==5&&value.CargoCount==0&&value.CargoCapacity==192
                &&value.LoadGameAt is {} load&&load.UtcTicks>=started-TimeSpan.FromSeconds(5).Ticks
                &&!NavigationSafety.UnhealthyHull(value)&&!NavigationSafety.HasExternalThreat(value);
        }
        async Task State(bool force=false)
        {
            if(!force&&DateTimeOffset.UtcNow-lastWritten<TimeSpan.FromSeconds(10))return;
            var value=new JsonObject{["phase"]=phase,["at"]=DateTimeOffset.UtcNow,["began"]=began,["processId"]=pid,["gameStartedUtcTicks"]=started,
                ["observerPid"]=observerPid,["observerStartedUtcTicks"]=observerStarted,["stopFile"]=stopFile,["fixtureReport"]=fixtureReport,["sessionReads"]=reads,
                ["testStarted"]=testStarted,["identityReady"]=IdentityReady(),["entryAttempted"]=attemptedEntry,["reason"]=reason,["clientCreated"]=false,["clientTerminated"]=false,
                ["oldEntryFailurePreserved"]=true,["osInputSent"]=false,["tradeOrdersSent"]=0};
            if(native is {} frame)value["session"]=JsonNode.Parse(frame.GetRawText());
            if(passed is {} result)value["passed"]=result;
            var text=value.ToJsonString(ToolboxSettings.JsonOptions);await File.WriteAllTextAsync(report+".tmp",text,CancellationToken.None);File.Move(report+".tmp",report,true);lastWritten=DateTimeOffset.UtcNow;
            var key=phase+"|"+lastState+"|"+reason;
            if(key!=lastLog){lastLog=key;await log.WriteLineAsync(text);await log.FlushAsync();Console.WriteLine(phase+" · "+(lastState??"")+" · "+reason);}
        }
        async Task<bool> ProtectionReady()
        {
            try
            {
                Verify();using var registry=SharedJson(Path.Combine(root,"active-ship-protection.json"));var r=registry.RootElement;
                if(r.GetProperty("gamePid").GetInt32()!=pid||r.GetProperty("gameStartedUtcTicks").GetInt64()!=started||r.GetProperty("observerPid").GetInt32()!=observerPid||r.GetProperty("observerStartedUtcTicks").GetInt64()!=observerStarted)throw new InvalidDataException("Independent protection registry changed.");
                var witness=Path.GetFullPath(r.GetProperty("report").GetString()!)+".ready.json";
                if(Path.GetDirectoryName(witness)!=root)throw new InvalidDataException("Independent witness moved outside the work directory.");
                using var document=SharedJson(witness);var w=document.RootElement;var now=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();var at=w.GetProperty("healthAtUnixMilliseconds").GetInt64();
                if(!w.GetProperty("ready").GetBoolean()||w.GetProperty("gamePid").GetInt32()!=pid||w.GetProperty("gameStartedUtcTicks").GetInt64()!=started
                    ||w.GetProperty("observerPid").GetInt32()!=observerPid||w.GetProperty("observerStartedUtcTicks").GetInt64()!=observerStarted||w.GetProperty("shipId").GetInt64()!=5||at>now+50||now-at>500)return false;
                var health=await pipe.CallAsync(new{op="health.read"},token);var h=health.GetProperty("shipHealth");
                return health.GetProperty("localPlayerShipVerified").GetBoolean()&&h.GetProperty("verified").GetBoolean()&&h.GetProperty("hullFraction").GetDouble()>=.95
                    &&DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()-h.GetProperty("observedAtUnixMilliseconds").GetInt64()<=500;
            }
            catch(Exception error)when(error is IOException or JsonException or KeyNotFoundException
                ||error is InvalidOperationException&&error.Message.StartsWith("Native main-ship health unavailable",StringComparison.Ordinal)){return false;}
        }
        async Task EnterFromMenu()
        {
            var channel=new NativeRelogChannel(pid,NativeRelogChannel.SupportedGameSha256,cancellation=>{cancellation.ThrowIfCancellationRequested();Verify();return Task.CompletedTask;},entryOnly:true);
            var owner=Guid.NewGuid().ToString("N");string? pending=null,pendingState=null,lastIssuedState=null;var issued=DateTimeOffset.MinValue;var actions=new List<string>();
            try
            {
                await channel.BeginAsync(owner,token);
                while(true)
                {
                    token.ThrowIfCancellationRequested();Verify();var frame=await channel.ReadAsync(owner,token);lastState=frame.State;
                    native=await pipe.CallAsync(new{op="session.status"},token);reads++;await State();
                    if(frame.Stable&&frame.State=="InGame"&&IdentityReady())break;
                    if(frame.State=="FailedToConnect")throw new IOException("Original game connection failed during same-process entry.");
                    if(DateTimeOffset.UtcNow-frame.ObservedAt>TimeSpan.FromMilliseconds(350)||frame.ObservedAt>DateTimeOffset.UtcNow.AddMilliseconds(100)){await Task.Delay(100,token);continue;}
                    if(pending is not null)
                    {
                        if(frame.LastRequestId==pending&&(frame.ActionApplied||frame.ActionFailed)){if(frame.ActionFailed)lastIssuedState=null;pending=null;}
                        else if(frame.State!=pendingState){pending=null;}
                        else if(DateTimeOffset.UtcNow-issued>TimeSpan.FromSeconds(3))throw new IOException("Original entry acknowledgement remains uncertain; no duplicate menu input sent.");
                    }
                    if(pending is null)
                    {
                        var action=NativeRelogWorkflow.ChooseAction(frame,"Solo",reachedMainMenu:true);
                        if(action is not null&&lastIssuedState!=frame.State)
                        {
                            pending=Guid.NewGuid().ToString("N");pendingState=frame.State;lastIssuedState=frame.State;issued=DateTimeOffset.UtcNow;
                            try{await channel.SubmitAsync(owner,pending,frame,action,token);actions.Add(action);}
                            catch(NativeSessionStateChangedException){pending=null;lastIssuedState=null;}
                            catch(IOException){}catch(OperationCanceledException)when(!token.IsCancellationRequested){}
                        }
                    }
                    await Task.Delay(100,token);
                }
                await File.WriteAllTextAsync(report+".same-process-entry.json",JsonSerializer.Serialize(new{passed=true,pid,started,actions,at=DateTimeOffset.UtcNow,newClient=false,originalEntryFailurePreserved=true},ToolboxSettings.JsonOptions),token);
            }
            finally{await channel.EndAsync(owner);}
        }
        Verify();await State(true);
        var sentinel=Task.Run(async()=>
        {
            while(!token.IsCancellationRequested)
            {
                try
                {
                    if(File.Exists(stopFile)){reason="Explicit test stop file detected.";stop.Cancel();return;}
                    Verify();await Task.Delay(250,token);
                }
                catch(OperationCanceledException)when(token.IsCancellationRequested){return;}
                catch(Exception error){reason=error.Message;stop.Cancel();return;}
            }
        });
        try
        {
            while(true)
            {
                token.ThrowIfCancellationRequested();Verify();
                try
                {
                    native=await pipe.CallAsync(new{op="session.status"},token);reads++;var state=native.Value.GetProperty("state").GetString()!;var next=native.Value.GetProperty("nextState").GetString()!;
                    var stateChanged=state!=lastState;lastState=state;
                    if(state=="FailedToConnect"){phase="original-connection-failed";reason="Original game reported FailedToConnect; no forced state transition sent.";await State(true);throw new IOException(reason);}
                    if(state==next&&state is "MainMenu" or "ContinueMenu"&&!attemptedEntry)
                    {attemptedEntry=true;phase="same-process-entry-only";await State(true);await EnterFromMenu();phase="waiting-independent-protection";}
                    if(state=="InGame"&&state==next&&IdentityReady()&&await ProtectionReady())
                    {
                        phase="testing-normal-space-075-zero";testStarted=true;reason=null;await State(true);
                        await DockingFixtureAcceptance.RunAsync(pid,fixtureReport,stopFile,token);
                        using var fixture=SharedJson(fixtureReport);passed=fixture.RootElement.GetProperty("passed").GetBoolean();phase=passed==true?"passed":"fixture-failed";await State(true);break;
                    }
                    await State(stateChanged);
                }
                catch(Exception error)when(!token.IsCancellationRequested&&!testStarted&&phase is "waiting-original-session" or "waiting-independent-protection"
                    &&error is OperationCanceledException or IOException)
                {reason="Waiting for original session/health: "+error.Message;await State();}
                await Task.Delay(1000,token);
            }
        }
        catch(OperationCanceledException)when(token.IsCancellationRequested){phase="stopped";reason??="Explicit cancellation.";await State(true);}
        catch(Exception error){phase=testStarted?"fixture-failed":"waiting-failed";reason=error.ToString();await State(true);throw;}
        finally{stop.Cancel();await sentinel;}
    }
}