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;

internal static class FsdCalibration
{
    public static async Task<bool> RunAsync(int pid,string profilePath,string reportPath,string mode)
    {
        if(mode is not ("cancel" or "timeout"))throw new ArgumentException("Expected cancel or timeout calibration.");
        if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new calibration evidence path.");
        Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(reportPath))!);
        using var profile=JsonDocument.Parse(await File.ReadAllTextAsync(profilePath));
        var image=Path.GetFullPath(profile.RootElement.GetProperty("image").GetString()!);
        var hash=profile.RootElement.GetProperty("sha256").GetString();
        await using(var stream=File.OpenRead(image))
            if(Convert.ToHexStringLower(await SHA256.HashDataAsync(stream))!=hash)throw new InvalidDataException("Game build changed.");
        using var game=Process.GetProcessById(pid);
        if(!string.Equals(game.MainModule?.FileName,image,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("Selected game process changed.");
        var started=game.StartTime.ToUniversalTime().Ticks;
        var pipe=new NavigationPipeClient(pid);var owner=Guid.NewGuid().ToString("N");
        await using var events=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
        var clock=Stopwatch.StartNew();var frames=0;var peak=0.0;var sawCharging=false;var sawFilled=false;
        var stopped=false;var released=false;var unexpectedJump=false;string? failure=null;double? deadlineToCancel=null;
        double? chargingAt=null,filledAt=null,stopRenewAt=null,cancelAt=null;var commands=new List<object>();
        async Task Log(object row){await events.WriteLineAsync(JsonSerializer.Serialize(row));await events.FlushAsync();}
        async Task<JsonElement> Read()
        {
            using var current=Process.GetProcessById(pid);
            if(current.StartTime.ToUniversalTime().Ticks!=started||!string.Equals(current.MainModule?.FileName,image,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("Game process identity changed.");
            var frame=await pipe.CallAsync(new{op="read"});
            if(!frame.GetProperty("inGame").GetBoolean())throw new InvalidOperationException("The ship is outside the active game.");
            peak=Math.Max(peak,frame.GetProperty("heatPercent").GetDouble());
            return frame;
        }
        async Task Command(string desired,JsonElement frame)
        {
            var result=await pipe.CallAsync(new{op="calibrate.fsd",owner,sequence=frame.GetProperty("sequence").GetInt64(),ttlMilliseconds=180,desired,heatCeilingPercent=90});
            var row=new{kind="command",seconds=clock.Elapsed.TotalSeconds,desired,result};commands.Add(row);await Log(row);
        }
        try
        {
            var description=await pipe.CallAsync(new{op="describe"});
            if(description.GetProperty("processId").GetInt32()!=pid||description.GetProperty("gameSha256").GetString()!=hash||description.GetProperty("version").GetString()!="0.3.1-dev")throw new InvalidDataException("Native calibration protocol mismatch.");
            await Log(new{kind="description",description});
            var statusPath=Path.Combine(new ToolboxSettings().JournalDirectory,"Status.json");
            using var status=JsonDocument.Parse(await File.ReadAllTextAsync(statusPath));
            var flags=status.RootElement.GetProperty("Flags").GetUInt64();
            if((flags&(1UL|16UL|1048576UL|2097152UL))!=0||status.RootElement.GetProperty("GuiFocus").GetInt32()!=0)throw new InvalidOperationException("Calibration requires a stationary, undocked ship in normal space without danger or UI focus.");
            var initial=await Read();
            if(initial.GetProperty("speedMetresPerSecond").GetDouble()>=1||initial.GetProperty("heatPercent").GetDouble()>=60)throw new InvalidOperationException("The ship is moving or too hot for this calibration.");
            var before=await pipe.CallAsync(new{op="fsd.describe"});
            if(!before.GetProperty("fresh").GetBoolean()||before.GetProperty("state").GetInt32()!=2)throw new InvalidOperationException("Native FSD telemetry is not fresh and idle.");
            await Command("beginSupercruise",initial);
            while(clock.Elapsed.TotalSeconds<35)
            {
                var frame=await Read();var fsd=await pipe.CallAsync(new{op="fsd.describe"});var elapsed=clock.Elapsed.TotalSeconds;
                frames++;await Log(new{kind="sample",seconds=elapsed,frame,fsd});
                var state=fsd.GetProperty("state").GetInt32();var owned=fsd.GetProperty("ownedCharging").GetBoolean();
                if(fsd.GetProperty("beginRejected").GetBoolean())throw new InvalidOperationException("The original game FSD eligibility checks rejected the input.");
                if(state==4){unexpectedJump=true;throw new InvalidOperationException("FSD entered a jump before the holding test was complete.");}
                if(state==3){sawCharging=true;chargingAt??=elapsed;}
                var full=state==3&&fsd.GetProperty("duration").GetDouble()>0&&fsd.GetProperty("clock").GetDouble()>=fsd.GetProperty("duration").GetDouble();
                if(full){sawFilled=true;filledAt??=elapsed;}
                var stopWanted=mode=="timeout"&&chargingAt is {} at&&elapsed-at>=1||mode=="cancel"&&filledAt is {} filled&&elapsed-filled>=2;
                if(peak>=89)throw new InvalidOperationException("Calibration reached its heat bound; cancelling owned charging.");
                if(stopWanted&&cancelAt is null)
                {
                    cancelAt=elapsed;
                    if(mode=="cancel")await Command("cancel",frame);else stopRenewAt=elapsed;
                }
                if(cancelAt is {} cancelled&&!owned&&state!=3)
                {
                    stopped=true;deadlineToCancel=elapsed-cancelled;break;
                }
                if(cancelAt is null||mode=="cancel")await Command("keep",frame);
                if(cancelAt is {} timeout&&elapsed-timeout>2)throw new TimeoutException("Owned charging did not cancel within two seconds.");
                await Task.Delay(50);
            }
            if(!sawCharging||!stopped||mode=="cancel"&&!sawFilled)throw new InvalidOperationException("Charging/holding/cancellation acceptance was not reached.");
        }
        catch(Exception error){failure=error.ToString();}
        finally
        {
            var releaseDeadline=Stopwatch.StartNew();
            try
            {
                while(releaseDeadline.Elapsed<TimeSpan.FromSeconds(2))
                {
                    var result=await pipe.CallAsync(new{op="release",owner});await Log(new{kind="release",result});
                    if(result.GetProperty("released").GetBoolean()){released=true;break;}
                    await Task.Delay(50);
                }
                if(!released)failure=(failure??"")+" Native release was not confirmed.";
            }
            catch(Exception error){failure=(failure??"")+" Release: "+error.Message;}
        }
        var passed=failure is null&&sawCharging&&stopped&&released&&!unexpectedJump&&(mode=="timeout"||sawFilled);
        await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{mode,pid,gameSha256=hash,frames,maximumHeatPercent=peak,sawCharging,sawFilled,stopped,released,unexpectedJump,
            stopRenewAt,cancellationSeconds=deadlineToCancel,passed,failure,commands,screenUse=false,osInputSent=false,completeAutomaticNavigationAccepted=false},new JsonSerializerOptions{WriteIndented=true}));
        Console.WriteLine(JsonSerializer.Serialize(new{mode,passed,sawCharging,sawFilled,stopped,released,maximumHeatPercent=peak,cancellationSeconds=deadlineToCancel,failure,reportPath}));
        return passed;
    }
}