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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Collections.Concurrent;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Planning;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.NativeSupport;

internal static class TradePauseRecoveryAcceptance
{
    private sealed record WorkerStatus(DateTimeOffset At,string Message);
    // No automatic menu opener is inferred from the old relog capability. The
    // selected client must actually enter its original Pause during this window.
    internal static async Task RunAsync(int pid,string directory,int seconds=45,bool openVerifiedPause=false)
    {
        if(seconds is <10 or >60)throw new ArgumentOutOfRangeException(nameof(seconds));
        directory=Path.GetFullPath(directory);if(Directory.Exists(directory))throw new IOException("Use a new pause-acceptance evidence directory.");
        Directory.CreateDirectory(directory);
        try{await RunCoreAsync(pid,directory,seconds,openVerifiedPause);}
        catch(Exception error)
        {
            var result=Path.Combine(directory,"result.json");
            if(!File.Exists(result))await File.WriteAllTextAsync(result,JsonSerializer.Serialize(new{passed=false,pid,failure=error.ToString(),stage="before-running",openVerifiedPause,
                flightInputsSent=0,tradeOrdersSent=0,stationServiceMutationsSent=0,osInputsSent=0,gameTerminationRequested=false},ToolboxSettings.JsonOptions));
            throw;
        }
    }
    private static async Task RunCoreAsync(int pid,string directory,int seconds,bool openVerifiedPause)
    {
        using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(seconds));var token=deadline.Token;
        var settingsDirectory=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox");
        var settings=ToolboxSettings.Load(Path.Combine(settingsDirectory,"settings.json"));
        await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync(token);
        using var game=Process.GetProcessById(pid);var gameStarted=game.StartTime.ToUniversalTime().Ticks;var gamePath=game.MainModule?.FileName;
        if(game.ProcessName!="EliteDangerous64"||string.IsNullOrWhiteSpace(gamePath))throw new InvalidDataException("Exact existing game identity required.");
        var pipe=new NavigationPipeClient(pid);var hash=NativeSystemLocationReader.GameSha256;
        var hello=await pipe.CallAsync(new{op="describe"},token);
        if(hello.GetProperty("processId").GetInt32()!=pid||hello.GetProperty("gameSha256").GetString()!=hash)throw new InvalidDataException("Build-pinned game identity required.");
        var initial=journal.State;
        if(!initial.InGame||!initial.Docked||initial.CurrentShipId is null||!initial.CargoReliable||initial.CargoCount!=0||initial.Credits is not >0||!initial.CreditsReliable)
            throw new InvalidOperationException("This non-mutation acceptance requires the actual empty healthy docked ship and known wallet.");
        var channel=new NativeCommerceChannel(pid);var initialInventory=await channel.CallAsync(new{op="commerce.inventory"},token);
        var initialRaw=await pipe.CallAsync(new{op="read"},token);
        if(initialRaw.GetProperty("shipHealth").GetProperty("emergency").GetBoolean())throw new InvalidOperationException("Existing health emergency was preserved; this is not a maintenance override.");
        var catalogue=new NativeCurrentSystemSource(pid,hash,()=>journal.State);using var catalogueLifetime=catalogue;
        var initialLocations=await catalogue.ReadLocationsAsync(token);
        var repository=new MarketRepository(Path.Combine(settingsDirectory,"markets.db"));
        var book=repository.LoadRegion(initial.MarketId!.Value,150,80);
        var range=initial.MaxJumpRange??initial.Ships.Single(value=>value.ShipId==initial.CurrentShipId).MaxJumpRange;
        if(range is not >0)throw new InvalidOperationException("Actual current ship jump range is not known.");
        var request=new PlannerRequest{StartMarketId=initial.MarketId.Value,Credits=initial.Credits.Value,ReserveCredits=Math.Max(initial.Rebuy,0),
            CargoCapacity=initial.CargoCapacity,OccupiedCargo=initial.CargoCount,LadenJumpRangeLy=range.Value,JumpRangeIsEstimate=true,
            CurrentPosition=initial.Position,CurrentMarketId=initial.MarketId,CurrentSystemName=initial.SystemName,MaxLegDistanceLy=150,SearchRadiusLy=150,
            MaxLegs=1,Loop=false,MaxMarketAgeMinutes=10080,MaxArrivalDistanceLs=200000,MinimumProfitPerTon=0,CostPerJump=0,BeamWidth=8,CargoSearchNodes=128};
        var plan=new TradeRoutePlanner().CalculateCandidates(book,request,1,token).SingleOrDefault()
            ??throw new InvalidOperationException("No actual feasible cached route is available; no synthetic trade intent was created.");
        await File.WriteAllTextAsync(Path.Combine(directory,"actual-intent-plan.json"),JsonSerializer.Serialize(plan,ToolboxSettings.JsonOptions),token);
        await using var protection=await FlightTestProtectionScope.ArmAsync(pid,automaticExit:false,exitForFailedHandoff:false,token:token);
        async Task Check(CancellationToken cancellation)
        {
            cancellation.ThrowIfCancellationRequested();game.Refresh();var current=journal.State;
            if(game.HasExited||game.StartTime.ToUniversalTime().Ticks!=gameStarted||game.MainModule?.FileName!=gamePath||protection.Stopped
                ||!current.InGame||!current.Docked||current.CurrentShipId!=initial.CurrentShipId||current.CommanderId!=initial.CommanderId
                ||current.LoadGameAt!=initial.LoadGameAt||current.MarketId!=initial.MarketId||current.CargoCount!=initial.CargoCount
                ||!current.CargoReliable||NavigationSafety.UnhealthyHull(current)||NavigationSafety.HasExternalThreat(current))
                throw new InvalidOperationException("Real game, docked ship or safety identity changed.");
            var health=await pipe.CallAsync(new{op="health.read"},cancellation);
            if(!health.GetProperty("shipHealth").GetProperty("verified").GetBoolean()||health.GetProperty("shipHealth").GetProperty("emergency").GetBoolean())
                throw new InvalidOperationException("Original current health guard refused production preflight.");
        }
        var workerStatuses=new ConcurrentQueue<WorkerStatus>();
        void LogStatus(string message){workerStatuses.Enqueue(new(DateTimeOffset.UtcNow,message));Console.WriteLine(message);}
        var navigation=new NativeTradeNavigation(pid,hash,()=>journal.State,NativeSupportPackage.Directory,()=>settings.NavigationFuelReserve,Check);
        navigation.Status+=LogStatus;
        Task ForbiddenTravel(Station station,Func<CancellationToken,Task> before,CancellationToken cancellation)
            =>throw new InvalidOperationException("Acceptance does not permit a travel call.");
        var production=new NativeTradeCommandDriver(channel,()=>journal.State,Path.Combine(directory,"operations"),ForbiddenTravel,Check)
            {JournalDirectory=settings.JournalDirectory,RunningMenuRecoveryFactory=navigation.EnterRunningMenuRecovery};
        production.Status+=LogStatus;
        var barrier=new TradePauseMutationBarrier(production,production);
        var runner=new TradeRunner(()=>journal.State,(market,key)=>book.Quotes.FirstOrDefault(value=>value.MarketId==market&&value.Commodity==key));
        var progress=new List<RunProgress>();var progressLock=new object();
        runner.Progress+=value=>{lock(progressLock)progress.Add(value);};journal.JournalEvent+=runner.Observe;journal.PlayerChanged+=runner.ObservePlayer;
        var observer=new NativeTradePauseRecoveryChannel(pid,hash,Path.Combine(NativeSupportPackage.Directory,"DockingControl"));
        await observer.PrepareAsync(token);
        var originalReceipt=await observer.ReadAsync(token);var originalRequestId=originalReceipt.GetProperty("tradePauseRecovery").GetProperty("requestId").GetString();
        using var frames=new StreamWriter(new FileStream(Path.Combine(directory,"frames.jsonl"),FileMode.CreateNew,FileAccess.Write,FileShare.Read)){AutoFlush=true};
        Task? running=null;string? failure=null;JsonElement? positive=null,paused=null,resumed=null,stopped=null,finalInventory=null,finalRaw=null;
        bool originalResumeConfirmed=false,pauseRevoked=false,resumeNewScope=false,stopRevoked=false,cargoUnchanged=false,walletUnchanged=false,targetUnchanged=false;
        string? runningOwner=null,openRequestId=null;var pauseOpenActionsSent=0;JsonElement? pauseOpenReceipt=null;
        try
        {
            running=runner.RunAsync(plan,ExecutionMode.NativeBridge,barrier,token);
            await Task.WhenAny(running,barrier.FirstMutationHeld).WaitAsync(TimeSpan.FromSeconds(10),token);
            if(runner.State!=RunState.Running||!barrier.FirstMutationHeld.IsCompleted)throw new InvalidOperationException("Production preflight did not reach a real Running mutation barrier.");
            Console.WriteLine("Real TradeRunner Running; all Buy/Sell/Travel/Service calls are blocked. "+(openVerifiedPause?"One explicit verified original Pause entry is authorized.":"Waiting for an actual original Pause; no menu-opening input is sent."));
            do
            {
                token.ThrowIfCancellationRequested();var ready=await observer.ReadAsync(token);var authorization=ready.GetProperty("tradePauseRecovery");
                await frames.WriteLineAsync(JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,stage="running-scope-ready",runnerState=runner.State,raw=ready}));
                if(authorization.GetProperty("scopeActive").GetBoolean()&&runner.State==RunState.Running&&barrier.BlockedBuys>0&&barrier.BlockedServices>0)
                {runningOwner=authorization.GetProperty("owner").GetString();break;}
                if(running.IsCompleted||protection.Stopped)throw new InvalidOperationException("Real runner or health protection ended before an original menu scope became active.");
                await Task.Delay(80,token);
            }while(true);
            if(openVerifiedPause)
            {
                // Live evidence: persistent075-0514j-production-01 recovery
                // opened Pause on this exact game SHA. Current native dispatch
                // revalidates flow/CSM and finds one actual graph state Pause.
                var opener=new NativeRelogChannel(pid,hash,returnToMenuTrial:true);var openOwner=Guid.NewGuid().ToString("N");
                openRequestId=Guid.NewGuid().ToString("N");
                try
                {
                    await opener.BeginAsync(openOwner,token);var frame=await opener.ReadAsync(openOwner,token);
                    if(runner.State!=RunState.Running||!frame.Stable||frame.State!="InGame"||DateTimeOffset.UtcNow-frame.ObservedAt>TimeSpan.FromMilliseconds(200))
                        throw new InvalidOperationException("One verified Pause entry requires the real Running current InGame scope.");
                    pauseOpenActionsSent++;await opener.SubmitAsync(openOwner,openRequestId,frame,"@pause",token);
                    var until=DateTimeOffset.UtcNow.AddSeconds(3);
                    do
                    {
                        var observed=await opener.ReadAsync(openOwner,token);
                        if(observed.LastRequestId==openRequestId&&observed.ActionApplied)
                        {pauseOpenReceipt=JsonSerializer.SerializeToElement(observed,ToolboxSettings.JsonOptions);break;}
                        if(observed.LastRequestId==openRequestId&&observed.ActionFailed)throw new InvalidOperationException("The original unique Pause graph action refused entry; it was not retried.");
                        if(DateTimeOffset.UtcNow>=until)throw new TimeoutException("Original Pause entry was not acknowledged; it was not retried.");
                        await Task.Delay(30,token);
                    }while(true);
                }
                finally{await opener.EndAsync(openOwner);}
            }
            while(true)
            {
                token.ThrowIfCancellationRequested();if(runner.State!=RunState.Running||protection.Stopped)throw new InvalidOperationException("The real trade runner or actual health protection stopped before pause recovery.");
                var current=journal.State;
                if(!current.InGame||!current.Docked||current.CommanderId!=initial.CommanderId||current.CurrentShipId!=initial.CurrentShipId
                    ||current.LoadGameAt!=initial.LoadGameAt||current.MarketId!=initial.MarketId||!current.CargoReliable||current.CargoCount!=initial.CargoCount
                    ||NavigationSafety.HasExternalThreat(current)||NavigationSafety.UnhealthyHull(current))throw new InvalidOperationException("The real docked session changed while observing Pause.");
                var value=await observer.ReadAsync(token);await frames.WriteLineAsync(JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,runnerState=runner.State,raw=value}));
                var receipt=value.GetProperty("tradePauseRecovery");
                if(receipt.GetProperty("applied").GetBoolean()&&receipt.GetProperty("requestId").GetString() is {Length:32}
                    &&receipt.GetProperty("requestId").GetString()!=originalRequestId&&receipt.GetProperty("owner").GetString()==runningOwner
                    &&receipt.GetProperty("scopeActive").GetBoolean()
                    &&value.GetProperty("state").GetString()=="InGame"&&value.GetProperty("nextState").GetString()=="InGame")
                {positive=value;originalResumeConfirmed=true;break;}
                await Task.Delay(80,token);
            }
            var owner=positive.Value.GetProperty("tradePauseRecovery").GetProperty("owner").GetString();
            runner.Pause();paused=await observer.ReadAsync(token);pauseRevoked=!paused.Value.GetProperty("tradePauseRecovery").GetProperty("scopeActive").GetBoolean();
            if(!pauseRevoked)throw new InvalidOperationException("User Pause did not immediately revoke the original menu scope.");
            await Task.Delay(350,token);var pausedAgain=await observer.ReadAsync(token);
            if(runner.State!=RunState.Paused||pausedAgain.GetProperty("tradePauseRecovery").GetProperty("scopeActive").GetBoolean())throw new InvalidOperationException("A delayed Running report reauthorized a paused trade run.");
            runner.Resume();
            do
            {
                token.ThrowIfCancellationRequested();resumed=await observer.ReadAsync(token);var scope=resumed.Value.GetProperty("tradePauseRecovery");
                if(scope.GetProperty("scopeActive").GetBoolean()&&scope.GetProperty("owner").GetString()!=owner){resumeNewScope=true;break;}
                await Task.Delay(80,token);
            }while(true);
            runner.Stop();await running.WaitAsync(TimeSpan.FromSeconds(4));stopped=await observer.ReadAsync(CancellationToken.None);
            stopRevoked=runner.State==RunState.Stopped&&!stopped.Value.GetProperty("tradePauseRecovery").GetProperty("scopeActive").GetBoolean();
            if(!stopRevoked)throw new InvalidOperationException("Stop did not revoke the real Running scope.");
            finalInventory=await channel.CallAsync(new{op="commerce.inventory"},CancellationToken.None);finalRaw=await pipe.CallAsync(new{op="read"});
            var finalLocations=await catalogue.ReadLocationsAsync();
            cargoUnchanged=initialInventory.GetProperty("count").GetInt32()==finalInventory.Value.GetProperty("count").GetInt32()
                &&journal.State.CargoReliable&&journal.State.CargoCount==initial.CargoCount&&journal.State.Inventory.SequenceEqual(initial.Inventory);
            walletUnchanged=initialInventory.GetProperty("credits").GetInt64()==finalInventory.Value.GetProperty("credits").GetInt64();
            targetUnchanged=initialLocations.Locations.Where(value=>value.Selected).Select(value=>value.Identity).Order().SequenceEqual(finalLocations.Locations.Where(value=>value.Selected).Select(value=>value.Identity).Order());
            if(!cargoUnchanged||!walletUnchanged||!targetUnchanged||finalRaw.Value.GetProperty("controlApplied").GetBoolean()
                ||finalRaw.Value.GetProperty("shipHealth").GetProperty("emergency").GetBoolean()||finalRaw.Value.GetProperty("shipHealth").GetProperty("damageObserved").GetBoolean())
                throw new InvalidOperationException("Final cargo, wallet, target, health or released control did not match.");
        }
        catch(Exception error){failure=error.ToString();}
        finally
        {
            runner.Stop();if(running is not null)try{await running.WaitAsync(TimeSpan.FromSeconds(4));}catch(Exception error){failure??=error.ToString();}
            journal.JournalEvent-=runner.Observe;journal.PlayerChanged-=runner.ObservePlayer;
            await File.WriteAllTextAsync(Path.Combine(directory,"result.json"),JsonSerializer.Serialize(new{passed=failure is null&&originalResumeConfirmed&&pauseRevoked&&resumeNewScope&&stopRevoked&&cargoUnchanged&&walletUnchanged&&targetUnchanged,
                failure,pid,gameStarted,gamePath,initialShip=initial.CurrentShipId,initial.CargoCount,initial.CargoCapacity,initialInventory,finalInventory,
                originalResumeConfirmed,pauseRevoked,resumeNewScope,stopRevoked,cargoUnchanged,walletUnchanged,targetUnchanged,
                positive,paused,resumed,stopped,finalRaw,progress,workerStatuses=workerStatuses.ToArray(),barrier.BlockedBuys,barrier.BlockedSells,barrier.BlockedTravels,barrier.BlockedServices,
                tradeOrdersSent=TradePauseMutationBarrier.ForwardedMutations,flightInputsSent=0,stationServiceMutationsSent=0,pauseOpenActionsSent,openVerifiedPause,openRequestId,pauseOpenReceipt,
                gameExited=game.HasExited,gameTerminationRequested=false,screenUse=false,osInputsSent=0,limitations="One safe docked ship and exact original Pause only. No financial, travel, service or outside-Running menu-resume claim."},ToolboxSettings.JsonOptions));
        }
        if(failure is not null)throw new InvalidOperationException("Bounded zero-mutation pause acceptance did not pass; see result.json.");
    }
}