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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;

/// <summary>A bounded, explicitly started one-hop acceptance. It never claims complete navigation acceptance.</summary>
internal static class HyperspaceAcceptance
{
    private static IEnumerable<JsonElement> JournalEvents(string path)
    {
        using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
        using var reader=new StreamReader(stream);
        while(reader.ReadLine() is {} line)
        {
            // A writer may be appending the last event; use only complete JSON objects.
            if(!line.TrimEnd().EndsWith('}'))continue;
            using var document=JsonDocument.Parse(line);yield return document.RootElement.Clone();
        }
    }
    public static async Task<bool> RunAsync(int pid,string profilePath,string reportPath,string mode,string? explicitDestination=null)
    {
        if(mode is not ("hold" or "jump"))throw new ArgumentException("Expected hold or jump.");
        if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new evidence path.");
        using var profile=JsonDocument.Parse(await File.ReadAllTextAsync(profilePath));
        var image=profile.RootElement.GetProperty("image").GetString()!;
        var hash=profile.RootElement.GetProperty("sha256").GetString();
        await using(var file=File.OpenRead(image))if(Convert.ToHexStringLower(await SHA256.HashDataAsync(file))!=hash)throw new InvalidDataException("Game build changed.");
        using var game=Process.GetProcessById(pid);var started=game.StartTime.ToUniversalTime().Ticks;
        if(!string.Equals(game.MainModule?.FileName,image,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("Selected game changed.");
        var settingsPath=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json");
        var settings=JsonSerializer.Deserialize<ToolboxSettings>(await File.ReadAllTextAsync(settingsPath),ToolboxSettings.JsonOptions)!;
        var destination=explicitDestination??settings.NavigationDestination;
        if(string.IsNullOrWhiteSpace(destination))throw new InvalidOperationException("A saved navigation destination is required.");
        var journal=Directory.GetFiles(settings.JournalDirectory,"Journal.*.log").OrderBy(File.GetLastWriteTimeUtc).Last();
        var startedAt=DateTimeOffset.UtcNow;var journalOffset=new FileInfo(journal).Length;
        using var status=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(settings.JournalDirectory,"Status.json")));
        var flags=status.RootElement.GetProperty("Flags").GetUInt64();
        var supercruise=(flags&16)!=0;
        if((flags&(1UL|1048576UL|2097152UL))!=0||status.RootElement.GetProperty("GuiFocus").GetInt32()!=0||mode=="hold"&&supercruise)throw new InvalidOperationException("Requires undocked controllable space without danger or UI focus.");
        var pipe=new NavigationPipeClient(pid);var owner=Guid.NewGuid().ToString("N");var clock=Stopwatch.StartNew();
        await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
        async Task Log(object row){await log.WriteLineAsync(JsonSerializer.Serialize(row));await log.FlushAsync();}
        static Vector3d V(JsonElement value,string key){var a=value.GetProperty(key);return new(a[0].GetDouble(),a[1].GetDouble(),a[2].GetDouble());}
        var frames=0;var peak=0.0;var sawCharging=false;var sawFull=false;var sawJump=false;var fullThrottleBeforeJump=false;var commitRequested=false;var chargeRequested=false;var released=false;var arrived=false;
        double? filledAt=null,alignedAt=null,jumpAt=null,invalidTargetAt=null,telemetryGapAt=null;string? failure=null;ulong target=0;JsonElement? arrival=null;
        async Task Axes(JsonElement frame,AttitudeInput aim,double throttle)
        {
            // Holding at rest does not need a throttle override; retain the game's ordinary input mode.
            if(mode=="hold")throttle=-1;
            await pipe.CallAsync(new{op="calibrate.axes",owner,sequence=frame.GetProperty("sequence").GetInt64(),ttlMilliseconds=250,throttle,
                axes=new[]{0.0,0,0,aim.Yaw,aim.Pitch,aim.Roll}});
        }
        async Task Fsd(JsonElement frame,string desired)
        {
            await pipe.CallAsync(new{op="calibrate.fsd",owner,sequence=frame.GetProperty("sequence").GetInt64(),ttlMilliseconds=250,desired,targetSystemAddress=target,heatCeilingPercent=90});
        }
        async Task Release()
        {
            var stop=Stopwatch.StartNew();
            while(stop.Elapsed.TotalSeconds<3)
            {
                var result=await pipe.CallAsync(new{op="release",owner});await Log(new{kind="release",seconds=clock.Elapsed.TotalSeconds,result});
                if(result.GetProperty("released").GetBoolean()){released=true;return;}
                await Task.Delay(40);
            }
            throw new TimeoutException("Control release was not acknowledged.");
        }
        async Task PauseTelemetry(string reason)
        {
            telemetryGapAt??=clock.Elapsed.TotalSeconds;
            await Log(new{kind="telemetry-pause",seconds=clock.Elapsed.TotalSeconds,reason});
            await Release();
            if(clock.Elapsed.TotalSeconds-telemetryGapAt>2)throw new InvalidOperationException("Flight telemetry remained unavailable for more than two seconds.");
            await Task.Delay(50);
        }
        try
        {
            var hello=await pipe.CallAsync(new{op="describe"});
            if(hello.GetProperty("version").GetString()!="0.3.1-dev"||hello.GetProperty("gameSha256").GetString()!=hash||hello.GetProperty("processId").GetInt32()!=pid)throw new InvalidDataException("Native acceptance protocol mismatch.");
            var initial=await pipe.CallAsync(new{op="read"});var fsd=await pipe.CallAsync(new{op="fsd.describe"});
            if(!initial.GetProperty("localPlayerShipVerified").GetBoolean()||fsd.GetProperty("entityId").GetString()!=initial.GetProperty("entityId").GetString())throw new InvalidDataException("Pose and FSD must belong to the original game's local PlayerShip reference.");
            if(!supercruise&&initial.GetProperty("speedMetresPerSecond").GetDouble()>=1||initial.GetProperty("heatPercent").GetDouble()>=(supercruise?82:60)||fsd.GetProperty("state").GetInt32()!=2||!fsd.GetProperty("fresh").GetBoolean()||!fsd.GetProperty("targetValid").GetBoolean())throw new InvalidOperationException("Ship/target/FSD initial conditions are not ready.");
            target=ulong.Parse(fsd.GetProperty("targetAddress").GetString()!);
            JsonElement? locked=null;
            foreach(var row in JournalEvents(journal))
            {
                if(row.GetProperty("event").GetString()=="FSDTarget")locked=row;
            }
            if(locked is not {} selected||selected.GetProperty("SystemAddress").GetUInt64()!=target||!string.Equals(selected.GetProperty("Name").GetString(),destination,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("The actual game target does not match the saved destination.");
            var geometry=await pipe.CallAsync(new{op="geometry.describe"});
            var position=V(initial,"position");var ray=V(fsd,"targetDirection").Unit();var along=Vector3d.Dot(position,ray);
            var clearance=along<0?Math.Sqrt(Math.Max(0,position.Length*position.Length-along*along)):position.Length;
            if(!geometry.GetProperty("fresh").GetBoolean()||geometry.GetProperty("bodyKind").GetInt32()!=1||clearance<=geometry.GetProperty("escapeRadius").GetDouble()*1.2)throw new InvalidOperationException("This one-hop test requires an already unobscured stellar departure.");
            if(supercruise&&Vector3d.Dot(position.Unit(),ray)<.95)throw new InvalidOperationException("This bounded SC acceptance only permits an outward departure.");
            var escapeRadius=geometry.GetProperty("escapeRadius").GetDouble();
            await Log(new{kind="preflight",mode,supercruise,startedAt,pid,target,destination,locked,initial,fsd,geometry,clearance});
            // An explicit new run acquires its own lease; current physical input still cancels it natively.
            await Axes(initial,default,mode=="hold"?-1:0);await Task.Delay(50);
            var phase=mode=="jump"?"prealign":"charge";
            while(clock.Elapsed.TotalSeconds<300)
            {
                try
                {
                using var current=Process.GetProcessById(pid);
                if(current.StartTime.ToUniversalTime().Ticks!=started)throw new InvalidOperationException("Process identity changed.");
                if(sawJump)
                {
                    foreach(var e in JournalEvents(journal))
                    {
                        if(e.GetProperty("event").GetString()=="FSDJump"&&e.GetProperty("timestamp").GetDateTimeOffset()>=startedAt.AddSeconds(-1)&&e.GetProperty("SystemAddress").GetUInt64()==target){arrival=e.Clone();arrived=true;break;}
                    }
                    if(arrived)break;
                    if(clock.Elapsed.TotalSeconds-jumpAt>75)throw new TimeoutException("No actual Journal FSDJump arrival after native jump.");
                    await Task.Delay(200);continue;
                }
                JsonElement frame;
                try{frame=await pipe.CallAsync(new{op="read"});fsd=await pipe.CallAsync(new{op="fsd.describe"});}
                catch(InvalidOperationException error)when(error.Message=="Native player telemetry unavailable or stale")
                {await PauseTelemetry(error.Message);continue;}
                frames++;
                peak=Math.Max(peak,frame.GetProperty("heatPercent").GetDouble());
                var state=fsd.GetProperty("state").GetInt32();var charge=fsd.GetProperty("clock").GetDouble();var duration=fsd.GetProperty("duration").GetDouble();
                if(state==4)
                {
                    sawFull|=duration>0&&charge>=duration;
                    await Log(new{kind="commit-observed",seconds=clock.Elapsed.TotalSeconds,frame,fsd});
                    fullThrottleBeforeJump|=commitRequested&&fsd.GetProperty("throttle").GetDouble()>=.999;
                    if(mode!="jump"||!fullThrottleBeforeJump)failure="Unexpected jump before verified full throttle.";
                    sawJump=true;jumpAt=clock.Elapsed.TotalSeconds;await Release();continue;
                }
                if(frame.GetProperty("playerOverride").GetBoolean())
                {await Log(new{kind="guard-observation",seconds=clock.Elapsed.TotalSeconds,frame,fsd});throw new InvalidOperationException("Flight state became stale or was overridden.");}
                if(!frame.GetProperty("inGame").GetBoolean()||!fsd.GetProperty("fresh").GetBoolean())
                {await Log(new{kind="transient-observation",seconds=clock.Elapsed.TotalSeconds,frame,fsd});await PauseTelemetry("Current ship or FSD sample is temporarily unavailable");continue;}
                telemetryGapAt=null;
                var observedTarget=ulong.Parse(fsd.GetProperty("targetAddress").GetString()!);
                if(observedTarget!=0&&observedTarget!=target)
                {await Log(new{kind="target-changed",seconds=clock.Elapsed.TotalSeconds,frame,fsd});throw new InvalidOperationException("The locked destination changed.");}
                if(!fsd.GetProperty("targetValid").GetBoolean())
                {
                    invalidTargetAt??=clock.Elapsed.TotalSeconds;
                    await Log(new{kind="target-temporarily-unavailable",seconds=clock.Elapsed.TotalSeconds,frame,fsd});
                    await Release();
                    if(clock.Elapsed.TotalSeconds-invalidTargetAt>2)throw new InvalidOperationException("The destination telemetry remained unavailable.");
                    alignedAt=null;await Task.Delay(50);continue;
                }
                invalidTargetAt=null;
                if(released){released=false;if(state==2&&chargeRequested){sawCharging=false;chargeRequested=false;filledAt=null;sawFull=false;phase=frame.GetProperty("heatPercent").GetDouble()>=60?"cool":"prealign";}}
                var aim=AttitudeController.Aim(V(frame,"forward"),V(frame,"up"),V(fsd,"targetDirection"),.6);
                await Log(new{kind="sample",seconds=clock.Elapsed.TotalSeconds,phase,aim,frame,fsd});
                if(peak>=89.5)throw new InvalidOperationException("Reached the bounded thermal ceiling; cancelling charging.");
                if(phase=="cool")
                {
                    await Axes(frame,default,0);
                    if(frame.GetProperty("heatPercent").GetDouble()<55)phase="prealign";
                }
                else if(phase=="prealign")
                {
                    await Axes(frame,aim,0);
                    if(aim.ErrorDegrees<=8)phase=supercruise&&frame.GetProperty("heatPercent").GetDouble()>=55?"escape":"charge-start";
                }
                else if(phase=="escape")
                {
                    await Axes(frame,default,1);
                    if(frame.GetProperty("heatPercent").GetDouble()<55&&V(frame,"position").Length>=escapeRadius*3)phase="charge-start";
                }
                else if(phase=="charge-start")
                {
                    // Full throttle and charge are requested together. Current speed need not
                    // have reached its maximum; the original FSD still enforces its own speed check.
                    await Axes(frame,default,1);
                    phase="charge";await Fsd(frame,"beginHyperspace");chargeRequested=true;
                    await Log(new{kind="full-throttle-and-charge-requested",seconds=clock.Elapsed.TotalSeconds,frame,fsd});
                }
                else
                {
                    if(mode=="jump"&&fsd.GetProperty("throttle").GetDouble()>=.999)
                    {
                        fullThrottleBeforeJump=true;
                    }
                    if(!sawCharging&&state==2)
                    {
                        if(!chargeRequested){await Fsd(frame,"beginHyperspace");chargeRequested=true;}
                        else if(fsd.GetProperty("beginRejected").GetBoolean())throw new InvalidOperationException("Original game eligibility rejected hyperspace.");
                    }
                    if(state==3){sawCharging=true;}
                    if(sawCharging&&state==2)throw new InvalidOperationException("Original charging stopped before acceptance.");
                    if(state==3&&duration>0&&charge>=duration){sawFull=true;filledAt??=clock.Elapsed.TotalSeconds;}
                    if(mode=="hold")
                    {
                        await Axes(frame,default,0);
                        if(filledAt is {} filled&&clock.Elapsed.TotalSeconds-filled>=1){await Fsd(frame,"cancel");await Release();break;}
                    }
                    else
                    {
                        var late=state==3&&duration>0&&charge>=duration-4;
                        if(late&&aim.ErrorDegrees<.7)alignedAt??=clock.Elapsed.TotalSeconds;else alignedAt=null;
                        var commit=sawFull&&alignedAt is {} aligned&&clock.Elapsed.TotalSeconds-aligned>=.3;
                        await Axes(frame,late?aim:default,1);
                        commitRequested|=commit;
                        if(commit&&fsd.GetProperty("throttle").GetDouble()>=.999)fullThrottleBeforeJump=true;
                    }
                    if(sawCharging)await Fsd(frame,"keep");
                }
                await Task.Delay(40);
                }
                catch(InvalidOperationException error)when(error.Message=="Navigation reference frame expired; read fresh telemetry and retry")
                {await PauseTelemetry(error.Message);}
            }
            if(!sawCharging||!sawFull||mode=="jump"&&(!arrived||!fullThrottleBeforeJump))throw new InvalidOperationException("Requested hyperspace acceptance did not complete.");
        }
        catch(Exception error){failure=error.ToString();}
        finally
        {
            if(!released)try{await Release();}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;}
        }
        var passed=failure is null&&released&&sawCharging&&sawFull&&(mode=="hold"?!sawJump:arrived&&fullThrottleBeforeJump);
        await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{mode,pid,hash,target,destination,startedAt,journal,journalOffset,frames,peak,sawCharging,sawFull,sawJump,fullThrottleBeforeJump,released,arrived,arrival,passed,failure,screenUse=false,osInputSent=false,completeAutomaticNavigationAccepted=false},new JsonSerializerOptions{WriteIndented=true}));
        Console.WriteLine(JsonSerializer.Serialize(new{mode,passed,target,destination,frames,peak,sawCharging,sawFull,sawJump,fullThrottleBeforeJump,released,arrived,failure}));
        return passed;
    }
}