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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

internal static class ManualVisualHandoff
{
    private sealed record Ready(int Pid,long StartedUtcTicks,DateTimeOffset? LoadGameAt,long ShipId,string FromSystem,string TargetSystem,string TargetStation);
    private static async Task<Ready> Capture(int pid)
    {
        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 state=journal.State;
        if(!state.InGame||state.Docked||state.CurrentShipId!=5||state.CargoCapacity!=192||state.CargoCount!=0
            ||NavigationSafety.UnhealthyHull(state)||NavigationSafety.HasExternalThreat(state)||(state.Status?.Flags&16)!=0)
            throw new InvalidOperationException("人工观察准备需要当前正常空间中的健康、空载Corsair/5。");
        if(!NavigationAssists.From(state).AutoDock||!NavigationAssists.From(state).Supercruise)
            throw new InvalidOperationException("当前装备不满足原生辅助交接观察。");
        var pipe=new NavigationPipeClient(pid);var hello=await pipe.CallAsync(new{op="describe"});
        if(hello.GetProperty("gameSha256").GetString()!=NativeSystemLocationReader.GameSha256||hello.GetProperty("version").GetString()!="0.5.13-dev")
            throw new InvalidDataException("人工观察所绑定的游戏构建或插件候选版本已改变。");
        var health=await pipe.CallAsync(new{op="health.read"});var h=health.GetProperty("shipHealth");
        var at=DateTimeOffset.FromUnixTimeMilliseconds(h.GetProperty("observedAtUnixMilliseconds").GetInt64());
        if(!health.GetProperty("localPlayerShipVerified").GetBoolean()||!h.GetProperty("verified").GetBoolean()||h.GetProperty("hullFraction").GetDouble()<.5
            ||DateTimeOffset.UtcNow-at>TimeSpan.FromMilliseconds(500))throw new InvalidDataException("当前主船健康数据尚未就绪。");
        var raw=await pipe.CallAsync(new{op="read"});
        if(!raw.GetProperty("inGame").GetBoolean()||raw.GetProperty("entityId").GetString()!=health.GetProperty("entityId").GetString()
            ||raw.GetProperty("speedMetresPerSecond").GetDouble()>1)throw new InvalidOperationException("请保持当前飞船停止,等待开始指令。");
        var inventory=await new NativeCommerceChannel(pid).CallAsync(new{op="commerce.inventory"},CancellationToken.None);
        if(!inventory.GetProperty("verified").GetBoolean()||inventory.GetProperty("count").GetInt32()!=0||inventory.GetProperty("capacity").GetInt32()!=192)
            throw new InvalidDataException("原始服务器货舱与观察准备不一致。");
        var target=state.SystemName switch{"Duamta"=>("AD Leonis","Homer Platform"),"AD Leonis"=>("Duamta","Polyakov Station"),_=>throw new InvalidOperationException("当前星系不在已核对的空载观察航段内。")};
        using var game=Process.GetProcessById(pid);
        return new(pid,game.StartTime.ToUniversalTime().Ticks,state.LoadGameAt,5,state.SystemName,target.Item1,target.Item2);
    }
    internal static async Task PrepareAsync(int pid,string workRoot)
    {
        workRoot=Path.GetFullPath(workRoot);var ready=await Capture(pid);var path=Path.Combine(workRoot,"manual-visual-ready.json");
        var text=JsonSerializer.Serialize(ready,ToolboxSettings.JsonOptions);
        await File.WriteAllTextAsync(path+".tmp",text);File.Move(path+".tmp",path,true);
        Console.WriteLine(JsonSerializer.Serialize(new{ready=true,flightInputsSent=0,automaticExitEnabled=false,tradeOrdersSent=0,ready.FromSystem,ready.TargetSystem,ready.TargetStation},ToolboxSettings.JsonOptions));
    }
    // Invoke only after the human says to start. Preparation above is read-only.
    internal static async Task RunAsync(int pid,string workRoot)
    {
        workRoot=Path.GetFullPath(workRoot);
        using var lease=new FileStream(Path.Combine(workRoot,"manual-visual-runner.lock"),FileMode.OpenOrCreate,FileAccess.ReadWrite,FileShare.None);
        var expected=JsonSerializer.Deserialize<Ready>(await File.ReadAllTextAsync(Path.Combine(workRoot,"manual-visual-ready.json")),ToolboxSettings.JsonOptions)
            ??throw new InvalidDataException("缺少人工观察准备记录。");
        if(expected!=await Capture(pid))throw new InvalidOperationException("游戏、会话或船舶已改变,需要重新准备,尚未开始飞行。");
        var directory=Path.Combine(workRoot,"manual-visual-"+Guid.NewGuid().ToString("N"));Directory.CreateDirectory(directory);
        using var stop=new CancellationTokenSource(TimeSpan.FromMinutes(30));
        using var sentinel=new Timer(_=>{if(File.Exists(Path.Combine(workRoot,"manual-visual-observation.stop")))stop.Cancel();},null,100,100);
        string? failure=null;
        try
        {
            await InterstellarTravelAcceptance.RunAsync(pid,expected.TargetSystem,Path.Combine(directory,"route.json"),stop.Token,manualVisual:true);
            await LocalNavigationAcceptance.RunAsync(pid,expected.TargetStation,Path.Combine(directory,"arrival.json"),true,true,true,stop.Token,manualVisual:true);
        }
        catch(Exception error){failure=error.ToString();}
        bool gameRunning;try{using var game=Process.GetProcessById(pid);gameRunning=!game.HasExited&&game.StartTime.ToUniversalTime().Ticks==expected.StartedUtcTicks;}catch(ArgumentException){gameRunning=false;}
        await File.WriteAllTextAsync(Path.Combine(directory,"result.json"),JsonSerializer.Serialize(new{passed=failure is null,pid,expected.TargetStation,manualVisual=true,
            automaticExitEnabled=false,gameRunning,tradeOrdersSent=0,failure,finished=DateTimeOffset.UtcNow},ToolboxSettings.JsonOptions));
        Console.WriteLine(JsonSerializer.Serialize(new{finished=true,manualVisual=true,gameRunning,automaticExitEnabled=false,failure},ToolboxSettings.JsonOptions));
    }
}