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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

namespace XFEEDToolBox.Core.Navigation;

/// <summary>Local, bounded IPC to the optional build-specific game plugin.</summary>
public sealed class NavigationPipeClient(int processId,string service="Navigation")
{
    public int ProcessId { get; }=processId>0?processId:throw new ArgumentOutOfRangeException(nameof(processId));
    private readonly string serviceName=service is "Navigation" or "Interaction" or "SessionRecovery0514" or "SessionRecovery0514c" or "SessionRecovery0514d" or "SessionRecovery0514e" or "SessionRecovery0514f" or "SessionRecovery0514g" or "SessionRecovery0514h" or "SessionRecovery0514i" or "SessionRecovery0514j" or "SessionRecovery0514k" or "CruiseObservation0514a" or "CruiseObservation0514b" or "CruiseObservation0514c" or "CruiseControl0514a" or "CruiseControl0514b" or "CruiseControl0514c" or "CruiseControl0514d" or CruiseControlProtocol.Service or CruiseControlProtocol.LegacyService?service:throw new ArgumentException("Unknown game service.",nameof(service));
    private readonly SemaphoreSlim gate=new(1,1);
    public async Task<JsonElement> CallAsync(object request,CancellationToken token=default)
    {
        await gate.WaitAsync(token);
        try
        {
            using var deadline=CancellationTokenSource.CreateLinkedTokenSource(token);deadline.CancelAfter(TimeSpan.FromMilliseconds(500));
            var data=JsonSerializer.SerializeToUtf8Bytes(request,ToolboxSettings.JsonOptions);
            using(var guardRequest=JsonDocument.Parse(data))
                if(guardRequest.RootElement.TryGetProperty("op",out var operation)&&operation.GetString() is "apply" or "local.apply" or "local.action" or "fsd" or "fsd.command" or "input.set" or "calibrate.axes" or "calibrate.fsd" or "inputs.set" or "commerce.trade" or "commerce.trade.batch" or "commerce.trade.formal" or "cg.join" or "station.services" or "docking.validation.arm" or "session.recovery.leave" or "docking.reset.arm" or "flight.context.arm" or "flight.context.renew" or "cruise.set")
                    XFEEDToolBox.Core.Session.NativeFlightTestProtection.CheckBeforeInput(ProcessId);
            if(data.Length>=65535)throw new InvalidDataException("导航指令超出消息上限。");
            using var pipe=new NamedPipeClientStream(".",$"XFEEDToolBox.{serviceName}.{ProcessId}",PipeDirection.InOut,PipeOptions.Asynchronous);
            await pipe.ConnectAsync(deadline.Token);await pipe.WriteAsync(data,deadline.Token);await pipe.FlushAsync(deadline.Token);
            var buffer=new byte[65536];var count=0;
            while(count<buffer.Length)
            {
                var read=await pipe.ReadAsync(buffer.AsMemory(count),deadline.Token);if(read==0)break;count+=read;
                if(buffer[count-1]=='\n')break;
            }
            if(count==0||count==buffer.Length)throw new InvalidDataException("导航驱动返回空消息或超长消息。");
            using var document=JsonDocument.Parse(buffer.AsMemory(0,count));var root=document.RootElement;
            if(!root.TryGetProperty("ok",out var ok)||ok.ValueKind!=JsonValueKind.True)
                throw new InvalidOperationException(root.TryGetProperty("error",out var error)?error.GetString():"原生导航操作被拒绝。");
            return root.GetProperty("result").Clone();
        }
        finally {gate.Release();}
    }
}

public sealed record NativeAppliedControl(long Sequence,double? Throttle,double? Yaw,double? Pitch,double? PredictedPeak,double? Roll=null);
public sealed record NativeChargeRequest(long Sequence,long TargetSystemAddress,double HeatPercent,double PredictedPeak,string Mechanism);

