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();
}
}
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();
}
}