using System.Diagnostics; using System.Globalization; using System.Security.Cryptography; using System.Text.Json; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Native; namespace XFEEDToolBox.Core.Navigation; /// Original local target/assist interfaces with expiring application flight inputs. public sealed partial class NativeSystemFlightControlDriver(int processId,string gameHash,Func player,IGameInputChannel? gameInputs=null,Func? ensureFlightProtection=null,bool useNativeSupercruiseAssist=true):ISystemFlightControlDriver,IFlightDepartureDriver { public event Action? DepartureObserved; private readonly NavigationPipeClient pipe=new(processId); private readonly string owner=Guid.NewGuid().ToString("N"); private readonly BankedAttitudeController attitude=new(); private SystemNavigationTarget? selected; private SystemNavigationTarget? nativeSelected; private long? journalShip; private string commander=""; private DateTimeOffset? sessionAt,entryRequestedAt,exitRequestedAt,assistRequestedAt,sceneTransitionAt; private ulong boundEntity,replacesEntity; private long generation; private int owningThread; private bool ownsCruiseAssist; private bool ownsDockingAssist; private bool ownsLaunch; private bool ownsController; private bool nativeDockingThrottle; private bool cooldownFlightControls; private bool cruisePolicyPrepared; private bool emergencyExited; public bool Active=>ownsController; private SystemFlightFrame? last; public JsonElement? LastTelemetry{get;private set;} public event Action? DepartureProgress; private static bool Flag(JsonElement e,string key)=>e.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.True; private static double Number(JsonElement e,string key)=>e.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.Number?value.GetDouble():0; private static Vector3d Vector(JsonElement e,string key){var a=e.GetProperty(key);var v=new Vector3d(a[0].GetDouble(),a[1].GetDouble(),a[2].GetDouble());return v.Finite?v:throw new InvalidDataException("星系内位置无效。");} private static ulong Address(JsonElement e,string key)=>ulong.Parse(e.GetProperty(key).GetString()!,CultureInfo.InvariantCulture); public async Task InspectAsync(CancellationToken token) { 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 ||hello.GetProperty("gameSha256").GetString()!=gameHash||gameHash.Length!=64||!Flag(hello,"verified"))throw new InvalidDataException("星系内驾驶接口与当前游戏构建不匹配。"); owningThread=hello.GetProperty("owningThread").GetInt32(); nativeDockingThrottle=Flag(hello,"nativeDockingThrottle"); cooldownFlightControls=Flag(hello,"localCooldownControls"); var applicationSchema=hello.TryGetProperty("localApplicationFlightSchema",out var schema)&&schema.TryGetInt32(out var schemaValue)?schemaValue:0; var physicalSchema=hello.TryGetProperty("physicalInputSourceGuardSchema",out var physical)&&physical.TryGetInt32(out var physicalValue)?physicalValue:0; var applicationFlight = !useNativeSupercruiseAssist && Flag(hello,"localApplicationFlightControl")&&applicationSchema==1&&physicalSchema==1 && Flag(hello,"localTargets") && Flag(hello,"applicationSteeringAxes") && Flag(hello,"referenceGeometry") && Flag(hello,"adaptiveFlightControl") &&Flag(hello,"nativeSteering")&&Flag(hello,"independentShipHealth")&&Flag(hello,"continuousShipHealth")&&Flag(hello,"chargeAndCommitInterlock") && hello.TryGetProperty("localNavigationSchema",out var localSchema) && localSchema.GetInt32()==1; var originalDocking = dockingResetPipe is not null && requestOriginalDockingStart; return new(gameHash,Flag(hello,"localTargets"),Flag(hello,"localCruiseControl")||applicationFlight, Flag(hello,"localDockingRequests")||originalDocking,Flag(hello,"localNativeAssists")||originalDocking, Flag(hello,"localDockingPaths"),Flag(hello,"localPlanetaryApproach"),hello.GetProperty("evidence").GetString()??"") { ApplicationFlightControlVerified=applicationFlight,ApplicationFlightControlSchema=applicationSchema,UsesNativeSupercruiseAssist=useNativeSupercruiseAssist, OriginalOverchargeConfigured=originalCruiseControl is not null,OriginalDockingConfigured=originalDocking, TelemetryReady=Flag(hello,"inGame")&&Flag(hello,"ready")&&Flag(hello,"continuousPose")&&Flag(hello,"continuousHeat")&&owningThread>0, TelemetryIssue=!Flag(hello,"inGame")?"等待进入驾驶舱场景。":!Flag(hello,"ready")||owningThread<=0?"等待原生观察接口就绪。": !Flag(hello,"continuousPose")||!Flag(hello,"continuousHeat")?"正在恢复当前飞船姿态与热量观测。":null, ControlUnavailableReason=applicationFlight||Flag(hello,"localCruiseControl")?null:useNativeSupercruiseAssist? "当前驱动选择了未开放的游戏超巡辅助,请使用工具箱自主驾驶。":"当前插件缺少已验证的应用驾驶或物理接管契约,请更新游戏内插件。" }; } public async Task SelectAsync(SystemNavigationTarget target,CancellationToken token) { target.Validate();if(target.NativeKey<0||target.NativeKind<=0||target.NativeIdentity.Length!=64)throw new ArgumentException("目标缺少当前游戏目录中的完整身份。"); if(!(await InspectAsync(token)).LocalTargets)throw new NotSupportedException("当前插件尚不支持星系内目标选择。"); var state=player();NavigationSafety.RequireLocalSelection(state,target.SystemAddress); await SelectNativeAsync(target,token);selected=nativeSelected=target;last=null;attitude.Reset();ownsCruiseAssist=ownsDockingAssist=false; journalShip=state.CurrentShipId;commander=state.CommanderId;sessionAt=state.LoadGameAt;boundEntity=replacesEntity=0;cruisePolicyPrepared=false; entryRequestedAt=exitRequestedAt=assistRequestedAt=sceneTransitionAt=null; } private async Task SelectNativeAsync(SystemNavigationTarget target,CancellationToken token) { var result=await pipe.CallAsync(new{op="local.select",owner,key=target.NativeKey,bodyAddress=target.BodyAddress.ToString(CultureInfo.InvariantCulture),kind=target.NativeKind, marketId=target.MarketId??uint.MaxValue,name=target.Name},token); generation=result.GetProperty("generation").GetInt64();var watch=Stopwatch.StartNew(); ownsController=true; while(!result.GetProperty("selected").GetBoolean()) { if(!result.GetProperty("pending").GetBoolean()||watch.Elapsed>TimeSpan.FromSeconds(2))throw new InvalidOperationException("游戏未确认所选的星系内目标。"); await Task.Delay(25,token);result=await pipe.CallAsync(new{op="local.status",owner,generation},token); } MatchTarget(result.GetProperty("target"),target); } public async Task PrepareAsync(CancellationToken token) { if(ensureFlightProtection is not null)await ensureFlightProtection(token); if(selected is null)throw new InvalidOperationException("请先选择星系内目的地。"); void RequirePreparationIdentity() { token.ThrowIfCancellationRequested();var current=player(); if(!current.InGame||current.CommanderId!=commander||current.CurrentShipId!=journalShip||current.LoadGameAt!=sessionAt ||current.Navigation.CurrentSystemAddress!=selected.SystemAddress||NavigationSafety.UnhealthyHull(current)) throw new InvalidOperationException("准备星系内驾驶时飞船、会话或安全状态改变。"); if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException(); } async Task PreserveArrivalAsync(CancellationToken cancellation) { var observation=Stopwatch.StartNew(); while(true) { cancellation.ThrowIfCancellationRequested();RequirePreparationIdentity(); if(TravelPreparation.AtDockedDestination(player(),selected))return true; var observed=await ReadSystemFlightAsync(cancellation);RequirePreparationIdentity(); if(observed.PlayerOverride)throw new InvalidOperationException("玩家已主动接管星系内驾驶。"); if(TravelPreparation.AtDockedDestination(player(),selected)||TravelPreparation.PreserveOriginalLanding(observed,selected,DateTimeOffset.UtcNow))return true; var local=LastTelemetry!.Value; if(!NativeSystemFlightObservation.ObserveOriginalArrival(local.GetProperty("docking"),local.GetProperty("rawDockingState").GetInt32()))return false; // Native states 5/7 are physically docked, before Journal may // arrive. An active original duty without a fresh grant is also // observation-only; neither state permits retracting its gear. if(observation.Elapsed>=TimeSpan.FromSeconds(30))throw new FlightTelemetryUnavailableException("正在等待原版降落与实际停靠记录同步,不发送设备输入。"); await Task.Delay(80,cancellation); } } RequirePreparationIdentity();if(TravelPreparation.AtDockedDestination(player(),selected))return; if(player() is {Docked:true} docked&&docked.MarketId!=selected.MarketId) { await DepartDockedStationAsync(token); RequirePreparationIdentity(); } SystemFlightFrame frame;var watch=Stopwatch.StartNew(); while(true) { try{frame=await ReadSystemFlightAsync(token);break;} catch(Exception error)when(NavigationRecovery.CanRetry(error,token)&&watch.Elapsed=until)throw new TimeoutException("游戏超巡辅助尚未交还控制。"); await ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting,"正在接续工具箱自主超巡。",null,null, SystemNavigationAction.CancelOwnedAssists|SystemNavigationAction.ReleaseFlightAxes),frame,selected,frame.ObservedAt.AddMilliseconds(250),token); await Task.Delay(60,token); } } } public Task DepartAsync(CancellationToken token) =>FlightControlCleanup.RunAsync(()=>DepartCoreAsync(token),ReleaseOwnedDepartureControlsAsync); private Task ReleaseOwnedDepartureControlsAsync()=>ownsController||ownsLaunch?ReleaseSystemFlightAsync():Task.CompletedTask; private async Task DepartCoreAsync(CancellationToken token) { if(ensureFlightProtection is not null)await ensureFlightProtection(token); await DepartDockedStationAsync(token,restoreDestination:false); } private async Task DepartDockedStationAsync(CancellationToken token,bool restoreDestination=true,SystemNavigationTarget? resumeOrigin=null, DateTimeOffset? resumedUndockedAt=null,bool returnToOrigin=false) { var destination=selected!;var initial=player(); if(!NavigationAssists.From(initial).AdvancedDocking)throw new NotSupportedException("当前飞船缺少已启用的高级自动起降模块,手动离场尚未完成验证。"); using var source=new NativeCurrentSystemSource(processId,gameHash,player);var catalogue=await source.ReadLocationsAsync(token); var origin=resumeOrigin??StationDepartureTarget.Resolve(catalogue,initial); await SelectAsync(origin,token);var started=resumedUndockedAt??GameJournalClock.Now(player(),DateTimeOffset.UtcNow); var resumedAge=resumedUndockedAt is not null?GameJournalClock.Now(player(),DateTimeOffset.UtcNow)-started:TimeSpan.Zero; if(resumedAgewatch.Elapsed+resumedAge; if(resumeOrigin is null)await RequestLaunchAsync(token); TimeSpan? takeoverRequestedAt=null;string? lastMessage=null;var departurePath=new StationDeparturePathEvidence();var clearingMassLock=false; void Report(string message) { if(message==lastMessage)return;lastMessage=message; if(DepartureProgress is not {} handlers)return; foreach(Action handler in handlers.GetInvocationList())try{handler(message);}catch{/* UI reporting cannot change departure authority. */} } var nextObservation=TimeSpan.Zero; void ObserveDeparture(PlayerState current,SystemFlightFrame? frame,JsonElement docking,string? error=null) { if(Elapsed() handler in handlers.GetInvocationList())try{handler(observation);}catch{/* Optional diagnostics cannot affect departure. */} } while(true) { token.ThrowIfCancellationRequested();var state=player(); if(state.CurrentShipId!=initial.CurrentShipId||state.LoadGameAt!=initial.LoadGameAt||state.CommanderId!=initial.CommanderId||NavigationSafety.UnhealthyHull(state)) throw new InvalidOperationException("离场中的飞船或安全状态改变。"); if(NavigationSafety.HasExternalThreat(state))throw new FlightThreatPauseException(); // An uncertain cancellation is observed once, never resubmitted. // Recovery begins well before the station's five-minute deadline. if(takeoverRequestedAt is {} requested&&Elapsed()-requested>=StationDepartureTakeoverPolicy.CancellationObservationLimit) throw new StationDepartureRecoveryException(origin.MarketId!.Value,Elapsed()); try { var frame=await ReadSystemFlightAsync(token); var dock=LastTelemetry!.Value.GetProperty("docking"); var nativeSource=StationDepartureContinuity.NativeDutySourceVerified(dock); departurePath.Observe(frame,origin,state,started,DateTimeOffset.UtcNow,nativeSource, Flag(dock,"nativeLaunchActive"),Flag(dock,"nativeLaunchRequested"),takeoverRequestedAt is not null); ObserveDeparture(state,frame,dock); if(resumeOrigin is not null&&returnToOrigin&&frame.Mode==LocalFlightMode.Normal&&!frame.PlayerOverride&&!frame.ExternalThreat &&Flag(dock,"verified")&&StationDepartureContinuity.NativeLaunchInactive(dock)) {Report("原版离场任务已结束,保持正常同站返航停靠入口;未确认或执行向外推进。");break;} var action=StationDepartureTakeoverPolicy.Decide(frame,origin,state,started,Elapsed(),DateTimeOffset.UtcNow, nativeSource,Flag(dock,"nativeLaunchActive"),Flag(dock,"nativeLaunchRequested"),Flag(dock,"nativeDutyActive"),Flag(dock,"verified"),departurePath); if(action==StationDepartureAction.ManualTakeover)throw new InvalidOperationException("玩家已主动接管离站,自动航程已停止。"); if(action==StationDepartureAction.RecoverSession)StationDepartureWatchdog.Check(origin.MarketId!.Value,Elapsed()); if(action==StationDepartureAction.Completed) { ownsLaunch=false; Report(takeoverRequestedAt is null?"已确认离站与质量锁定解除,正在接续原航程。":"已确认原版离场任务退出,工具箱接管后续自动驾驶。"); break; } if(action==StationDepartureAction.ContinueVerifiedDeparture) { clearingMassLock=true; Report("原版已完成安全出站,正沿已验证出站航向固定节流前进,直到解除质量锁定。"); // No steering, radial escape vector, boost or cancellation. // Renew only on the same fresh outward path and body envelope. await ApplySystemFlightAsync(new(SystemNavigationPhase.Preparing,"沿原版出站路径解除质量锁定",null,StationDeparturePathEvidence.ContinuationThrottle), frame,origin,frame.ObservedAt.AddMilliseconds(250),token); } else if(clearingMassLock){clearingMassLock=false;await SuspendSystemFlightAsync();} if(action==StationDepartureAction.CancelNativeForTakeover&&takeoverRequestedAt is null) { Report("离站接近等待期限;已确认站体质量锁定解除,正在接管自动驾驶。"); takeoverRequestedAt=Elapsed(); await ActionAsync("cancelLaunch",frame,frame.ObservedAt.AddMilliseconds(250),token); } else if(action!=StationDepartureAction.ContinueVerifiedDeparture)Report("正在等待原版安全离场;站内路径未确认时保持原版驾驶。"); } catch(Exception error)when(NavigationRecovery.CanRetry(error,token)) { ObserveDeparture(state,null,default,error.ToString()); StationDepartureWatchdog.Check(origin.MarketId!.Value,Elapsed()); await SuspendSystemFlightAsync(); } await Task.Delay(80,token); } await ReleaseSystemFlightAsync();if(restoreDestination)await SelectAsync(destination,token); } private static void MatchTarget(JsonElement actual,SystemNavigationTarget target) { if(actual.GetProperty("key").GetInt32()!=target.NativeKey||Address(actual,"bodyAddress")!=target.BodyAddress||actual.GetProperty("kind").GetInt32()!=target.NativeKind ||actual.GetProperty("marketId").GetUInt32()!=(target.MarketId??uint.MaxValue)||actual.GetProperty("name").GetString()!=target.Name) throw new InvalidOperationException("游戏选中的地点已经改变,已停止星系内驾驶。"); } private async Task RebindSceneAsync(JsonElement raw,PlayerState state,ulong entity,CancellationToken token) { var target=selected!;var now=DateTimeOffset.UtcNow;var journalNow=GameJournalClock.Now(state,now); bool Recent(DateTimeOffset? requested,DateTimeOffset? confirmed)=>requested is {} request&&confirmed is {} actual &&actual>=(request+(journalNow-now)).AddSeconds(-1)&&actual<=journalNow&&journalNow-actualx is {} requested&&now-requestedrow.Name==target.Name&&row.NativeKind==target.NativeKind &&(target.MarketId is {} market?row.MarketId==market:row.WorldAddress==target.BodyAddress); var current=snapshot.Locations.SingleOrDefault(x=>x.Selected); if(snapshot.EntityId!=entity||current is not null&&!SameDestination(current))throw new InvalidOperationException("切换后的当前目标与原目的地不一致。"); var row=snapshot.Locations.SingleOrDefault(SameDestination)??throw new FlightTelemetryUnavailableException("正在恢复目的地目录。"); var binding=target with{NativeKey=row.Key,NativeKind=row.NativeKind,NativeIdentity=row.Identity,BodyAddress=row.WorldAddress}; await ReleaseSystemFlightAsync(); try{await SelectNativeAsync(binding,token);} catch(InvalidOperationException error)when(error.Message=="Stop flight control before selecting a local destination") {throw new FlightTelemetryUnavailableException("正在等待旧驾驶输入释放并恢复同一目的地。");} nativeSelected=binding; replacesEntity=boundEntity;boundEntity=entity;sceneTransitionAt=entered?state.Navigation.SupercruiseEnteredAt:state.Navigation.SupercruiseExitedAt; if(entered)entryRequestedAt=null;if(exited)exitRequestedAt=assistRequestedAt=null; attitude.Reset(); // Only this validated scene change may attach protection to the new // local entity. Health monitoring remains alive while old inputs bar. if(ensureFlightProtection is not null)await ensureFlightProtection(token); await PhysicalInputPreparation.RequireAsync(pipe,token); } public async Task ReadSystemFlightAsync(CancellationToken token) { var target=selected??throw new InvalidOperationException("星系内目标尚未确认。");var state=player(); var raw=await pipe.CallAsync(new{op="read"},token);var entity=Address(raw,"entityId"); if(!Flag(raw,"inGame"))throw new FlightTelemetryUnavailableException("正在等待驾驶舱场景切换完成。"); var ship=state.Ships.SingleOrDefault(s=>s.ShipId==state.CurrentShipId); if(ship?.ConfigurationAt is null||ship.ConfigurationChanged||!Flag(raw,"localPlayerShipVerified"))throw new FlightTelemetryUnavailableException("等待当前飞船的完整驾驶状态。"); var fingerprint=NativeSystemFlightObservation.LoadoutFingerprint(ship); if(last is {} previous&&previous.LoadoutFingerprint!=fingerprint)throw new InvalidOperationException("飞船装配已改变,已停止星系内驾驶。"); if(boundEntity!=0&&entity!=boundEntity)await RebindSceneAsync(raw,state,entity,token); else if(boundEntity==0)boundEntity=entity; if(!ownsLaunch&&state.Docked&&state.MarketId==target.MarketId&&target.NeedsDocking&&last is {} arrival&&arrival.ShipEntity==entity) { // Actual Docked Journal identity is independent of the navigation // selection, which the original game may clear after docking. return last=arrival with{Sequence=raw.GetProperty("sequence").GetInt64(),ObservedAt=DateTimeOffset.FromUnixTimeMilliseconds(raw.GetProperty("observedAtUnixMilliseconds").GetInt64()), InGame=state.InGame,LoadoutFingerprint=fingerprint,Mode=LocalFlightMode.Docked,DockedMarketId=state.MarketId,Speed=0}; } JsonElement local; try{local=await pipe.CallAsync(new{op="local.read"},token);} catch(InvalidOperationException error)when(error.Message=="Local target telemetry unavailable or stale") { LocalFlightTelemetryStatus? status=null; try { var report=await pipe.CallAsync(new{op="local.status",owner,generation},token); status=LocalFlightTelemetryStatus.Parse(report,generation,nativeSelected??target); } catch(InvalidDataException statusError){throw new InvalidOperationException("当前地点诊断身份未通过核对,未重新选择目标。",statusError);} catch(Exception statusError)when(NavigationRecovery.CanRetry(statusError,token)){/* Retain the original sampling failure when the read-only status is also in flight. */} if(status?.TargetChanged==true)throw new FlightDestinationChangedException(target.Name,status.ActualTarget,error); Exception samplingFailure=status is null?error:new IOException($"local.status: sampleStage={status.SampleStage}, selected={status.Selected}, telemetryReady={status.TelemetryReady}",error); throw new FlightTelemetryUnavailableException("正在重新核对星系内目标与飞行数据。",samplingFailure); } LastTelemetry=local;MatchTarget(local.GetProperty("target"),nativeSelected??target); if(!TravelPreparation.StatusConfirmed(state,DateTimeOffset.UtcNow))throw new FlightTelemetryUnavailableException("正在同步当前驾驶舱状态。"); var world=Address(local,"referenceWorldAddress");var at=DateTimeOffset.FromUnixTimeMilliseconds(local.GetProperty("observedAtUnixMilliseconds").GetInt64());var now=DateTimeOffset.UtcNow; if(Address(local,"entityId")!=entity||world!=Address(raw,"referenceWorldAddress")||NativeFrameProjector.SystemAddressFromBody(world)!=target.SystemAddress ||!Flag(local,"geometryVerified")||!Flag(local,"velocityVerified")||now-at>TimeSpan.FromMilliseconds(300)||at>now) throw new FlightTelemetryUnavailableException("正在同步星系内目标、参考系和飞船速度。"); var sc=Flag(local,"inSupercruise");var rawState=local.GetProperty("rawFsdState").GetInt32();var position=Vector(local,"position");var destination=Vector(local,"targetPosition"); var forward=Vector(local,"forward");var up=Vector(local,"up");var assist=local.GetProperty("supercruiseAssist");var dock=local.GetProperty("docking"); var installed=NavigationAssists.From(state);var obscured=Flag(assist,"obscured");Vector3d? clear=null; var geometry=raw.GetProperty("geometry"); var targetExclusionRadius=StationDepartureEnvelope.ReadRadius(geometry,world,entity,target,destination,now); // Only the original assist may report obstruction. Geometry is used // afterwards to choose a clear bypass, never to take over its controls. if(obscured&&Flag(geometry,"fresh")&&Flag(geometry,"referenceFrameVerified")&&Address(geometry,"worldAddress")==world) { var offset=position-Vector(geometry,"position"); var bodies=new List(); StarEnvelope? blocking=null; foreach(var body in geometry.GetProperty("nearbyBodies").EnumerateArray()) { if(Address(body,"bodyAddress")==target.PlanetAddress&&target.IsSurface)continue; var centre=Vector(body,"centre")+offset;var radius=body.GetProperty("exclusionRadius").GetDouble(); if(!double.IsFinite(radius)||radius<=0)throw new FlightTelemetryUnavailableException("等待天体避障包络。"); var envelope=new StarEnvelope(0,"",centre,radius,null,null);bodies.Add(envelope); if(FlightGeometry.Obscures(envelope,position,destination))blocking??=envelope; } clear=FlightGeometry.ClearPerpendicularDirection(bodies,position,destination, last?.ClearDirection??(blocking is {} obstacle?FlightGeometry.FastClearDirection(obstacle,position,destination,up):up)); } if(obscured&&clear is null)throw new FlightTelemetryUnavailableException("正在寻找被天体遮挡目标的进近方向。"); var pad=local.GetProperty("rawPad").GetInt32();DockingClearance? clearance=null; var dockingState=local.GetProperty("rawDockingState").GetInt32(); if(target.MarketId is {} market&&Flag(dock,"verified")&&Flag(dock,"clearanceGranted")&&dockingState is 3 or 4&&pad is >=0 and <255) // This is a short-lived permission observation. Every renewed lease // requires the original game to still report a valid grant and pad. clearance=new(market,DockingPermission.Granted,pad+1,at,at.AddMilliseconds(300)); var frame=new SystemFlightFrame{Sequence=local.GetProperty("sequence").GetInt64(),ObservedAt=at,GameSha256=gameHash,LoadoutFingerprint=fingerprint,ShipEntity=entity,ReplacesShipEntity=replacesEntity,SceneTransitionAt=sceneTransitionAt, SystemAddress=target.SystemAddress,ReferenceWorld=world,SelectedTarget=target,GeometryVerified=true,InGame=Flag(raw,"inGame")&&state.InGame,PlayerOverride=Flag(raw,"playerOverride"), ExternalThreat=NavigationSafety.HasExternalThreat(state),Mode=NativeSystemFlightObservation.Mode(state.Docked&&state.MarketId==target.MarketId,sc,rawState, Flag(dock,"verified")&&dockingState is 5 or 7,cooldownFlightControls), DockedMarketId=state.Docked?state.MarketId:null, Position=position,Velocity=Vector(local,"velocity"),Forward=forward,Up=up,AngularVelocityDegrees=Flag(local,"angularVelocityVerified")?Vector(local,"angularVelocity"):null,TargetPosition=destination,TargetVelocity=Vector(local,"targetVelocity"),TargetExclusionRadius=targetExclusionRadius, TargetDistance=local.GetProperty("distanceMetres").GetDouble(),Speed=local.GetProperty("speedMetresPerSecond").GetDouble(),Throttle=local.GetProperty("throttle").GetDouble(), EnginePips=state.Status?.Pips is {Count:3} pips?pips[1]:null, MaximumNormalSpeed=local.GetProperty("normalSpeedLimit").GetDouble(),MaximumCruiseSpeed=local.GetProperty("cruiseSpeedLimit").GetDouble(), CruiseParametersVerified=Flag(local,"cruiseParametersVerified"),CruiseApproachSeconds=Number(local,"cruiseApproachSeconds"),CruiseApproachThrottle=Number(local,"cruiseApproachThrottle"), SafeDisengage=Flag(local,"safeDisengage"),SafeDropDistance=Number(local,"safeDropDistance"),SafeDropSpeed=Number(local,"safeDropSpeed"), BrakingAcceleration=local.GetProperty("accelerationLowerBound").GetDouble(),TranslationAcceleration=local.GetProperty("accelerationLowerBound").GetDouble(),LegalApproachSpeed=100, AssistAlignmentDegrees=5,TargetObscured=obscured,ClearDirection=clear,FsdReady=rawState==2&&!sc&&((state.Status?.Flags??0)&(1L<<16))==0, LandingGearDown=((state.Status?.Flags??0)&4)!=0,ThrusterBoostAvailable=Flag(local,"normalBoostAvailable"),Assists=installed.Available(Flag(dock,"computerAvailable"),useNativeSupercruiseAssist&&Flag(assist,"available")),HeatPercent=raw.GetProperty("heatPercent").GetDouble(),Clearance=clearance, SupercruiseAssist=NativeSystemFlightObservation.CruiseAssist(Flag(assist,"available"),Flag(assist,"enabled"),Flag(assist,"engaged"),Flag(assist,"matchesTarget"),sc), DockingAssist=NativeSystemFlightObservation.DockingAssist(installed.AutoDock&&Flag(dock,"computerAvailable"),Flag(dock,"verified"), clearance is not null&&Flag(dock,"nativeDutyActive")&&!Flag(dock,"nativeLaunchActive")), DockingAssistEnabled=installed.AutoDock&&Flag(dock,"computerAvailable")&&nativeDockingThrottle&&NativeSystemFlightObservation.DockingInputZeroConfirmed(local)}; if(cruisePolicyPrepared&&!useNativeSupercruiseAssist&&(Flag(assist,"enabled")||Flag(assist,"engaged"))) throw new InvalidOperationException("游戏超巡辅助重新取得控制,已停止工具箱驾驶以核对输入来源。"); if(originalCruiseControl is not null&&!useNativeSupercruiseAssist&&sc) { var thermalExpiry=await originalCruiseControl.ReadThermalControlExpiryAsync(frame,token); frame=frame with{OriginalScoThermalControlVerified=thermalExpiry is not null,OriginalScoThermalControlExpiresAt=thermalExpiry}; } last=frame;return frame; } public async Task ApplySystemFlightAsync(SystemFlightDecision decision,SystemFlightFrame reference,SystemNavigationTarget target,DateTimeOffset expiresAt,CancellationToken token) { if(ensureFlightProtection is not null)await ensureFlightProtection(token); if(selected is null||!selected.SameIdentity(target)||last?.Sequence!=reference.Sequence)throw new InvalidOperationException("驾驶指令与目标或飞行帧不一致。"); if(expiresAt<=DateTimeOffset.UtcNow)throw new FlightTelemetryUnavailableException("正在刷新星系内飞行数据。"); await UpdateOriginalCruiseAsync(decision,reference,nativeSelected??target,expiresAt,token); if(decision.Actions.HasFlag(SystemNavigationAction.ExitForFailedHandoff)) { emergencyExited=await Session.NativeFlightTestProtection.HandleFailedHandoffAsync(processId); if(emergencyExited)ownsController=false;return; } var actions=decision.Actions; if((actions&SystemNavigationAction.CancelOwnedAssists)!=0) { // The selected target and controller ownership have already been // validated above. Even the first frame can contain a surviving // original duty whose module became unavailable before selection. if(LastTelemetry is {} observed) { ownsCruiseAssist|=NativeSystemFlightObservation.CruiseNeedsCancellation(observed.GetProperty("supercruiseAssist")); ownsDockingAssist|=NativeSystemFlightObservation.DockingNeedsCancellation(observed.GetProperty("docking")); } if(ownsCruiseAssist) { if(LastTelemetry is {} cruise&&NativeSystemFlightObservation.CruiseNeedsCancellation(cruise.GetProperty("supercruiseAssist"))) { await pipe.CallAsync(new{op="local.axes.release",owner,generation},token); await ActionAsync("cancelSupercruiseAssist",reference,expiresAt,token); // A request acknowledgement is not an inactive-duty // observation. Re-read before issuing competing inputs. return; } ownsCruiseAssist=false; } if(ownsDockingAssist) { if(LastTelemetry is {} docking&&NativeSystemFlightObservation.DockingNeedsCancellation(docking.GetProperty("docking"))) { await pipe.CallAsync(new{op="local.axes.release",owner,generation},token); await ActionAsync("cancelDockingAssist",reference,expiresAt,token); return; } ownsDockingAssist=false; } } if(decision.Translation is {Length:>0})throw new NotSupportedException("空间站手动进场路径尚未通过验收。"); if((decision.Actions&SystemNavigationAction.ReleaseFlightAxes)!=0) { // Adopt an already engaged original duty for our selected target. // A later module failure must still relinquish that duty before // application control can resume. ownsCruiseAssist|=reference.Assists.Supercruise&&reference.SupercruiseAssist==NativeAssistState.Engaged; ownsDockingAssist|=reference.Assists.AutoDock&&reference.DockingAssist==NativeAssistState.Engaged; attitude.Reset(); await pipe.CallAsync(new{op="local.axes.release",owner,generation},token); } else { var aim=decision.Direction is {} direction?attitude.Update(new(){Sequence=reference.Sequence,ObservedAt=reference.ObservedAt,SystemAddress=reference.SystemAddress, LoadoutFingerprint=reference.LoadoutFingerprint,InSupercruise=reference.Mode==LocalFlightMode.Supercruise, EnginePips=reference.EnginePips,SpeedFraction=reference.Speed/Math.Max(1,reference.Mode==LocalFlightMode.Supercruise?reference.MaximumCruiseSpeed:reference.MaximumNormalSpeed), Forward=reference.Forward,Up=reference.Up},direction,decision.UpDirection??reference.Up,reference.AngularVelocityDegrees,decision.UpDirection is not null):new AttitudeInput(); try { await pipe.CallAsync(new{op="local.apply",owner,generation,sequence=reference.Sequence,expiresAtUnixMilliseconds=expiresAt.ToUnixTimeMilliseconds(), axes=new[]{0.0,0,0,aim.Yaw,aim.Pitch,aim.Roll},throttle=decision.Throttle??-1, yieldOnDockingGrant=reference.Assists.AutoDock&&target.NeedsDocking&&decision.Phase is SystemNavigationPhase.NativeLanding or SystemNavigationPhase.RequestingDocking or SystemNavigationPhase.AwaitingClearance},token); } catch(InvalidOperationException error)when(error.Message==PhysicalInputPreparation.NativeDisabledContextPending) { await pipe.CallAsync(new{op="local.axes.release",owner,generation},token); if(originalCruiseControl is not null)await originalCruiseControl.ReleaseAsync(token); await PhysicalInputPreparation.RequireAsync(pipe,token); throw new FlightTelemetryUnavailableException("原始物理输入图已确认,正在重新读取当前飞行帧。",error); } if(decision.Direction is not null)attitude.ConfirmApplied(DateTimeOffset.UtcNow,aim); } if((actions&SystemNavigationAction.EnterSupercruise)!=0){await ActionAsync("enterSupercruise",reference,expiresAt,token);entryRequestedAt=DateTimeOffset.UtcNow;} if((actions&SystemNavigationAction.BoostThrusters)!=0&&reference.Mode==LocalFlightMode.Normal&&reference.ThrusterBoostAvailable &&reference.TargetDistance>SystemFlightNavigator.DockingContactDistance&&decision.Throttle==1) await ActionAsync("boostThrusters",reference,expiresAt,token); if((actions&SystemNavigationAction.RequestDocking)!=0)await ActionAsync("requestDocking",reference,expiresAt,token); if((actions&SystemNavigationAction.EnableSupercruiseAssist)!=0){ownsCruiseAssist=true;await ActionAsync("enableSupercruiseAssist",reference,expiresAt,token);assistRequestedAt=DateTimeOffset.UtcNow;} if((actions&SystemNavigationAction.EnableDockingAssist)!=0) { if(!nativeDockingThrottle)throw new NotSupportedException("当前插件尚未接入已验证的停靠节流阀归零接口,请更新插件后重启游戏。"); ownsDockingAssist=true; await ActionAsync("zeroThrottleForDocking",reference,expiresAt,token); } if((actions&SystemNavigationAction.SafeDisengage)!=0) { if(!reference.SafeDisengage||reference.TargetDistance>=reference.SafeDropDistance||reference.Speed>=reference.SafeDropSpeed)throw new InvalidOperationException("尚未满足游戏原生安全脱离条件。"); await ActionAsync("safeDisengage",reference,expiresAt,token);exitRequestedAt=DateTimeOffset.UtcNow; } if((actions&SystemNavigationAction.DeployLandingGear)!=0) { if(gameInputs is null)throw new NotSupportedException("起落架输入通道尚未连接。"); var inputs=await gameInputs.ReadAsync(token);if(inputs.EntityId!=reference.ShipEntity)throw new InvalidDataException("起落架输入不是当前飞船。");await gameInputs.SetAsync(inputs,"landingGear",true,token); } } private async Task ActionAsync(string action,SystemFlightFrame frame,DateTimeOffset deadline,CancellationToken token) { if(deadline<=DateTimeOffset.UtcNow)throw new FlightTelemetryUnavailableException("正在刷新星系内操作数据。"); if(action=="zeroThrottleForDocking"&&dockingResetPipe is not null){await OriginalDockingResetAsync(frame,deadline,token);return;} var queued=await pipe.CallAsync(new{op="local.action",owner,generation,sequence=frame.Sequence,expiresAtUnixMilliseconds=deadline.ToUnixTimeMilliseconds(),action},token); var actionGeneration=queued.GetProperty("actionGeneration").GetInt64(); while(true) { var status=await pipe.CallAsync(new{op="local.action.status",owner,generation,actionGeneration},token); if(Flag(status,"applied"))return; if(!Flag(status,"pending")) { if(action=="requestDocking")throw new DockingRequestNotAppliedException(); throw new FlightTelemetryUnavailableException("游戏条件尚未就绪,正在重新读取并重试。"); } await Task.Delay(15,token); } } /// Send the original player speed-zero input without approach thrust. public async Task SetNormalThrottleZeroAsync(SystemFlightFrame frame,CancellationToken token) { if(frame.Mode!=LocalFlightMode.Normal||frame.PlayerOverride||frame.SupercruiseAssist==NativeAssistState.Engaged) throw new InvalidOperationException("普通飞行节流阀归零需要当前正常飞行状态。"); if(ensureFlightProtection is not null)await ensureFlightProtection(token); await pipe.CallAsync(new{op="local.axes.release",owner,generation},token); await ActionAsync("setNormalThrottleZero",frame,frame.ObservedAt.AddMilliseconds(250),token); } /// Ordinary low-speed exit for cancellation/recovery; does not attest arrival at a station. public async Task LeaveSupercruiseSlowlyAsync(SystemFlightFrame frame,CancellationToken token) { if(frame.Mode!=LocalFlightMode.Supercruise||frame.Speed>100000||frame.SupercruiseAssist==NativeAssistState.Engaged) throw new InvalidOperationException("等待超巡减速后安全退出。"); var target=selected??throw new InvalidOperationException("星系内目标尚未确认。"); var deadline=frame.ObservedAt.AddMilliseconds(250); await ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting,"超巡减速退出",null,0),frame,target,deadline,token); await ActionAsync("leaveSupercruiseSlowly",frame,deadline,token); exitRequestedAt=DateTimeOffset.UtcNow; } public async Task RequestLaunchAsync(CancellationToken token) { if(ensureFlightProtection is not null)await ensureFlightProtection(token); var target=selected??throw new InvalidOperationException("请先选择当前停靠站点。");var state=player(); if(!state.Docked||state.MarketId!=target.MarketId||!NavigationAssists.From(state).AdvancedDocking)throw new InvalidOperationException("自动起飞需要当前停靠站点和已启用的高级自动起降模块。"); var requested=DateTimeOffset.UtcNow;var requestedJournalAt=GameJournalClock.Now(state,requested); while(true) { token.ThrowIfCancellationRequested(); var current=player(); if(current.CurrentShipId!=state.CurrentShipId||current.LoadGameAt!=state.LoadGameAt||current.CommanderId!=state.CommanderId||NavigationSafety.UnhealthyHull(current)) throw new InvalidOperationException("起飞前的飞船或安全状态改变。"); if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException(); if(!current.Docked&¤t.Navigation.UndockedAt>=requestedJournalAt.AddSeconds(-1) &¤t.Navigation.UndockedAt<=GameJournalClock.Now(current,DateTimeOffset.UtcNow).AddSeconds(5))return; try { var frame=await ReadSystemFlightAsync(token); // Cancellation may arrive after the original launch request has // been queued but before its acknowledgement reaches this task. ownsLaunch=true; await ActionAsync("launch",frame,frame.ObservedAt.AddMilliseconds(250),token);return; } catch(Exception error)when(NavigationRecovery.CanRetry(error,token)&&DateTimeOffset.UtcNow-requestedawait FlightControlCleanup.RunAllAsync( ()=>originalCruiseControl?.ReleaseAsync(cause:"telemetry-suspend")??Task.CompletedTask, async()=>{attitude.Reset();await pipe.CallAsync(new{op="local.suspend",owner});}); public async Task ReleaseSystemFlightAsync() =>await FlightControlCleanup.RunAllAsync( ()=>originalCruiseControl?.ReleaseAsync()??Task.CompletedTask, CancelOriginalDockingResetAsync, ReleaseOriginalAssistsAsync, ReleasePrimaryFlightInputsAsync); private async Task ReleaseOriginalAssistsAsync() { if(emergencyExited)return; if(ownsLaunch) { var watch=Stopwatch.StartNew(); DateTimeOffset? clearSince=null; while(true) { try { // Do not send a cancellation against a cached, expired // frame or leave a just-queued launch request alive. var launch=await ReadSystemFlightAsync(CancellationToken.None); if(launch.PlayerOverride)break; if(LastTelemetry is {} local&&StationDepartureContinuity.NativeLaunchInactive(local.GetProperty("docking"))) { // The launch acknowledgement can precede the next // observation of its one-shot request. One empty // pre-request frame must not finish cancellation. clearSince??=DateTimeOffset.UtcNow; if(watch.Elapsed>=TimeSpan.FromMilliseconds(500)&&DateTimeOffset.UtcNow-clearSince>=TimeSpan.FromMilliseconds(250))break; } else if(LastTelemetry is {} pending&&StationDepartureContinuity.NativeDutySourceVerified(pending.GetProperty("docking")) &&NativeSystemFlightObservation.LaunchNeedsCancellation(pending.GetProperty("docking"))) { clearSince=null; await ActionAsync("cancelLaunch",launch,launch.ObservedAt.AddMilliseconds(250),CancellationToken.None); } else clearSince=null; if(watch.Elapsed>TimeSpan.FromSeconds(3))throw new InvalidOperationException("游戏尚未确认自动起飞已取消。"); } catch(Exception error)when(NavigationRecovery.CanRetry(error,CancellationToken.None)&&watch.ElapsedTimeSpan.FromSeconds(3))throw new InvalidOperationException("等待游戏释放自有驾驶输入。");await Task.Delay(25);} } } finally{attitude.Reset();ownsCruiseAssist=ownsDockingAssist=false;ownsLaunch=false;} } } /// Original duty engagement can outlive its enable flag during a scene drop. public static class NativeSystemFlightObservation { public static bool ObserveOriginalArrival(JsonElement docking,int dockingState)=>Flag(docking,"verified") &&!LaunchNeedsCancellation(docking)&&(dockingState is 5 or 7||Flag(docking,"nativeDutyActive")); private static bool Flag(JsonElement value,string name)=>value.TryGetProperty(name,out var flag)&&flag.ValueKind==JsonValueKind.True; public static bool DockingInputZeroConfirmed(JsonElement local) { if(!local.TryGetProperty("throttle",out var throttle)||throttle.ValueKind!=JsonValueKind.Number ||!double.IsFinite(throttle.GetDouble())||Math.Abs(throttle.GetDouble())>.001 ||!local.TryGetProperty("docking",out var docking))return false; static bool True(JsonElement value,string key)=>value.TryGetProperty(key,out var flag)&&flag.ValueKind==JsonValueKind.True; // A controller receipt is insufficient if the actual desired input // still contains the SCA's previous 75% throttle. return True(docking,"throttleZero")&&True(docking,"zeroThrottleInputSent"); } public static string LoadoutFingerprint(ShipRecord ship) { static bool Assist(FittedModule module)=>module.Item.Equals("int_supercruiseassist",StringComparison.OrdinalIgnoreCase) ||module.Item.Equals("int_dockingcomputer_standard",StringComparison.OrdinalIgnoreCase) ||module.Item.Equals("int_dockingcomputer_advanced",StringComparison.OrdinalIgnoreCase); // Operational assist changes belong to the fallback state machine. // They must not masquerade as a physical loadout change and abort it. return Convert.ToHexStringLower(SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(new{ship.ShipId,ship.Type,ship.UnladenMass, Modules=ship.Modules.Select(m=>new{m.Slot,m.Item,Enabled=Assist(m)?(bool?)null:m.Enabled,Priority=Assist(m)?(int?)null:m.Priority,m.Engineering})},ToolboxSettings.JsonOptions))); } public static bool CruiseNeedsCancellation(JsonElement assist)=> assist.TryGetProperty("matchesTarget",out var matches)&&matches.ValueKind==JsonValueKind.True &&(assist.TryGetProperty("enabled",out var enabled)&&enabled.ValueKind==JsonValueKind.True ||assist.TryGetProperty("engaged",out var engaged)&&engaged.ValueKind==JsonValueKind.True); public static bool DockingNeedsCancellation(JsonElement docking)=> docking.TryGetProperty("nativeDutyActive",out var active)&&active.ValueKind==JsonValueKind.True &&!(docking.TryGetProperty("nativeLaunchActive",out var launch)&&launch.ValueKind==JsonValueKind.True); public static bool LaunchNeedsCancellation(JsonElement docking)=> docking.TryGetProperty("nativeLaunchActive",out var active)&&active.ValueKind==JsonValueKind.True ||docking.TryGetProperty("nativeLaunchRequested",out var requested)&&requested.ValueKind==JsonValueKind.True; public static LocalFlightMode Mode(bool docked,bool supercruise,int fsdState,bool nativeDocked=false,bool cooldownControls=false)=>docked?LocalFlightMode.Docked :nativeDocked||!(fsdState is 2 or 3||cooldownControls&&fsdState==1)?LocalFlightMode.Transition:supercruise?LocalFlightMode.Supercruise:LocalFlightMode.Normal; public static NativeAssistState CruiseAssist(bool available,bool enabled,bool engaged,bool matchesTarget,bool supercruise)=> !available?NativeAssistState.Unavailable:engaged&&matchesTarget&&supercruise?NativeAssistState.Engaged :enabled&&matchesTarget?NativeAssistState.Requested:NativeAssistState.Ready; public static NativeAssistState DockingAssist(bool available,bool verified,bool engaged)=>!available?NativeAssistState.Unavailable :verified&&engaged?NativeAssistState.Engaged:NativeAssistState.Ready; }