public sealed class NativeFlightControlDriver(int processId,string expectedGameSha256,Func<PlayerState>? player=null,Func<double>? fuelReserve=null,IGameInputChannel? gameInputs=null,Func<CancellationToken,Task>? ensureFlightProtection=null) : IFlightControlDriver, IGameRouteOptionsPlanningDriver, IGameRouteCancellationDriver, ISystemFlightControlDriver,IFlightDepartureDriver
{
    private NativeSystemFlightControlDriver? systemDriver;
    private NativeSystemFlightControlDriver SystemDriver=>systemDriver??=new(processId,expectedGameSha256,player??throw new InvalidOperationException("星系内驾驶需要实时飞船状态。"),gameInputs,ensureFlightProtection);
    Task<SystemNavigationCapabilities> ISystemFlightControlDriver.InspectAsync(CancellationToken token)=>SystemDriver.InspectAsync(token);
    Task ISystemFlightControlDriver.SelectAsync(SystemNavigationTarget target,CancellationToken token)=>SystemDriver.SelectAsync(target,token);
    Task ISystemFlightControlDriver.PrepareAsync(CancellationToken token)=>SystemDriver.PrepareAsync(token);
    Task<SystemFlightFrame> ISystemFlightControlDriver.ReadSystemFlightAsync(CancellationToken token)=>SystemDriver.ReadSystemFlightAsync(token);
    Task ISystemFlightControlDriver.ApplySystemFlightAsync(SystemFlightDecision decision,SystemFlightFrame reference,SystemNavigationTarget target,DateTimeOffset expiresAt,CancellationToken token)
        =>SystemDriver.ApplySystemFlightAsync(decision,reference,target,expiresAt,token);
    Task ISystemFlightControlDriver.ReleaseSystemFlightAsync()=>systemDriver?.ReleaseSystemFlightAsync()??Task.CompletedTask;
    Task ISystemFlightControlDriver.SuspendSystemFlightAsync()=>SystemDriver.SuspendSystemFlightAsync();
    Task IFlightDepartureDriver.DepartAsync(CancellationToken token)=>SystemDriver.DepartAsync(token);
    Task IFlightDepartureDriver.EnsureDepartureClearanceAsync(long? localDestinationMarket,CancellationToken token)=>SystemDriver.EnsureDepartureClearanceAsync(localDestinationMarket,token);
    private readonly NavigationPipeClient pipe=new(processId);
    private readonly string owner=Guid.NewGuid().ToString("N");
    private FlightDriverCapabilities? capabilities;
    private string? identityRejection;
    private long selectedSystem;
    private bool ownsControl;
    private NativeFrameProjector projector=new(expectedGameSha256);
    private NavWaypoint? selectedWaypoint;
    private readonly BankedAttitudeController bankedAttitude=new();
    private int owningThread;
    public event Action<GameInputState>? GameInputsObserved;
    public event Action<NativeChargeRequest>? OriginalChargeRequested;
    public event Action<JsonElement>? TargetSelectionObserved;
    public JsonElement? LastTargetSelectionDiagnostic{get;private set;}
    private ulong ExpectedInputEntity()=>LastRawTelemetry is {} raw&&raw.TryGetProperty("entityId",out var value)&&ulong.TryParse(value.GetString(),out var entity)&&entity>0?entity:throw new InvalidDataException("缺少受控飞船的原生身份。");
    public FlightObservation? LastObservation { get; private set; }
    public NativeAppliedControl? LastAppliedControl { get; private set; }
    public JsonElement? LastRawTelemetry { get; private set; }
    public event Action<FlightObservation,FlightDecision?>? Observed;
    public int ProcessId=>pipe.ProcessId;
    public bool SupportsGameRoutePlanning { get; private set; }
    public bool SupportsGameRouteOptions {get;private set;}
    public bool SupportsRouteStarClassFilter {get;private set;}
    public bool SupportsRouteMapFilters {get;private set;}
    public bool SupportsRouteFilterChoices {get;private set;}
    public bool SupportsGameRouteCancellation { get; private set; }
    public bool SupportsGalaxyCatalog { get; private set; }
    public bool SupportsGalaxyRegion { get; private set; }
    public bool SupportsGalaxyCells { get; private set; }
    public bool SupportsGalaxyCatalogueProfile { get; private set; }
    public bool SupportsGalaxyDustInputs {get;private set;}
    public bool SupportsGalaxyBasicSystemFacts { get; private set; }
    public bool SupportsGalaxyCivilizationFacts {get;private set;}
    public bool SupportsGalaxyCarrierFacts {get;private set;}
    public bool SupportsGalaxyServiceFacts {get;private set;}
    public bool SupportsGalaxyAccountBookmarks { get; private set; }
    public bool SupportsGalaxyResolveAddress { get; private set; }
    public bool SupportsGalaxyAccountColonisation { get; private set; }
    public bool SupportsGalaxyFilterFacts { get; private set; }
    public bool SupportsGalaxyColonisationEligibility { get; private set; }
    public bool SupportsGalaxyAccountSocial { get; private set; }
    public bool SupportsGalaxyPowerOptions { get; private set; }
    public bool SupportsGalaxyPowerActivityFacts { get; private set; }
    public bool SupportsGalaxyPowerOuterFacts { get; private set; }
    public bool SupportsGalaxyPowerOuterExtent {get;private set;}
    public bool SupportsGalaxyPowerInfluence {get;private set;}
    public bool SupportsGalaxyStateFacts {get;private set;}
    public bool SupportsGalaxyColonisationOwnership {get;private set;}
    public bool SupportsGalaxyColonisationHighlight {get;private set;}
    public bool SupportsGalaxyColonisationNormalStatus {get;private set;}
    public bool SupportsGalaxyPowerTerritoryFacts { get; private set; }
    public bool SupportsGalaxyThargoid { get; private set; }
    public bool SupportsGalaxyCommunityGoals { get; private set; }
    public bool SupportsGalaxyFleet { get; private set; }
    public bool SupportsGalaxyMissions { get; private set; }
    public bool SupportsGalaxyCommodityOptions { get; private set; }
    public bool SupportsGalaxyCommodityFilterFacts { get; private set; }
    public bool SupportsGalaxyCurrentTradeFlows { get; private set; }
    public bool SupportsGalaxyPowerStatusFacts { get; private set; }
    public bool SupportsReferenceGeometry { get; private set; }
    public bool SupportsAdaptiveFlightControl { get; private set; }
    public bool SupportsApplicationSteeringAxes { get; private set; }
    public Task BeginNavigationSessionAsync(CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
        if(ownsControl||systemDriver?.Active==true)throw new InvalidOperationException("已有自动驾驶持有飞行控制,不能开始另一段导航。");
        selectedSystem=0;selectedWaypoint=null;projector=new(expectedGameSha256);
        LastObservation=null;LastAppliedControl=null;bankedAttitude.Reset();
        return Task.CompletedTask;
    }
    public async Task<FlightDriverCapabilities> PreflightAsync(CancellationToken token)
    {
        if(Volatile.Read(ref identityRejection) is {} rejected)throw new InvalidDataException(rejected);
        // Describe is a read-only handshake, also used by UI diagnostics. Do not
        // invalidate a working connection while awaiting it, or reset the
        // selected target and the ship's measured response during flight.
        var hello=await pipe.CallAsync(new{op="describe"},token);
        if(hello.GetProperty("schema").GetInt32()!=1||hello.GetProperty("name").GetString()!="XFEEDToolBox.Navigation"||hello.GetProperty("processId").GetInt32()!=ProcessId
            ||expectedGameSha256.Length!=64||!string.Equals(hello.GetProperty("gameSha256").GetString(),expectedGameSha256,StringComparison.Ordinal))
        {
            // A known foreign build/process is not an uninitialized connection.
            // This adapter must be replaced after inspecting the actual process;
            // reconnecting the same handle cannot make its identity valid.
            Volatile.Write(ref identityRejection,"原生导航进程、构建或协议不匹配。");
            Volatile.Write(ref capabilities,null);
            throw new InvalidDataException("原生导航进程、构建或协议不匹配。");
        }
        bool Flag(string name)=>hello.TryGetProperty(name,out var value)&&value.ValueKind==JsonValueKind.True;
        SupportsGameRoutePlanning=Flag("gameRoutePlanning");SupportsGameRouteOptions=Flag("gameRouteOptions");SupportsRouteStarClassFilter=Flag("routeStarClassFilter");SupportsRouteMapFilters=Flag("routeMapFilters");SupportsGameRouteCancellation=Flag("gameRouteCancellation");
        SupportsReferenceGeometry=Flag("referenceGeometry");
        SupportsAdaptiveFlightControl=Flag("adaptiveFlightControl");
        SupportsApplicationSteeringAxes=Flag("applicationSteeringAxes");
        SupportsGalaxyCatalog=Flag("galaxyCatalog");
        SupportsGalaxyRegion=Flag("galaxyRegion");
        SupportsGalaxyCells=Flag("galaxyCellQueries");
        SupportsGalaxyCatalogueProfile=Flag("galaxyCatalogueProfile");
        SupportsGalaxyDustInputs=Flag("dustInputs");
        SupportsRouteFilterChoices=Flag("routeFilterChoices");
        SupportsGalaxyBasicSystemFacts=Flag("galaxyBasicSystemFacts");
        SupportsGalaxyCivilizationFacts=Flag("galaxyCivilizationFacts");
        SupportsGalaxyCarrierFacts=Flag("galaxyCarrierFacts");
        SupportsGalaxyServiceFacts=Flag("galaxyServiceFacts");
        SupportsGalaxyAccountBookmarks=Flag("galaxyAccountBookmarks");
        SupportsGalaxyResolveAddress=Flag("galaxyResolveAddress");
        SupportsGalaxyAccountColonisation=Flag("galaxyAccountColonisation");
        SupportsGalaxyFilterFacts=Flag("galaxyFilterFacts");
        SupportsGalaxyColonisationEligibility=Flag("galaxyColonisationEligibility");
        SupportsGalaxyAccountSocial=Flag("galaxyAccountSocial");
        SupportsGalaxyPowerOptions=Flag("galaxyPowerOptions");
        SupportsGalaxyPowerActivityFacts=Flag("galaxyPowerActivityFacts");
        SupportsGalaxyPowerOuterFacts=Flag("galaxyPowerOuterFacts");
        SupportsGalaxyPowerOuterExtent=Flag("galaxyPowerOuterExtent");
        SupportsGalaxyPowerInfluence=Flag("galaxyPowerInfluence");
        SupportsGalaxyStateFacts=Flag("galaxyStateFacts");
        SupportsGalaxyColonisationOwnership=Flag("galaxyColonisationOwnership");
        SupportsGalaxyColonisationHighlight=Flag("galaxyColonisationHighlight");
        SupportsGalaxyColonisationNormalStatus=Flag("galaxyColonisationNormalStatus");
        SupportsGalaxyPowerTerritoryFacts=Flag("galaxyPowerTerritoryFacts");
        SupportsGalaxyThargoid=Flag("galaxyThargoid");
        SupportsGalaxyCommunityGoals=Flag("galaxyCommunityGoals");
        SupportsGalaxyFleet=Flag("galaxyFleet");
        SupportsGalaxyMissions=Flag("galaxyMissions");
        SupportsGalaxyCommodityOptions=Flag("galaxyCommodityOptions");
        SupportsGalaxyCommodityFilterFacts=Flag("galaxyCommodityFilterFacts");
        SupportsGalaxyCurrentTradeFlows=Flag("galaxyCurrentTradeFlows");
        SupportsGalaxyPowerStatusFacts=Flag("galaxyPowerStatusFacts");
        owningThread=hello.TryGetProperty("owningThread",out var thread)?thread.GetInt32():0;
        var inspected=new FlightDriverCapabilities(Flag("verified"),expectedGameSha256,Flag("continuousPose"),Flag("continuousHeat"),Flag("starEnvelopes"),Flag("nativeSteering"),Flag("chargeAndCommitInterlock"),hello.GetProperty("evidence").GetString()??"");
        Volatile.Write(ref capabilities,inspected);
        return inspected;
    }
    private void RequireConnectionReady()
    {
        if(Volatile.Read(ref identityRejection) is {} rejected)throw new InvalidOperationException(rejected);
        NavigationRecovery.RequireReady(Volatile.Read(ref capabilities));
    }
    public Task<long> RequestGameRouteAsync(NavWaypoint destination,CancellationToken token)=>RequestGameRouteCoreAsync(destination,null,token);
    public Task<long> RequestGameRouteAsync(NavWaypoint destination,GameRoutePlotOptions options,CancellationToken token)
    {
        options.Validate(player?.Invoke().CargoCapacity);
        if(!SupportsGameRouteOptions)throw new InvalidOperationException("当前游戏插件尚不支持原航线设置。");
        if(options.StarClassFilter is not null&&!SupportsRouteStarClassFilter)throw new InvalidOperationException("当前游戏插件尚不支持恒星类别路线筛选。");
        if(options.MapFilter is not null&&!SupportsRouteMapFilters)throw new InvalidOperationException("当前游戏插件尚不支持此类别的路线筛选。");
        return RequestGameRouteCoreAsync(destination,options.HasOverrides?options:null,token);
    }
    private async Task<long> RequestGameRouteCoreAsync(NavWaypoint destination,GameRoutePlotOptions? options,CancellationToken token)
    {
        if(capabilities?.Ready!=true||!SupportsGameRoutePlanning)throw new InvalidOperationException("游戏中加载的插件尚不支持航线规划,请更新插件并重新启动游戏。");
        if(destination.SystemAddress<=0||string.IsNullOrWhiteSpace(destination.Name))throw new ArgumentException("目标星系无效。",nameof(destination));
        object request=options is null?new{op="route.plan",owner,destinationSystemAddress=destination.SystemAddress}:
            new{op="route.plan",owner,destinationSystemAddress=destination.SystemAddress,routeOptions=new
            {mode=(int?)options.Mode,useJetConeBoost=options.UseJetConeBoost,fsdBoostLevel=options.FsdBoostLevel,virtualCargoMass=options.VirtualCargoMass,starClassMask=options.StarClassFilter?.OriginalMask,
                mapFilter=options.MapFilter is{} map?new{categoryId=map.CategoryId,expectedKeys=map.ExpectedKeys,selectedKeys=map.SelectedKeys,affectsRoutePlotting=map.AffectsRoutePlotting}:null}};
        JsonElement result;var watch=System.Diagnostics.Stopwatch.StartNew();
        while(true)
        {
            try{result=await pipe.CallAsync(request,token);break;}
            catch(InvalidOperationException pending)when(pending.Message=="Game route planning requires a fresh idle FSD"&&watch.Elapsed<TimeSpan.FromSeconds(30))
            {
                var fsd=await pipe.CallAsync(new{op="fsd.describe"},token);var state=fsd.GetProperty("state").GetInt32();
                var arrived=player?.Invoke() is {} current&&GameJournalClock.IsRecentArrival(current,DateTimeOffset.UtcNow);
                if(!fsd.GetProperty("fresh").GetBoolean()||fsd.GetProperty("ownedCharging").GetBoolean()||state is not (0 or 1 or 2 or 4)||state==4&&!arrived)throw;
                await Task.Delay(80,token);
            }
        }
        var generation=result.GetProperty("generation").GetInt64();
        if(generation<=0)throw new InvalidDataException("游戏航线请求缺少有效的回执。");
        return generation;
    }
    public async Task<long> RequestGameRouteCancellationAsync(long expectedDestination,CancellationToken token)
    {
        if(capabilities?.Ready!=true||!SupportsGameRouteCancellation)throw new InvalidOperationException("当前游戏插件尚未接入航线取消。");
        if(expectedDestination<=0)throw new ArgumentException("航线终点无效。",nameof(expectedDestination));
        while(true)
        {
            try
            {
                var result=await pipe.CallAsync(new{op="route.clear",owner,destinationSystemAddress=expectedDestination},token);
                return result.GetProperty("generation").GetInt64();
            }
            catch(InvalidOperationException error)when(error.Message=="Game route planning requires a fresh idle FSD")
            {
                var fsd=await pipe.CallAsync(new{op="fsd.describe"},token);
                if(!fsd.GetProperty("fresh").GetBoolean()||fsd.GetProperty("ownedCharging").GetBoolean()||fsd.GetProperty("state").GetInt32() is not(0 or 1 or 2))throw;
                await Task.Delay(80,token);
            }
        }
    }
    public async Task<GameRouteStatus> ReadGameRouteStatusAsync(long generation,long destinationAddress,CancellationToken token)
    {
        if(generation<=0||destinationAddress<=0)throw new ArgumentException("航线回执或终点无效。");
        var result=await pipe.CallAsync(new{op="route.status",owner,generation,destinationSystemAddress=destinationAddress},token);
        long Address(string name)=>long.TryParse(result.GetProperty(name).GetString(),System.Globalization.NumberStyles.None,System.Globalization.CultureInfo.InvariantCulture,out var value)&&value>=0?value:throw new InvalidDataException("游戏航线地址无效。");
        if(!ulong.TryParse(result.GetProperty("routeFingerprint").GetString(),System.Globalization.NumberStyles.None,System.Globalization.CultureInfo.InvariantCulture,out var fingerprint))throw new InvalidDataException("游戏航路校验值无效。");
        return new(result.GetProperty("generation").GetInt64(),result.GetProperty("applied").GetBoolean(),result.GetProperty("pending").GetBoolean(),result.GetProperty("failed").GetBoolean(),Address("destinationAddress"),Address("targetAddress"),result.GetProperty("targetFresh").GetBoolean(),result.GetProperty("topologyFresh").GetBoolean(),result.GetProperty("waypointCount").GetInt32(),fingerprint);
    }
    public async Task<FlightObservation> ReadAsync(CancellationToken token)
    {
        RequireConnectionReady();
        JsonElement response;
        try{response=await pipe.CallAsync(new{op="read"},token);}
        catch(InvalidOperationException error)when(error.Message=="Native player telemetry unavailable or stale")
        {throw new FlightTelemetryUnavailableException("原生遥测暂时不可用。",error);}
        LastRawTelemetry=response;
        FlightObservation observation;
        if(response.TryGetProperty("frame",out var frame))
            observation=new(JsonSerializer.Deserialize<FlightFrame>(frame,ToolboxSettings.JsonOptions)??throw new InvalidDataException("飞行帧无效。"),
                response.TryGetProperty("thermal",out var thermal)&&thermal.ValueKind!=JsonValueKind.Null?JsonSerializer.Deserialize<ShipThermalCalibration>(thermal,ToolboxSettings.JsonOptions):null);
        else observation=projector.Project(response,player?.Invoke()??throw new InvalidOperationException("原生导航必须绑定实时 Journal 数据。"),selectedWaypoint,fuelReserve?.Invoke()??.5,DateTimeOffset.UtcNow);
        if(observation.Frame.GameSha256!=expectedGameSha256)throw new InvalidDataException("飞行帧来自不匹配的游戏构建。");
        LastObservation=observation;Observed?.Invoke(observation,null);return observation;
    }
    public async Task<JsonElement> ReadShipTelemetryAsync(CancellationToken token)
    {
        if(capabilities?.Ready!=true)throw new InvalidOperationException("原生遥测连接尚未就绪。");
        var value=await pipe.CallAsync(new{op="read"},token);
        LastRawTelemetry=value;
        return value;
    }
    public async Task SelectTargetAsync(NavWaypoint next,CancellationToken token)
    {
        RequireConnectionReady();
        if(next.SystemAddress<=0||string.IsNullOrWhiteSpace(next.Name))throw new ArgumentException("下一跳星系无效。",nameof(next));
        ownsControl=true;
        JsonElement result;var selectionStarted=System.Diagnostics.Stopwatch.StartNew();var arrival=player?.Invoke();var usedArrivalWait=false;
        try{result=await pipe.CallAsync(new{op="target.select",owner,targetSystemAddress=next.SystemAddress},token);}
        catch(InvalidOperationException error)when(error.Message=="A fresh active ship and system target are required")
        {throw new FlightTelemetryUnavailableException("等待抵达后的飞船遥测和 FSD 就绪。",error);}
        catch(InvalidOperationException error)when(error.Message=="Select the destination before starting FSD charging"
            &&arrival is not null&&GameJournalClock.IsRecentArrival(arrival,DateTimeOffset.UtcNow))
        {
            // Arrival and the post-FSD/physics snapshots can straddle different
            // phases. Observe only, within this one immutable arrival deadline;
            // never interpret a Journal FSDTarget as native acknowledgement.
            usedArrivalWait=true;result=await WaitForArrivalTargetSelectionAsync(next,arrival,selectionStarted,error,token);
        }
        using var deadline=CancellationTokenSource.CreateLinkedTokenSource(token);deadline.CancelAfter(TimeSpan.FromSeconds(1));
        if(usedArrivalWait)
        {
            var remaining=NativeTargetSelectionRecovery.MaximumWait-selectionStarted.Elapsed;
            if(remaining<=TimeSpan.Zero)throw new InvalidDataException("到达后目标选择的10秒期限已耗尽。"+LastTargetSelectionDiagnostic?.GetRawText());
            if(remaining<TimeSpan.FromSeconds(1))deadline.CancelAfter(remaining);
        }
        var generation=result.TryGetProperty("generation",out var receipt)&&receipt.ValueKind==JsonValueKind.Number&&receipt.TryGetInt64(out var receiptGeneration)&&receiptGeneration>0?receiptGeneration:
            throw new InvalidDataException("下一跳目标选择缺少有效原生generation回执。");
        try
        {
        while(true)
        {
            if(!result.TryGetProperty("generation",out var currentReceipt)||currentReceipt.ValueKind!=JsonValueKind.Number||!currentReceipt.TryGetInt64(out var currentGeneration)||currentGeneration!=generation)
                throw new InvalidDataException("下一跳目标选择generation已改变,未允许充能。");
            if(result.TryGetProperty("selected",out var selected)&&selected.ValueKind==JsonValueKind.True
                &&result.GetProperty("targetAddress").GetString()==next.SystemAddress.ToString(System.Globalization.CultureInfo.InvariantCulture))
            {
                token.ThrowIfCancellationRequested();
                if(usedArrivalWait&&NativeTargetSelectionRecovery.ContextFailure(arrival!,player!(),next.SystemAddress,DateTimeOffset.UtcNow,selectionStarted.Elapsed) is {} changed)
                    throw new InvalidDataException("原生确认目标后到达身份或期限已改变:"+changed);
                selectedSystem=next.SystemAddress;selectedWaypoint=next;ObserveTargetSelection(next,"acknowledged",selectionStarted.Elapsed,JsonSerializer.SerializeToElement(new{generation,selected=true,targetSystemAddress=selectedSystem}));return;
            }
            if(!result.TryGetProperty("pending",out var pending)||pending.ValueKind!=JsonValueKind.True)throw new InvalidOperationException("游戏未确认锁定下一跳目的地。");
            await Task.Delay(25,deadline.Token);
            result=await pipe.CallAsync(new{op="target.status",owner,targetSystemAddress=next.SystemAddress},deadline.Token);
        }
        }
        catch(OperationCanceledException error)when(usedArrivalWait&&!token.IsCancellationRequested)
        {throw new InvalidDataException("到达后未在同一选择期限内收到原版准确目标确认:"+LastTargetSelectionDiagnostic?.GetRawText(),error);}
        catch(Exception error)when(usedArrivalWait&&NavigationRecovery.CanRetry(error,token))
        {throw new InvalidDataException("到达后原版目标确认回执中断,未重建等待期限:"+LastTargetSelectionDiagnostic?.GetRawText(),error);}
    }
    private void ObserveTargetSelection(NavWaypoint next,string phase,TimeSpan elapsed,JsonElement diagnostic)
    {
        var report=JsonSerializer.SerializeToElement(new{service="InterstellarTargetSelection",processId,phase,observedAtUnixMilliseconds=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
            targetSystemAddress=next.SystemAddress,elapsedMilliseconds=elapsed.TotalMilliseconds,maximumWaitMilliseconds=NativeTargetSelectionRecovery.MaximumWait.TotalMilliseconds,
            issue=phase,sample=new{state=phase},diagnostic});
        LastTargetSelectionDiagnostic=report;
        if(TargetSelectionObserved is {} handlers)foreach(Action<JsonElement> handler in handlers.GetInvocationList())try{handler(report);}catch{/* Diagnostics cannot change target authority. */}
    }
    private async Task<JsonElement> WaitForArrivalTargetSelectionAsync(NavWaypoint next,PlayerState arrival,System.Diagnostics.Stopwatch watch,Exception refusal,CancellationToken token)
    {
        using var bound=CancellationTokenSource.CreateLinkedTokenSource(token);
        var remaining=NativeTargetSelectionRecovery.MaximumWait-watch.Elapsed;
        if(remaining<=TimeSpan.Zero)throw new InvalidDataException("到达后选择目标已超过10秒期限。",refusal);
        bound.CancelAfter(remaining);
        try
        {
            while(true)
            {
                token.ThrowIfCancellationRequested();var current=player?.Invoke()??throw new InvalidDataException("到达后缺少原飞行会话身份。",refusal);
                if(NativeTargetSelectionRecovery.ContextFailure(arrival,current,next.SystemAddress,DateTimeOffset.UtcNow,watch.Elapsed) is {} contextFailure)
                {
                    ObserveTargetSelection(next,"arrival-context-rejected",watch.Elapsed,JsonSerializer.SerializeToElement(new{reason=contextFailure}));
                    if(contextFailure=="arrival-ship-health-failed")throw new FlightHealthPauseException();
                    if(contextFailure=="arrival-external-threat")throw new FlightThreatPauseException();
                    throw new InvalidDataException("到达后目标选择未获得有效身份或等待期限:"+LastTargetSelectionDiagnostic?.GetRawText(),refusal);
                }
                JsonElement fsd,raw;
                try{fsd=await pipe.CallAsync(new{op="fsd.describe"},bound.Token);raw=await pipe.CallAsync(new{op="read"},bound.Token);}
                catch(Exception error)when(!bound.IsCancellationRequested&&NavigationRecovery.CanRetry(error,token))
                {ObserveTargetSelection(next,"awaiting-arrival-read",watch.Elapsed,JsonSerializer.SerializeToElement(new{reason=error.Message}));await Task.Delay(80,bound.Token);continue;}
                var check=NativeTargetSelectionRecovery.Inspect(raw,fsd,next.SystemAddress,arrival.Navigation.CurrentSystemAddress!.Value,DateTimeOffset.UtcNow);
                ObserveTargetSelection(next,check.Reason,watch.Elapsed,check.Diagnostics);
                if(check.Reject)
                {
                    if(check.Diagnostics.TryGetProperty("playerOverride",out var manual)&&manual.ValueKind==JsonValueKind.True)
                        throw new InvalidOperationException("玩家已主动接管,停止下一跳目标选择。"+LastTargetSelectionDiagnostic?.GetRawText());
                    if(check.Reason=="native-session-ended")throw new InvalidDataException("目标选择期间原版飞行会话已结束:"+LastTargetSelectionDiagnostic?.GetRawText());
                    throw new FlightHealthPauseException();
                }
                if(check.CanRequest)
                {
                    // The readonly qualification never grants a lock. The
                    // original selector must still accept and acknowledge it.
                    token.ThrowIfCancellationRequested();
                    if(NativeTargetSelectionRecovery.ContextFailure(arrival,player!(),next.SystemAddress,DateTimeOffset.UtcNow,watch.Elapsed) is {} changed)
                        throw new InvalidDataException("请求原版目标选择前身份或期限已改变:"+changed);
                    try{return await pipe.CallAsync(new{op="target.select",owner,targetSystemAddress=next.SystemAddress},bound.Token);}
                    catch(InvalidOperationException error)when(error.Message=="Select the destination before starting FSD charging"||error.Message=="A fresh active ship and system target are required")
                    {ObserveTargetSelection(next,"native-rejected-after-qualified-read",watch.Elapsed,check.Diagnostics);}
                }
                await Task.Delay(80,bound.Token);
            }
        }
        catch(OperationCanceledException error)when(!token.IsCancellationRequested)
        {throw new InvalidDataException("到达后目标选择10秒内仍未获得原版许可,未发送飞行输入:"+LastTargetSelectionDiagnostic?.GetRawText(),new AggregateException(refusal,error));}
        catch(Exception error)when(NavigationRecovery.CanRetry(error,token))
        {throw new InvalidDataException("到达后目标选择原版回执中断,未重建等待期限:"+LastTargetSelectionDiagnostic?.GetRawText(),new AggregateException(refusal,error));}
    }
    public async Task PrepareForNavigationAsync(CancellationToken token)
    {
        if(ensureFlightProtection is not null)await ensureFlightProtection(token);
        var watch=System.Diagnostics.Stopwatch.StartNew();var preparationStarted=DateTimeOffset.FromUnixTimeSeconds(DateTimeOffset.UtcNow.ToUnixTimeSeconds());DateTimeOffset? unavailable=null;var startedSupercruise=false;var equipmentPrepared=false;var physicalInputsPrepared=false;
        while(watch.Elapsed<TimeSpan.FromSeconds(120))
        {
            token.ThrowIfCancellationRequested();FlightObservation observation;
            try{observation=await ReadAsync(token);}
            catch(Exception error)when(NavigationRecovery.CanRetry(error,token))
            {
                if(NavigationRecovery.Kind(error)==FlightRecovery.RefreshDriver)throw;
                await ReleaseAsync();unavailable??=DateTimeOffset.UtcNow;
                var nav=player?.Invoke().Navigation;
                var expectedTransition=startedSupercruise&&nav?.SupercruiseTransitionAt is {} transition&&transition>=preparationStarted&&DateTimeOffset.UtcNow-transition<TimeSpan.FromSeconds(30);
                if(DateTimeOffset.UtcNow-unavailable>TimeSpan.FromSeconds(expectedTransition?30:5))throw new FlightTelemetryUnavailableException("正在恢复导航准备数据。",error);
                await Task.Delay(100,token);continue;
            }
            unavailable=null;var frame=observation.Frame;
            var state=player?.Invoke()??throw new InvalidOperationException("缺少实时玩家状态。");
            if(!state.InGame)throw new InvalidOperationException("准备超巡时游戏会话已结束。");
            if(state.Docked)throw new InvalidOperationException("准备超巡时飞船进入停靠状态。");
            if(NavigationSafety.MustStop(state,frame,DateTimeOffset.UtcNow))
            {
                var flags=state.Status?.Flags??0;var reason=(flags&(1L<<20))!=0?"飞船过热":(flags&(1L<<23))!=0?"正在被拦截":"存在战斗威胁";
                throw new InvalidOperationException($"准备超巡时{reason},已释放控制。");
            }
            if(frame.PlayerOverride)throw new InvalidOperationException("准备超巡时玩家主动接管,已释放控制。");
            if(!physicalInputsPrepared){await PhysicalInputPreparation.RequireAsync(pipe,token);physicalInputsPrepared=true;}
            if(gameInputs is not null&&!equipmentPrepared)
            {
                await gameInputs.ConnectAsync(owningThread,token);
                await TravelPreparation.EnsureRetractedAsync(gameInputs,()=>player!(),token,ExpectedInputEntity());
                GameInputsObserved?.Invoke(await gameInputs.ReadAsync(token));equipmentPrepared=true;
            }
            else if(TravelPreparation.HasDeployedEquipment(state))throw new InvalidOperationException("武器挂点、货舱或起落架尚未收起,未开始跃迁。");
            if(frame.InSupercruise){await ReleaseAsync();return;}
            // Hyperspace has a separate original input. Normal space does not
            // require a supercruise transition or any measured-speed threshold.
            // SC is useful only when a stellar detour or scoop is actually needed.
            if(!startedSupercruise&&(frame.AdaptiveFlightControl||frame.NativeAllowsCharging==true)&&frame.CurrentStar is {} primary&&frame.FuelMain>=frame.FuelNeededToNextRefuel
                &&!frame.Obstacles.Append(primary).Any(body=>FlightGeometry.Obscures(body,frame.Position,frame.TargetPosition))
                &&(frame.AdaptiveFlightControl||!frame.OutwardDepartureRequired||Vector3d.Dot(frame.Position-primary.Centre,frame.TargetPosition-frame.Position)>=0))return;
            if(frame.Fsd==FsdPhase.Jumping){await ReleaseAsync();await Task.Delay(80,token);continue;}
            if(frame.CurrentStar is not {} star||frame.SystemAddress!=state.Navigation.CurrentSystemAddress)throw new InvalidOperationException("准备超巡时恒星位置与 Journal 不一致。");
            if(frame.HeatPercent is not {} heat||heat>=97)throw new InvalidOperationException("当前热量不适合进入超巡。");
            var escape=frame.Obstacles.Append(star).MinBy(body=>(frame.Position-body.Centre).Length/body.ExclusionRadius)!;
            var outward=(frame.Position-escape.Centre).Unit();var aimError=Vector3d.AngleDegrees(frame.Forward,outward);
            var charging=frame.Fsd is FsdPhase.Charging or FsdPhase.Charged;
            if(charging&&!startedSupercruise)throw new InvalidOperationException("检测到已有 FSD 充能,请先完成或取消当前操作。");
            var remaining=charging?Math.Clamp(frame.ChargeSecondsRemaining??double.NaN,0,5):5;
            // Five seconds is the original SC timer verified on this pinned build. It is
            // separate from the actual module's 15-second hyperspace charging parameters.
            var forecast=ThermalPredictor.Predict(observation.Thermal,frame.GameSha256,frame.LoadoutFingerprint,star.BodyId,heat,
                (frame.Position-star.Centre).Length,(frame.Position-star.Centre).Length,remaining,.5,DateTimeOffset.UtcNow,100,1);
            var begin=!charging&&frame.Fsd==FsdPhase.Idle&&frame.SpeedFraction>=.8&&aimError<=1&&forecast.AllowsCharging;
            var cancel=charging&&(!forecast.Known||!forecast.AllowsCharging);
            var decision=new FlightDecision(cancel?"取消超巡准备":charging?"进入超巡 · 充能中":begin?"进入超巡 · 开始充能":"进入超巡 · 对准外侧并加速",outward,aimError<=8?1:0,begin,cancel,false,forecast.PeakPercent,
                forecast.Known?forecast.Reason:"等待原生热源采样。"){EnterSupercruise=true};
            try{await ApplyAsync(decision,frame.Sequence,selectedWaypoint??throw new InvalidOperationException("请先锁定下一跳。"),frame.ObservedAt.AddMilliseconds(300),token);}
            catch(InvalidOperationException error)when(begin&&error.Message=="Supercruise entry requires verified outward full-speed departure")
            {
                // The FSD observation can be newer than the sequence-bound physics
                // snapshot at the exact eligibility crossing. Keep accelerating and
                // reread; the native gate remains authoritative and no pulse was sent.
                await Task.Delay(40,token);continue;
            }
            catch(Exception error)when(NavigationRecovery.CanRetry(error,token))
            {await Task.Delay(40,token);continue;}
            if(begin)startedSupercruise=true;
            if(cancel)throw new InvalidOperationException("预计进入超巡时热量超限,已取消自有充能。");
            await Task.Delay(40,token);
        }
        throw new TimeoutException("准备进入超巡超时,已停止控制。");
    }
    public async Task ApplyAsync(FlightDecision decision,long expectedSequence,NavWaypoint next,DateTimeOffset expiresAt,CancellationToken token)
    {
        RequireConnectionReady();
        if(ensureFlightProtection is not null)await ensureFlightProtection(token);
        if((decision.BeginCharging||decision.PermitJump)&&selectedSystem!=next.SystemAddress)throw new InvalidOperationException("充能前必须先在游戏中锁定并确认下一跳。");
        if(decision.PermitJump&&decision.Throttle!=1)throw new InvalidOperationException("实际跃迁前必须将节流阀提升至 100%。");
        if(expectedSequence<=0||expiresAt>DateTimeOffset.UtcNow.AddMilliseconds(300))throw new InvalidOperationException("导航指令的序号或有效期无效。");
        if(expiresAt<=DateTimeOffset.UtcNow)throw new FlightTelemetryUnavailableException("正在更新飞行数据。",recovery:FlightRecovery.ReadNextFrame);
        if(decision.BeginCharging||decision.PermitJump)
        {
            if(player?.Invoke() is {} state&&(TravelPreparation.HasDeployedEquipment(state)||gameInputs is not null&&!TravelPreparation.StatusConfirmed(state,DateTimeOffset.UtcNow)))
            {
                await ReleaseAsync();
                if(gameInputs is null)throw new InvalidOperationException("武器挂点、货舱或起落架尚未收起,未开始跃迁。");
                await TravelPreparation.EnsureRetractedAsync(gameInputs,()=>player(),token,ExpectedInputEntity());
                throw new FlightTelemetryUnavailableException("展开设备已收起,重新读取姿态后充能。");
            }
            if(gameInputs is not null)
            {
                var input=await gameInputs.ReadAsync(token);
                if(input.EntityId!=ExpectedInputEntity())throw new InvalidOperationException("输入通道与受控飞船身份不一致。");
                GameInputsObserved?.Invoke(input);
                if(!input.Retracted)throw new FlightTelemetryUnavailableException("等待展开设备收起确认。");
                // The extra asynchronous read can cross the original frame deadline.
                if(expiresAt<=DateTimeOffset.UtcNow)throw new FlightTelemetryUnavailableException("正在更新飞行数据。",recovery:FlightRecovery.ReadNextFrame);
            }
        }
        double[]? steeringAxes=null;
        AttitudeInput? applicationSteering=null;
        if(decision.Direction is {} direction)
        {
            if(!SupportsApplicationSteeringAxes)throw new InvalidOperationException("当前交互接口不支持按飞船计算转向,请在结束游玩后更新游戏插件。");
            if(LastObservation?.Frame is not {} attitudeFrame||attitudeFrame.Sequence!=expectedSequence)
                throw new FlightTelemetryUnavailableException("等待同一飞行帧的三轴姿态。");
            var controlAxes=bankedAttitude.Update(attitudeFrame,direction,decision.UpDirection??attitudeFrame.Up,controlRoll:decision.UpDirection is not null);
            applicationSteering=controlAxes;
            steeringAxes=[controlAxes.Yaw,controlAxes.Pitch,controlAxes.Roll];
        }
        else bankedAttitude.Reset();
        ownsControl=true;
        JsonElement result;
        try{result=await pipe.CallAsync(new{op="apply",owner,sequence=expectedSequence,targetSystemAddress=next.SystemAddress,expiresAtUnixMilliseconds=expiresAt.ToUnixTimeMilliseconds(),decision,steeringAxes},token);}
        catch(InvalidOperationException error)when(error.Message==PhysicalInputPreparation.NativeDisabledContextPending)
        {
            await ReleaseAsync();
            await PhysicalInputPreparation.RequireAsync(pipe,token);
            throw new FlightTelemetryUnavailableException("原始物理输入图已确认,正在重新读取当前飞行帧。",error,FlightRecovery.ReadNextFrame);
        }
        catch(InvalidOperationException error)when(error.Message=="Native departure must be outward before FSD charging"&&decision.BeginCharging&&!decision.EnterSupercruise)
        {
            // Compatibility with the loaded plugin's older outward-heading
            // policy. Its ordinary desired-FSD input is already exposed: keep
            // the same steering/commit lease and all software thermal, target,
            // clearance and fuel checks, without restarting or patching it.
            var frame=LastObservation?.Frame;
            if(frame is not {AdaptiveFlightControl:true,GeometryVerified:true,JumpCommitInterlockVerified:true,NativeAllowsCharging:true,CurrentStar:{}}||frame.Sequence!=expectedSequence
                ||frame.TargetSystemAddress!=next.SystemAddress||frame.Fsd!=FsdPhase.Idle||frame.HeatPercent is not <90||frame.NativeFullChargePeak is not <=95
                ||decision.PredictedPeakHeat is not <=95||frame.FuelMain is not {} fuel||frame.FuelNeededToNextRefuel is not {} needed||fuel<needed
                ||frame.Obstacles.Append(frame.CurrentStar).Any(body=>FlightGeometry.Obscures(body,frame.Position,frame.TargetPosition)))throw;
            if(player?.Invoke() is {} current&&(!current.InGame||NavigationSafety.MustStop(current,frame,DateTimeOffset.UtcNow)||TravelPreparation.HasDeployedEquipment(current)))throw;
            var controls=decision with{BeginCharging=false};
            result=await pipe.CallAsync(new{op="apply",owner,sequence=expectedSequence,targetSystemAddress=next.SystemAddress,expiresAtUnixMilliseconds=expiresAt.ToUnixTimeMilliseconds(),decision=controls,steeringAxes},token);
            if(!result.GetProperty("accepted").GetBoolean())throw new InvalidOperationException("游戏未接受对齐控制。");
            var ttl=Math.Min(200,(int)(expiresAt-DateTimeOffset.UtcNow).TotalMilliseconds);
            if(ttl<1)throw new FlightTelemetryUnavailableException("正在更新飞行数据。",recovery:FlightRecovery.ReadNextFrame);
            JsonElement charge;
            try{charge=await pipe.CallAsync(new{op="calibrate.fsd",owner,sequence=expectedSequence,targetSystemAddress=next.SystemAddress,ttlMilliseconds=ttl,desired="beginHyperspace",heatCeilingPercent=90},token);}
            catch(InvalidOperationException pending)when(pending.Message=="Hyperspace calibration requires a matching locked system target and idle FSD")
            {
                var fsd=await pipe.CallAsync(new{op="fsd.describe"},token);
                if(fsd.GetProperty("fresh").GetBoolean()&&!fsd.GetProperty("ownedCharging").GetBoolean()&&fsd.GetProperty("state").GetInt32() is 0 or 1 or 2
                    &&fsd.GetProperty("targetValid").GetBoolean()&&fsd.GetProperty("targetAddress").GetString()==next.SystemAddress.ToString(System.Globalization.CultureInfo.InvariantCulture))
                    throw new FlightTelemetryUnavailableException("等待冷却结束的原生快照同步,下一帧继续充能。",pending);
                throw;
            }
            if(!charge.GetProperty("accepted").GetBoolean())throw new InvalidOperationException("游戏未接受原生充能输入。");
            OriginalChargeRequested?.Invoke(new(expectedSequence,next.SystemAddress,frame.HeatPercent.Value,decision.PredictedPeakHeat.Value,"existing-desired-fsd-input"));
        }
        catch(InvalidOperationException error)when(error.Message=="Navigation reference frame expired; read fresh telemetry and retry")
        {throw new FlightTelemetryUnavailableException("正在更新飞行数据。",error,FlightRecovery.ReadNextFrame);}
        catch(InvalidOperationException error)when(error.Message=="Native target or primary-star envelope changed")
        {throw new FlightTelemetryUnavailableException("正在重新确认下一跳。",error,FlightRecovery.ReselectTarget);}
        if(!result.TryGetProperty("accepted",out var accepted)||accepted.ValueKind!=JsonValueKind.True)throw new InvalidOperationException("游戏未接受本次导航指令。");
        if(applicationSteering is {} confirmed)bankedAttitude.ConfirmApplied(DateTimeOffset.UtcNow,confirmed);
        double? Number(string name)=>result.TryGetProperty(name,out var number)&&number.ValueKind==JsonValueKind.Number?number.GetDouble():null;
        var axes=result.TryGetProperty("steering",out var steering)&&steering.ValueKind==JsonValueKind.Array&&steering.GetArrayLength() is 2 or 3?steering:(JsonElement?)null;
        LastAppliedControl=new(expectedSequence,Number("throttleCommand"),axes?[0].GetDouble(),axes?[1].GetDouble(),Number("predictedPeak"),axes is {} bank&&bank.GetArrayLength()==3?bank[2].GetDouble():null);
        if(LastObservation is {} observation)Observed?.Invoke(observation,decision);
    }
    public async Task ReleaseAsync()
    {
        if(systemDriver?.Active==true)await systemDriver.ReleaseSystemFlightAsync();
        if(!ownsControl)return;
        using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(3));
        while(true)
        {
            var result=await pipe.CallAsync(new{op="release",owner},deadline.Token);
            if(result.TryGetProperty("released",out var released)&&released.ValueKind==JsonValueKind.True){ownsControl=false;bankedAttitude.Reset();return;}
            if(!result.TryGetProperty("cancellationPending",out var pending)||pending.ValueKind!=JsonValueKind.True)throw new InvalidOperationException("游戏未确认释放导航控制。");
            await Task.Delay(50,deadline.Token);
        }
    }
}