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;
/// <summary>Original local target/assist interfaces with expiring application flight inputs.</summary>
public sealed partial class NativeSystemFlightControlDriver(int processId,string gameHash,Func<PlayerState> player,IGameInputChannel? gameInputs=null,Func<CancellationToken,Task>? ensureFlightProtection=null,bool useNativeSupercruiseAssist=true):ISystemFlightControlDriver,IFlightDepartureDriver
{
public event Action<JsonElement>? 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<string>? 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<SystemNavigationCapabilities> 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<bool> 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<TimeSpan.FromSeconds(30))
{await Task.Delay(80,token);}
}
if(await PreserveArrivalAsync(token))return;
await PhysicalInputPreparation.RequireAsync(pipe,token);
if(gameInputs is not null)
{
await gameInputs.ConnectAsync(owningThread,token);
if(!await TravelPreparation.EnsureRetractedAsync(gameInputs,player,token,frame.ShipEntity,PreserveArrivalAsync))return;
}
else if(TravelPreparation.HasDeployedEquipment(player()))throw new InvalidOperationException("请先收起武器挂点、货舱舱门和起落架。");
if(await PreserveArrivalAsync(token))return;
if(!useNativeSupercruiseAssist)
{
var until=DateTimeOffset.UtcNow.AddSeconds(12);
while(true)
{
frame=await ReadSystemFlightAsync(token);var assist=LastTelemetry!.Value.GetProperty("supercruiseAssist");
if(!Flag(assist,"enabled")&&!Flag(assist,"engaged")){cruisePolicyPrepared=true;break;}
if(DateTimeOffset.UtcNow>=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(resumedAge<TimeSpan.Zero)resumedAge=TimeSpan.Zero;
var watch=Stopwatch.StartNew();TimeSpan Elapsed()=>watch.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<string> 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()<nextObservation)return;nextObservation=Elapsed().Add(TimeSpan.FromSeconds(2));
if(DepartureObserved is null)return;
object? flight=frame is {} observed?new{observed.ObservedAt,observed.Mode,observed.FsdReady,observed.PlayerOverride,observed.ExternalThreat,observed.GeometryVerified,
observed.Sequence,observed.ShipEntity,observed.ReferenceWorld,observed.TargetExclusionRadius,
observed.Position,observed.TargetPosition,observed.Forward,observed.Velocity,observed.TargetVelocity,target=observed.SelectedTarget?.Name,
distanceMetres=double.IsFinite(observed.TargetDistance)?(double?)observed.TargetDistance:null,
speedMetresPerSecond=double.IsFinite(observed.Speed)?(double?)observed.Speed:null,
throttle=double.IsFinite(observed.Throttle)?(double?)observed.Throttle:null}:null;
var observation=JsonSerializer.SerializeToElement(new{service="StationDeparture",processId,watchEntityId=frame?.ShipEntity.ToString(),issue=error??"",
applicationControl=new{detail=((int)Elapsed().TotalSeconds/2).ToString()},sample=new{state=frame?.Mode.ToString()},
elapsedSeconds=Elapsed().TotalSeconds,resumingOriginalLaunch=resumeOrigin is not null,current.Docked,current.Navigation.UndockedAt,statusFlags=current.Status?.Flags,
departurePath=new{departurePath.OriginalDutyObserved,departurePath.OriginalDutyNaturallyEnded,departurePath.PathVerified,departurePath.WithdrawalReason,clearingMassLock},
rawDockingState=LastTelemetry is {} telemetry?(int?)Number(telemetry,"rawDockingState"):null,
rawRequestState=LastTelemetry is {} requestTelemetry?(int?)Number(requestTelemetry,"rawRequestState"):null,
rawPad=LastTelemetry is {} padTelemetry?(int?)Number(padTelemetry,"rawPad"):null,
frame=flight, docking=docking.ValueKind==JsonValueKind.Object?docking:(JsonElement?)null});
if(DepartureObserved is {} handlers)foreach(Action<JsonElement> 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-actual<TimeSpan.FromSeconds(30);
var entered=Recent(entryRequestedAt,state.Navigation.SupercruiseEnteredAt);
var exited=Recent(exitRequestedAt??assistRequestedAt,state.Navigation.SupercruiseExitedAt);
if(!state.InGame||state.CurrentShipId!=journalShip||state.CommanderId!=commander||state.LoadGameAt!=sessionAt||state.Navigation.CurrentSystemAddress!=target.SystemAddress)
throw new InvalidOperationException("切换前后的飞船或会话不一致,已停止星系内驾驶。");
if(!entered&&!exited)
{
if(new[]{entryRequestedAt,exitRequestedAt,assistRequestedAt}.Any(x=>x is {} requested&&now-requested<TimeSpan.FromSeconds(35)))
throw new FlightTelemetryUnavailableException("正在等待游戏确认超巡切换。");
throw new InvalidOperationException("未获游戏确认的飞船实体切换,已停止星系内驾驶。");
}
if(!Flag(raw.GetProperty("shipHealth"),"verified"))throw new FlightTelemetryUnavailableException("正在恢复连续血量遥测。");
var transitionFsd=raw.GetProperty("rawFsdState").GetInt32();
if(transitionFsd!=2&&!(cooldownFlightControls&&transitionFsd==1))throw new FlightTelemetryUnavailableException("正在等待超巡切换后的飞行状态就绪。");
using var source=new NativeCurrentSystemSource(processId,gameHash,player);var snapshot=await source.ReadLocationsAsync(token);
bool SameDestination(SystemLocation row)=>row.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<SystemFlightFrame> 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>();
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);
}
}
/// <summary>Send the original player speed-zero input without approach thrust.</summary>
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);
}
/// <summary>Ordinary low-speed exit for cancellation/recovery; does not attest arrival at a station.</summary>
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-requested<TimeSpan.FromSeconds(15))
{await Task.Delay(100,token);}
}
}
public async Task SuspendSystemFlightAsync()
=>await 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.Elapsed<TimeSpan.FromSeconds(3)){}
await Task.Delay(50);
}
}
if(ownsCruiseAssist&&last is {PlayerOverride:false,Mode:LocalFlightMode.Supercruise} frame&&DateTimeOffset.UtcNow-frame.ObservedAt<TimeSpan.FromMilliseconds(150)
&&LastTelemetry is {} telemetry&&Flag(telemetry.GetProperty("supercruiseAssist"),"enabled"))
try{await ActionAsync("cancelSupercruiseAssist",frame,frame.ObservedAt.AddMilliseconds(250),CancellationToken.None);}catch(Exception error)when(NavigationRecovery.CanRetry(error,CancellationToken.None)){}
}
private async Task ReleasePrimaryFlightInputsAsync()
{
try
{
if(!emergencyExited)
{
var watch=Stopwatch.StartNew();
while(true){var result=await pipe.CallAsync(new{op="release",owner});if(Flag(result,"released")){ownsController=false;break;}if(watch.Elapsed>TimeSpan.FromSeconds(3))throw new InvalidOperationException("等待游戏释放自有驾驶输入。");await Task.Delay(25);}
}
}
finally{attitude.Reset();ownsCruiseAssist=ownsDockingAssist=false;ownsLaunch=false;}
}
}
/// <summary>Original duty engagement can outlive its enable flag during a scene drop.</summary>
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;
}
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;
/// <summary>Original local target/assist interfaces with expiring application flight inputs.</summary>
public sealed partial class NativeSystemFlightControlDriver(int processId,string gameHash,Func<PlayerState> player,IGameInputChannel? gameInputs=null,Func<CancellationToken,Task>? ensureFlightProtection=null,bool useNativeSupercruiseAssist=true):ISystemFlightControlDriver,IFlightDepartureDriver
{
public event Action<JsonElement>? 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<string>? 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<SystemNavigationCapabilities> 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<bool> 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<TimeSpan.FromSeconds(30))
{await Task.Delay(80,token);}
}
if(await PreserveArrivalAsync(token))return;
await PhysicalInputPreparation.RequireAsync(pipe,token);
if(gameInputs is not null)
{
await gameInputs.ConnectAsync(owningThread,token);
if(!await TravelPreparation.EnsureRetractedAsync(gameInputs,player,token,frame.ShipEntity,PreserveArrivalAsync))return;
}
else if(TravelPreparation.HasDeployedEquipment(player()))throw new InvalidOperationException("请先收起武器挂点、货舱舱门和起落架。");
if(await PreserveArrivalAsync(token))return;
if(!useNativeSupercruiseAssist)
{
var until=DateTimeOffset.UtcNow.AddSeconds(12);
while(true)
{
frame=await ReadSystemFlightAsync(token);var assist=LastTelemetry!.Value.GetProperty("supercruiseAssist");
if(!Flag(assist,"enabled")&&!Flag(assist,"engaged")){cruisePolicyPrepared=true;break;}
if(DateTimeOffset.UtcNow>=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(resumedAge<TimeSpan.Zero)resumedAge=TimeSpan.Zero;
var watch=Stopwatch.StartNew();TimeSpan Elapsed()=>watch.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<string> 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()<nextObservation)return;nextObservation=Elapsed().Add(TimeSpan.FromSeconds(2));
if(DepartureObserved is null)return;
object? flight=frame is {} observed?new{observed.ObservedAt,observed.Mode,observed.FsdReady,observed.PlayerOverride,observed.ExternalThreat,observed.GeometryVerified,
observed.Sequence,observed.ShipEntity,observed.ReferenceWorld,observed.TargetExclusionRadius,
observed.Position,observed.TargetPosition,observed.Forward,observed.Velocity,observed.TargetVelocity,target=observed.SelectedTarget?.Name,
distanceMetres=double.IsFinite(observed.TargetDistance)?(double?)observed.TargetDistance:null,
speedMetresPerSecond=double.IsFinite(observed.Speed)?(double?)observed.Speed:null,
throttle=double.IsFinite(observed.Throttle)?(double?)observed.Throttle:null}:null;
var observation=JsonSerializer.SerializeToElement(new{service="StationDeparture",processId,watchEntityId=frame?.ShipEntity.ToString(),issue=error??"",
applicationControl=new{detail=((int)Elapsed().TotalSeconds/2).ToString()},sample=new{state=frame?.Mode.ToString()},
elapsedSeconds=Elapsed().TotalSeconds,resumingOriginalLaunch=resumeOrigin is not null,current.Docked,current.Navigation.UndockedAt,statusFlags=current.Status?.Flags,
departurePath=new{departurePath.OriginalDutyObserved,departurePath.OriginalDutyNaturallyEnded,departurePath.PathVerified,departurePath.WithdrawalReason,clearingMassLock},
rawDockingState=LastTelemetry is {} telemetry?(int?)Number(telemetry,"rawDockingState"):null,
rawRequestState=LastTelemetry is {} requestTelemetry?(int?)Number(requestTelemetry,"rawRequestState"):null,
rawPad=LastTelemetry is {} padTelemetry?(int?)Number(padTelemetry,"rawPad"):null,
frame=flight, docking=docking.ValueKind==JsonValueKind.Object?docking:(JsonElement?)null});
if(DepartureObserved is {} handlers)foreach(Action<JsonElement> 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-actual<TimeSpan.FromSeconds(30);
var entered=Recent(entryRequestedAt,state.Navigation.SupercruiseEnteredAt);
var exited=Recent(exitRequestedAt??assistRequestedAt,state.Navigation.SupercruiseExitedAt);
if(!state.InGame||state.CurrentShipId!=journalShip||state.CommanderId!=commander||state.LoadGameAt!=sessionAt||state.Navigation.CurrentSystemAddress!=target.SystemAddress)
throw new InvalidOperationException("切换前后的飞船或会话不一致,已停止星系内驾驶。");
if(!entered&&!exited)
{
if(new[]{entryRequestedAt,exitRequestedAt,assistRequestedAt}.Any(x=>x is {} requested&&now-requested<TimeSpan.FromSeconds(35)))
throw new FlightTelemetryUnavailableException("正在等待游戏确认超巡切换。");
throw new InvalidOperationException("未获游戏确认的飞船实体切换,已停止星系内驾驶。");
}
if(!Flag(raw.GetProperty("shipHealth"),"verified"))throw new FlightTelemetryUnavailableException("正在恢复连续血量遥测。");
var transitionFsd=raw.GetProperty("rawFsdState").GetInt32();
if(transitionFsd!=2&&!(cooldownFlightControls&&transitionFsd==1))throw new FlightTelemetryUnavailableException("正在等待超巡切换后的飞行状态就绪。");
using var source=new NativeCurrentSystemSource(processId,gameHash,player);var snapshot=await source.ReadLocationsAsync(token);
bool SameDestination(SystemLocation row)=>row.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<SystemFlightFrame> 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>();
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);
}
}
/// <summary>Send the original player speed-zero input without approach thrust.</summary>
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);
}
/// <summary>Ordinary low-speed exit for cancellation/recovery; does not attest arrival at a station.</summary>
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-requested<TimeSpan.FromSeconds(15))
{await Task.Delay(100,token);}
}
}
public async Task SuspendSystemFlightAsync()
=>await 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.Elapsed<TimeSpan.FromSeconds(3)){}
await Task.Delay(50);
}
}
if(ownsCruiseAssist&&last is {PlayerOverride:false,Mode:LocalFlightMode.Supercruise} frame&&DateTimeOffset.UtcNow-frame.ObservedAt<TimeSpan.FromMilliseconds(150)
&&LastTelemetry is {} telemetry&&Flag(telemetry.GetProperty("supercruiseAssist"),"enabled"))
try{await ActionAsync("cancelSupercruiseAssist",frame,frame.ObservedAt.AddMilliseconds(250),CancellationToken.None);}catch(Exception error)when(NavigationRecovery.CanRetry(error,CancellationToken.None)){}
}
private async Task ReleasePrimaryFlightInputsAsync()
{
try
{
if(!emergencyExited)
{
var watch=Stopwatch.StartNew();
while(true){var result=await pipe.CallAsync(new{op="release",owner});if(Flag(result,"released")){ownsController=false;break;}if(watch.Elapsed>TimeSpan.FromSeconds(3))throw new InvalidOperationException("等待游戏释放自有驾驶输入。");await Task.Delay(25);}
}
}
finally{attitude.Reset();ownsCruiseAssist=ownsDockingAssist=false;ownsLaunch=false;}
}
}
/// <summary>Original duty engagement can outlive its enable flag during a scene drop.</summary>
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;
}