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>Bounded SC orbit/scoop experiment. Does not send FSD input or certify complete navigation.</summary>
internal static class StarOrbitAcceptance
{
    private static JsonDocument ReadShared(string path)
    {
        for(var attempt=0;;attempt++)
        {
            try
            {
                using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
                return JsonDocument.Parse(stream);
            }
            catch(Exception error)when(attempt<5&&error is IOException or JsonException)
            {Thread.Sleep(10);}
        }
    }
    private static IEnumerable<JsonElement> Events(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)
        {
            if(!line.TrimEnd().EndsWith('}'))continue;
            using var document=JsonDocument.Parse(line);yield return document.RootElement.Clone();
        }
    }
    private static Vector3d V(JsonElement row,string name){var v=row.GetProperty(name);return new(v[0].GetDouble(),v[1].GetDouble(),v[2].GetDouble());}
    public static async Task<bool> RunAsync(int pid,string profilePath,string reportPath)
    {
        if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new evidence path.");
        using var profile=ReadShared(profilePath);var image=profile.RootElement.GetProperty("image").GetString()!;
        var hash=profile.RootElement.GetProperty("sha256").GetString();
        await using(var input=File.OpenRead(image))if(Convert.ToHexStringLower(await SHA256.HashDataAsync(input))!=hash)throw new InvalidDataException("Build changed.");
        using var game=Process.GetProcessById(pid);var processStart=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");
        using var settings=ReadShared(settingsPath);var journalDirectory=settings.RootElement.GetProperty("journalDirectory").GetString()!;
        var journal=Directory.GetFiles(journalDirectory,"Journal.*.log").OrderBy(File.GetLastWriteTimeUtc).Last();
        var statusPath=Path.Combine(journalDirectory,"Status.json");
        var start=DateTimeOffset.UtcNow;var clock=Stopwatch.StartNew();var owner=Guid.NewGuid().ToString("N");var pipe=new NavigationPipeClient(pid);
        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();}
        var frames=0;var peak=0d;var minimumRadius=double.PositiveInfinity;var rotated=0d;var released=false;var cleared=false;var scooped=0d;var journalScooped=0d;var fuelGain=0d;var initialFuel=0d;var sawScooping=false;
        var samples=new List<object>();string? failure=null;ulong target=0;long world=0;double exclusion=0,initialRadius=0;bool thermalEscape=false;
        async Task Release()
        {
            var timeout=Stopwatch.StartNew();
            while(timeout.Elapsed.TotalSeconds<3)
            {
                var reply=await pipe.CallAsync(new{op="release",owner});
                if(reply.GetProperty("released").GetBoolean()){released=true;return;}
                await Task.Delay(40);
            }
            throw new TimeoutException("Orbit inputs were not released.");
        }
        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 identity changed.");
            using(var status=ReadShared(statusPath))
            {
                var flags=status.RootElement.GetProperty("Flags").GetUInt64();
                if((flags&16)==0||(flags&(1UL|65536UL|131072UL|1048576UL|4194304UL))!=0||status.RootElement.GetProperty("GuiFocus").GetInt32()!=0)throw new InvalidOperationException("Requires idle, safe main-ship supercruise.");
                initialFuel=status.RootElement.GetProperty("Fuel").GetProperty("FuelMain").GetDouble();
            }
            var loadout=Events(journal).Last(e=>e.GetProperty("event").GetString()=="Loadout");
            if(!loadout.GetProperty("Modules").EnumerateArray().Any(m=>m.GetProperty("Item").GetString()!.Contains("fuelscoop",StringComparison.OrdinalIgnoreCase)&&m.GetProperty("On").GetBoolean()))throw new InvalidOperationException("A powered fuel scoop is required.");
            var frame=await pipe.CallAsync(new{op="read"});var drive=await pipe.CallAsync(new{op="fsd.describe"});var geometry=await pipe.CallAsync(new{op="geometry.describe"});
            if(!frame.GetProperty("localPlayerShipVerified").GetBoolean()||drive.GetProperty("entityId").GetString()!=frame.GetProperty("entityId").GetString()||!drive.GetProperty("fresh").GetBoolean()||drive.GetProperty("state").GetInt32()!=2||!drive.GetProperty("targetValid").GetBoolean()||!geometry.GetProperty("fresh").GetBoolean()||geometry.GetProperty("bodyKind").GetInt32()!=1)throw new InvalidDataException("Coherent local-ship, idle FSD, real target and primary-star geometry are required.");
            world=long.Parse(geometry.GetProperty("worldAddress").GetString()!);target=ulong.Parse(drive.GetProperty("targetAddress").GetString()!);
            var starType=geometry.GetProperty("starType").GetInt32();
            if(starType is <0 or >6)throw new InvalidOperationException("This primary star is not a verified OBAFGKM type.");
            exclusion=geometry.GetProperty("exclusionRadius").GetDouble();var star=new StarEnvelope(world,"OBAFGKM"[starType].ToString(),Vector3d.Zero,exclusion,null,null);
            var position=V(frame,"position");var destination=position+V(drive,"targetDirection")*drive.GetProperty("targetDistance").GetDouble();
            initialRadius=position.Length;
            if(!FlightGeometry.Obscures(star,position,destination)||initialRadius<exclusion*1.4||frame.GetProperty("heatPercent").GetDouble()>=86)throw new InvalidOperationException("Requires a real star-obscured target, exterior approach and initial heat below 86 percent.");
            await Log(new{kind="preflight",start,pid,target,world,frame,drive,geometry,loadoutShip=loadout.GetProperty("Ship").GetString()});
            var previousRadial=position.Unit();var orbitRadius=Math.Max(exclusion*1.25,initialRadius*.9);var phase="prealign";double previousHeat=frame.GetProperty("heatPercent").GetDouble(),previousSeconds=0,lastBandChange=0;
            while(clock.Elapsed.TotalSeconds<300)
            {
                using(var current=Process.GetProcessById(pid))if(current.StartTime.ToUniversalTime().Ticks!=processStart)throw new InvalidOperationException("Process identity changed.");
                frame=await pipe.CallAsync(new{op="read"});drive=await pipe.CallAsync(new{op="fsd.describe"});geometry=await pipe.CallAsync(new{op="geometry.describe"});frames++;
                if(!frame.GetProperty("inGame").GetBoolean()||frame.GetProperty("playerOverride").GetBoolean()||!drive.GetProperty("fresh").GetBoolean()||drive.GetProperty("state").GetInt32()!=2||drive.GetProperty("entityId").GetString()!=frame.GetProperty("entityId").GetString()||!geometry.GetProperty("fresh").GetBoolean()||long.Parse(geometry.GetProperty("worldAddress").GetString()!)!=world||ulong.Parse(drive.GetProperty("targetAddress").GetString()!)!=target)throw new InvalidOperationException("Orbit state or ownership changed.");
                using var status=ReadShared(statusPath);var flags=status.RootElement.GetProperty("Flags").GetUInt64();
                if((flags&16)==0||(flags&(1UL|131072UL|1048576UL|4194304UL))!=0)throw new InvalidOperationException("Supercruise or safety state changed.");
                var activeScoop=(flags&2048)!=0;
                foreach(var e in Events(journal))if(e.GetProperty("event").GetString()=="FuelScoop"&&e.GetProperty("timestamp").GetDateTimeOffset()>=start.AddSeconds(-1))journalScooped=Math.Max(journalScooped,e.GetProperty("Scooped").GetDouble());
                fuelGain=Math.Max(fuelGain,status.RootElement.GetProperty("Fuel").GetProperty("FuelMain").GetDouble()-initialFuel);
                scooped=Math.Max(journalScooped,fuelGain);
                position=V(frame,"position");var radius=position.Length;var heat=frame.GetProperty("heatPercent").GetDouble();var seconds=clock.Elapsed.TotalSeconds;
                var slope=(heat-previousHeat)/Math.Max(.02,seconds-previousSeconds);previousHeat=heat;previousSeconds=seconds;
                peak=Math.Max(peak,heat);minimumRadius=Math.Min(minimumRadius,radius);rotated+=Vector3d.AngleDegrees(previousRadial,position);previousRadial=position.Unit();
                destination=position+V(drive,"targetDirection")*drive.GetProperty("targetDistance").GetDouble();cleared=!FlightGeometry.Obscures(star,position,destination);
                if(activeScoop)
                {
                    // Anchor at the first actual scoop boundary rather than continue toward a lower search radius.
                    if(!sawScooping)orbitRadius=Math.Max(exclusion*1.25,radius*.999);
                    sawScooping=true;samples.Add(new{radius,heat,scooped,seconds});
                }
                if(heat>=88||heat+Math.Max(0,slope)*8>=94||radius<exclusion*1.18)thermalEscape=true;
                var desired=thermalEscape?position.Unit():FlightGeometry.OrbitAtRadius(star,position,destination,V(frame,"up"),orbitRadius);
                var aim=AttitudeController.Aim(V(frame,"forward"),V(frame,"up"),desired,.6);
                double throttle=0;var speed=frame.GetProperty("speedMetresPerSecond").GetDouble();
                if(aim.ErrorDegrees<=8)
                {
                    phase=thermalEscape?"thermal-escape":"orbit";
                    var fraction=drive.GetProperty("speedFraction").GetDouble();
                    var maximum=fraction>1e-7?speed/fraction:double.NaN;
                    if(!double.IsFinite(maximum)||maximum<=0)throw new InvalidDataException("Native speed scale is unavailable.");
                    throttle=speed>12_000_000?0:Math.Clamp(10_000_000/maximum,0,.15);
                }
                if(thermalEscape&&radius>=initialRadius*1.05&&heat<75)throw new InvalidOperationException("Thermal guard completed an outward escape; scoop/orbit acceptance is incomplete.");
                if(heat>=95)throw new InvalidOperationException("Reached bounded orbit heat ceiling.");
                await Log(new{kind="sample",seconds,phase,orbitRadius,thermalEscape,aim,throttle,activeScoop,scooped,cleared,frame,drive,geometry});
                if(cleared&&sawScooping&&scooped>0&&rotated>=15){await Release();break;}
                if(!activeScoop&&!thermalEscape&&seconds-lastBandChange>15&&phase=="orbit")
                {orbitRadius=Math.Max(exclusion*1.25,orbitRadius-exclusion*.02);lastBandChange=seconds;}
                await pipe.CallAsync(new{op="calibrate.axes",owner,sequence=frame.GetProperty("sequence").GetInt64(),ttlMilliseconds=250,throttle,
                    axes=new[]{0d,0,0,aim.Yaw,aim.Pitch,aim.Roll}});
                await Task.Delay(60);
            }
            if(!cleared||!sawScooping||scooped<=0||rotated<15)throw new InvalidOperationException("Actual orbit, scoop and target clearance were not all observed within the bound.");
        }
        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&&cleared&&sawScooping&&scooped>0&&rotated>=15;
        await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{pid,hash,start,target,world,exclusion,initialRadius,minimumRadius,frames,peak,rotated,cleared,sawScooping,scooped,journalScooped,initialFuel,fuelGain,thermalEscape,released,passed,failure,scoopSamples=samples,screenUse=false,osInputSent=false,fsdInputsSent=0,completeAutomaticNavigationAccepted=false},new JsonSerializerOptions{WriteIndented=true}));
        Console.WriteLine(JsonSerializer.Serialize(new{passed,frames,peak,rotated,cleared,sawScooping,scooped,thermalEscape,released,failure}));return passed;
    }
}