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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

namespace XFEEDToolBox.Core.Session;

/// <summary>Live flight tests require a verified continuous health channel before their first input.</summary>
public sealed class NativeFlightTestProtection:IAsyncDisposable
{
    private static NativeFlightTestProtection? active;
    private readonly CancellationTokenSource stop=new();
    private readonly NavigationPipeClient pipe;
    private readonly ShipProtectionMonitor monitor;
    private readonly GameSessionTarget target;
    private readonly Func<PlayerState> player;
    private readonly Action stopAutomation;
    private readonly bool ownsMonitor;
    private readonly bool automaticExit;
    private readonly bool exitForFailedHandoff;
    private readonly Action? externalProtectionCheck;
    private readonly Func<bool>? expectedInputFreeJumpLoading;
    private readonly Action<Exception>? pauseAutomation;
    private readonly Action<NativeFlightProtectionDiagnostic>? diagnostics;
    private readonly NativeFlightHealthGapPolicy healthGap=new();
    private readonly object diagnosticGate=new();
    private string? lastDiagnosticPhase;
    private int monitoringPauseReported;
    private readonly IGameSessionDriver sessionDriver;
    private bool independentHealth;
    private Task? polling;
    private int unsafeState;
    private int damagePause;
    private long lastHealthTicks,lastSequence;
    private ulong entity;
    private ShipHealthSample? lastHealth;
    public ShipHealthSample? LastHealth=>Volatile.Read(ref lastHealth);
    public string? LastIssue{get;private set;}
    public bool Stopped=>Volatile.Read(ref unsafeState)!=0||Volatile.Read(ref damagePause)!=0;
    private bool HealthSampleFresh(DateTimeOffset now)
    {
        var ticks=Interlocked.Read(ref lastHealthTicks);
        return NativeFlightHealthGapPolicy.InputSampleFresh(ticks>0?new DateTimeOffset(ticks,TimeSpan.Zero):null,now);
    }
    public bool Fresh=>!Stopped&&Volatile.Read(ref damagePause)==0&&!monitor.TestingBlocked&&HealthSampleFresh(DateTimeOffset.UtcNow);
    private NativeFlightTestProtection(int pid,GameSessionTarget target,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,ShipProtectionMonitor? sharedMonitor,bool automaticExit,bool exitForFailedHandoff,Action? externalProtectionCheck,
        Func<bool>? expectedInputFreeJumpLoading,Action<Exception>? pauseAutomation,Action<NativeFlightProtectionDiagnostic>? diagnostics)
    {
        pipe=new(pid);this.target=target;this.player=player;this.stopAutomation=stopAutomation;sessionDriver=sessions;this.automaticExit=automaticExit;this.exitForFailedHandoff=exitForFailedHandoff;this.externalProtectionCheck=externalProtectionCheck;
        this.expectedInputFreeJumpLoading=expectedInputFreeJumpLoading;this.pauseAutomation=pauseAutomation;this.diagnostics=diagnostics;
        ownsMonitor=sharedMonitor is null;
        monitor=sharedMonitor??new(sessions,player,StopAutomation,automaticExit);
    }
    private void Diagnose(string phase,string issue,Exception? error=null,bool expectedLoading=false)
    {
        lock(diagnosticGate){if(lastDiagnosticPhase==phase)return;lastDiagnosticPhase=phase;}
        if(diagnostics is null)return;
        try
        {
            var now=DateTimeOffset.UtcNow;var health=LastHealth;var unavailable=healthGap.UnavailableSince;
            diagnostics(new(now,target.ProcessId,target.StartedUtcTicks,player().GameMode,phase,issue,
                health?.Sequence,health?.EntityId,health?.At,health is null?null:(now-health.At).TotalMilliseconds,
                unavailable,unavailable is {} began?(now-began).TotalMilliseconds:0,expectedLoading,
                phase is "health-gap" or "input-free-jump-loading",error?.GetType().FullName,error?.Message));
        }
        catch{/* Evidence delivery cannot grant input or prevent a required pause. */}
    }
    private void StopAutomation()
    {
        Diagnose("safety-stop",monitor.LastTrigger?.Message??LastIssue??"原飞船安全保护停止自动控制。");
        try{stopAutomation();}catch{/* A stopped worker cannot disarm health monitoring. */}
    }
    private void PauseMonitoring(Exception error,bool expectedLoading=false)
    {
        LastIssue=error.Message;Interlocked.Exchange(ref unsafeState,1);
        if(Interlocked.Exchange(ref monitoringPauseReported,1)!=0)return;
        Diagnose("monitoring-paused",error.Message,error,expectedLoading);
        try{if(pauseAutomation is not null)pauseAutomation(error);else stopAutomation();}
        catch{try{stopAutomation();}catch{/* The protected state is already latched. */}}
    }
    public static async Task ExitForFailedHandoffAsync(int pid)=>await HandleFailedHandoffAsync(pid);
    public static async Task<bool> HandleFailedHandoffAsync(int pid)
    {
        var protection=Volatile.Read(ref active);
        if(protection is null||protection.target.ProcessId!=pid)throw new InvalidOperationException("无法为未绑定的游戏实例执行保护退出。");
        Interlocked.Exchange(ref protection.unsafeState,1);
        protection.LastIssue="自动降落未接管,飞船仍在移动;已停止操作并保护退出。";
        protection.StopAutomation();
        if(!protection.automaticExit||!protection.exitForFailedHandoff){protection.LastIssue="自动降落未接管,已停止自动输入并保留游戏现场。";return false;}
        // Do not restart into the same unresolved approach or let cancelling
        // the navigation worker interrupt the already required protective exit.
        await new GameSessionController(protection.sessionDriver,()=>false).EmergencyExitAsync(protection.target,CancellationToken.None);
        return true;
    }
    public static async Task<NativeFlightTestProtection> ArmAsync(int pid,string hash,IGameSessionDriver sessions,Func<PlayerState> player,Action stopAutomation,CancellationToken token=default,ShipProtectionMonitor? sharedMonitor=null,bool automaticExit=true,bool exitForFailedHandoff=true,Action? externalProtectionCheck=null,
        Func<bool>? expectedInputFreeJumpLoading=null,Action<Exception>? pauseAutomation=null,Action<NativeFlightProtectionDiagnostic>? diagnostics=null)
    {
        if(Volatile.Read(ref active) is not null)throw new InvalidOperationException("已有飞行测试保护正在运行。");
        var pipe=new NavigationPipeClient(pid);var hello=await pipe.CallAsync(new{op="describe"},token);
        if(!hello.TryGetProperty("schema",out var schema)||schema.GetInt32()!=1||!hello.TryGetProperty("name",out var name)||name.GetString()!="XFEEDToolBox.Navigation"
            ||!hello.TryGetProperty("verified",out var verified)||verified.ValueKind!=JsonValueKind.True
            ||hello.GetProperty("processId").GetInt32()!=pid||hello.GetProperty("gameSha256").GetString()!=hash
            ||!hello.TryGetProperty("continuousShipHealth",out var flag)||flag.ValueKind!=JsonValueKind.True)
            throw new NotSupportedException("当前插件尚未提供已验证的连续船体血量,已禁止自动飞行测试。");
        var target=await sessions.FindTargetAsync(token);if(target.ProcessId!=pid)throw new InvalidOperationException("飞行测试与受保护游戏实例不一致。");
        if(sharedMonitor is not null&&sharedMonitor.BoundTarget!=target)throw new InvalidOperationException("连续血量监测与受保护游戏实例不一致。");
        if(sharedMonitor is not null&&sharedMonitor.AutomaticExitEnabled!=automaticExit)throw new InvalidOperationException("连续血量监测的退出模式不一致。");
        externalProtectionCheck?.Invoke();
        var protection=new NativeFlightTestProtection(pid,target,sessions,player,stopAutomation,sharedMonitor,automaticExit,exitForFailedHandoff,externalProtectionCheck,
            expectedInputFreeJumpLoading,pauseAutomation,diagnostics);
        protection.independentHealth=hello.TryGetProperty("independentShipHealth",out var independent)&&independent.ValueKind==JsonValueKind.True;
        try
        {
            if(protection.ownsMonitor)protection.monitor.EnableTarget(target);
            // Scene transitions can precede the first original health frame.
            // Keep all callers waiting for a fresh sample before arming input.
            var health=await NavigationRecovery.ReadWithRecoveryAsync(protection.ReadAsync,null,token,Timeout.InfiniteTimeSpan);protection.monitor.Observe(health,DateTimeOffset.UtcNow);
            if(health.HullHealth<=0)throw new InvalidOperationException("船体已失效,已停止自动操作。");
            protection.monitor.RequireHealthyShip();
            if(Interlocked.CompareExchange(ref active,protection,null) is not null)throw new InvalidOperationException("已有飞行测试保护正在运行。");
            protection.polling=protection.PollAsync();return protection;
        }
        catch{await protection.DisposeAsync();throw;}
    }
    public static void CheckBeforeInput(int pid)
    {
        var protection=Volatile.Read(ref active);
        if(protection is null)return;
        protection.monitor.ObserveInterdiction(protection.player(),DateTimeOffset.UtcNow);
        if(protection.target.ProcessId!=pid||Volatile.Read(ref protection.unsafeState)!=0||protection.monitor.EmergencyActive)
            throw new InvalidOperationException("飞船保护已停止测试,禁止继续发送飞行输入。");
        if(protection.monitor.TestingBlocked)throw new FlightHealthPauseException();
        protection.externalProtectionCheck?.Invoke();
        if(!protection.HealthSampleFresh(DateTimeOffset.UtcNow))
            throw new FlightTelemetryUnavailableException("正在恢复连续血量监测,恢复后继续驾驶。");
    }
    public void ObserveJournal(ObservedJournalEvent observed)=>monitor.Observe(observed,player());
    private async Task<ShipHealthSample> ReadAsync(CancellationToken token)
    {
        var frame=await pipe.CallAsync(new{op=independentHealth?"health.read":"read"},token);
        if(!frame.TryGetProperty("shipHealth",out var health)||!health.GetProperty("verified").GetBoolean())throw new InvalidDataException("连续船体遥测未通过校验。");
        var ship=player().CurrentShipId??throw new InvalidDataException("缺少当前主船身份。");
        if(!frame.GetProperty("localPlayerShipVerified").GetBoolean()||health.GetProperty("entityId").GetString()!=frame.GetProperty("entityId").GetString())throw new InvalidDataException("连续船体遥测不是当前主船。");
        var sample=new ShipHealthSample(health.GetProperty("sequence").GetInt64(),DateTimeOffset.FromUnixTimeMilliseconds(health.GetProperty("observedAtUnixMilliseconds").GetInt64()),
            target.ProcessId,target.StartedUtcTicks,ulong.Parse(frame.GetProperty("entityId").GetString()!,System.Globalization.CultureInfo.InvariantCulture),
            ship,health.GetProperty("hullFraction").GetDouble(),
            health.TryGetProperty("shieldFraction",out var shield)&&shield.ValueKind==JsonValueKind.Number?shield.GetDouble():null,health.GetProperty("damageObserved").GetBoolean());
        if(sample.ShipId!=player().CurrentShipId||sample.Sequence<=0||sample.EntityId==0
            ||!double.IsFinite(sample.HullHealth)||sample.HullHealth is <0 or >1||sample.ShieldHealth is {} value&&(!double.IsFinite(value)||value is <0 or >1))
            throw new InvalidDataException("连续船体遥测身份或时间不匹配。");
        if(sample.At>DateTimeOffset.UtcNow||DateTimeOffset.UtcNow-sample.At>TimeSpan.FromMilliseconds(500))
            throw new FlightTelemetryUnavailableException("正在重新读取连续船体与护盾监测。");
        if(sample.Sequence<lastSequence)
        {Interlocked.Exchange(ref unsafeState,1);throw new InvalidDataException("连续船体遥测倒退,已暂停驾驶输入。");}
        if(entity!=0&&sample.EntityId!=entity)
        {
            Interlocked.Exchange(ref unsafeState,1);LastIssue="飞船实体已切换,已暂停驾驶输入并继续监测健康。";
            // Keep protecting the newly verified local main ship, even while
            // old control leases are barred and the driver revalidates a transition.
        }
        // Multiple IPC reads can observe one physics frame. Retain its actual
        // timestamp; a duplicate neither refreshes the input lease nor stops protection.
        if(sample.Sequence==lastSequence)return sample;
        entity=sample.EntityId;lastSequence=sample.Sequence;Volatile.Write(ref lastHealth,sample);Interlocked.Exchange(ref lastHealthTicks,sample.At.UtcTicks);
        return sample;
    }
    private async Task PollAsync()
    {
        try
        {
            while(!stop.IsCancellationRequested)
            {
                monitor.ObserveInterdiction(player(),DateTimeOffset.UtcNow);
                try
                {
                    var health=await ReadAsync(stop.Token);monitor.Observe(health,DateTimeOffset.UtcNow);
                    if(healthGap.UnavailableSince is not null)
                        Diagnose(Stopped?"health-restored-awaiting-rearm":"health-recovered","已重新读取实际新鲜船体与护盾样本。");
                    healthGap.Recovered();if(!Stopped)LastIssue=null;
                }
                catch(Exception error)when(!stop.IsCancellationRequested)
                {
                    LastIssue=error.Message;
                    var expectedLoading=NativeFlightHealthGapPolicy.RecoverableGap(error,stop.Token)&&expectedInputFreeJumpLoading?.Invoke()==true;
                    var decision=healthGap.Observe(error,DateTimeOffset.UtcNow,expectedLoading,stop.Token);
                    if(decision.SafetyStop){Interlocked.Exchange(ref unsafeState,1);StopAutomation();}
                    else if(decision.PauseMonitoring)PauseMonitoring(error,expectedLoading);
                    else Diagnose(decision.Phase,decision.Issue,error,expectedLoading);
                }
                if(monitor.EmergencyActive){Interlocked.Exchange(ref unsafeState,1);StopAutomation();break;}
                if(monitor.TestingBlocked)
                {
                    if(Interlocked.Exchange(ref damagePause,1)==0)StopAutomation();
                    LastIssue="已暂停当前操作,继续观察受损停止后的稳定状态。";
                }
                else Interlocked.Exchange(ref damagePause,0);
                await Task.Delay(80,stop.Token);
            }
        }
        catch(OperationCanceledException)when(stop.IsCancellationRequested){}
        catch(Exception error){PauseMonitoring(error);}
    }
    public async ValueTask DisposeAsync()
    {
        Interlocked.Exchange(ref unsafeState,1);stop.Cancel();
        if(polling is not null)await polling;
        // Wait for an already triggered emergency before cancelling subsequent restart waits.
        if(ownsMonitor){await monitor.Completion;await monitor.DisposeAsync();}
        Interlocked.CompareExchange(ref active,null,this);stop.Dispose();
    }
}