using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Session;
using System.Diagnostics;
namespace XFEEDToolBox.Core.Navigation;
/// <summary>Native adapters for the shared journey pipeline; it owns no navigation runner.</summary>
public sealed class NativeFlightJourneyDriver(int processId,string gameHash,Func<PlayerState> player,string pluginDirectory,
Func<double> fuelReserve,Func<CancellationToken,Task> ensureProtection,Func<CancellationToken,Task>? beforeInput=null,Action<System.Text.Json.JsonElement>? cruiseDiagnostics=null,
string? serviceOperationDirectory=null,Func<decimal>? serviceReserveCredits=null,Action<NativeWalletObservation>? walletObserved=null,
Action<ConfirmedDockedServiceReceipt>? serviceConfirmed=null):IFlightJourneyDriver,IFlightJourneyFailureClassifier,IFlightFuelPreparationDriver
{
public event Action<string>? Status;
private PlayerState? flightSession;
private long processStartedUtcTicks;
private void RequireFlightBinding(CancellationToken token)
{
token.ThrowIfCancellationRequested();var current=player();
using var process=Process.GetProcessById(processId);var started=process.StartTime.ToUniversalTime().Ticks;
if(processStartedUtcTicks==0){processStartedUtcTicks=started;flightSession=current;}
var expected=flightSession!;
if(process.HasExited||started!=processStartedUtcTicks||current.CommanderId!=expected.CommanderId||current.CurrentShipId!=expected.CurrentShipId
||current.LoadGameAt!=expected.LoadGameAt||current.GameMode!=expected.GameMode||current.CargoCount!=expected.CargoCount)
throw new InvalidDataException("连接恢复前飞行实例、指挥官、飞船、会话、原模式或货物已改变。");
if(NavigationSafety.UnhealthyHull(current))throw new FlightHealthPauseException();
if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
}
public async Task<bool> PrepareDepartureFuelAsync(bool interstellar,CancellationToken token,bool refuel=true)
{
var initial=player();var market=initial.MarketId;
if(!initial.Docked||market is not >0)throw new InvalidDataException("站内补油需要确认实际停靠市场。");
if(!initial.StationServicesKnown)throw new InvalidDataException("站点服务尚未确认,尚未离站。");
void RequireSameDockedCargo()
{
var current=player();
if(!current.InGame||!current.Docked||current.MarketId!=market||current.CommanderId!=initial.CommanderId
||current.CurrentShipId!=initial.CurrentShipId||current.LoadGameAt!=initial.LoadGameAt)
throw new InvalidDataException("补油期间飞船、会话或停靠市场改变,已停止离站。");
NavigationFuelResupply.RequireSameCargo(initial,current);
}
async Task Check(CancellationToken cancellation)
{
cancellation.ThrowIfCancellationRequested();RequireSameDockedCargo();
if(beforeInput is not null)await beforeInput(cancellation);
await ensureProtection(cancellation);cancellation.ThrowIfCancellationRequested();RequireSameDockedCargo();
}
var snapshot=await NavigationFuelResupply.ReadDockedBudgetAsync(player,Check,fuelReserve,interstellar,token);
var prepared=snapshot.Player;var required=snapshot.Required;var available=snapshot.Available;
var ship=prepared.Ships.Single(value=>value.ShipId==prepared.CurrentShipId);
if(ship.FuelMain is not >0||required>ship.FuelMain.Value)
throw new NavigationFuelResupplyRequiredException(initial.Navigation.CurrentSystemAddress??0,available,required);
DateTimeOffset? refuelConfirmedAt=null,refuelSubmittedAt=null;
var refuelServiceCompleted=false;
if(refuel&&initial.StationServices.Contains("refuel"))
{
Status?.Invoke("正在通过原版站内服务补满燃油并核对实际回执。");
var directory=serviceOperationDirectory??Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","flight-services");
var reserve=Math.Max(initial.Rebuy,serviceReserveCredits?.Invoke()??initial.Rebuy);
var services=new NativeTradeCommandDriver(new NativeCommerceChannel(processId),player,directory,
(_,_,_)=>throw new InvalidOperationException("站内补油服务不能启动另一笔驾驶或交易。"),ensureProtection,
batchPolicy:new NativeTradeBatchPolicy(Math.Max(1,initial.CargoCapacity),reserve));
if(walletObserved is not null)services.WalletObserved+=walletObserved;
services.ServiceConfirmed+=receipt=>
{
refuelConfirmedAt=receipt.ConfirmedAt;
serviceConfirmed?.Invoke(receipt);
};
services.Status+=text=>Status?.Invoke(text);
await services.PreflightAsync(token);await Check(token);
refuelSubmittedAt=DateTimeOffset.UtcNow;
await services.RefuelDockedAsync(new(market.Value,initial.CargoCapacity),Check,token);
// RefuelDockedAsync returns only after the original final quote
// confirms zero remaining fuel cost. This never submits a second payment.
refuelServiceCompleted=true;
}
using var deadline=CancellationTokenSource.CreateLinkedTokenSource(token);deadline.CancelAfter(TimeSpan.FromSeconds(10));
try
{
while(true)
{
var updated=await NavigationFuelResupply.ReadDockedBudgetAsync(player,Check,fuelReserve,interstellar,deadline.Token);
var current=updated.Player;var actual=updated.Available;required=updated.Required;
// A paid receipt needs a main or reservoir increase in a newly
// written/read Status file from this mutation and same ship.
// A same-second content timestamp is valid; a reread is not.
var postReceipt=refuelConfirmedAt is not {} confirmed
||refuelSubmittedAt is {} submitted&&NavigationFuelResupply.IsPostReceiptSample(prepared,current,submitted,confirmed,DateTimeOffset.UtcNow,refuelServiceCompleted);
if(actual>=required&&postReceipt)return refuelConfirmedAt is not null&&NavigationFuelResupply.FuelMassIncreased(prepared,current);
if(!refuel||!initial.StationServices.Contains("refuel"))throw new NavigationFuelResupplyRequiredException(current.Navigation.CurrentSystemAddress??0,actual,required);
await Task.Delay(100,deadline.Token);
}
}
catch(OperationCanceledException)when(!token.IsCancellationRequested)
{throw new InvalidDataException("补油回执已保留,但实际燃油状态尚未确认足以完成航程,未执行离站。");}
}
private NativeGameInputChannel Inputs()=>new(processId,gameHash,pluginDirectory);
public async Task<Exception> ClassifyFailureAsync(Exception failure,CancellationToken token)
{
token.ThrowIfCancellationRequested();
if(FlightConnectionRecovery.HasCancellationCause(failure)||!FlightJourneyFailure.HasTransientCause(failure,token))return failure;
RequireFlightBinding(token);var expected=flightSession!;
if(expected.CurrentShipId is not {} ship||expected.LoadGameAt is not {} loaded)throw new InvalidDataException("原飞行会话身份缺失。");
var binding=new FlightConnectionBinding(processId,processStartedUtcTicks,gameHash,expected.CommanderId,ship,expected.GameMode,loaded,expected.CargoCount);
var pipe=new NavigationPipeClient(processId);
var classified=await FlightConnectionRecovery.ClassifyAsync(failure,binding,async cancellation=>
(await pipe.CallAsync(new{op="session.status"},cancellation),await pipe.CallAsync(new{op="describe"},cancellation)),
cancellation=>{RequireFlightBinding(cancellation);return Task.CompletedTask;},token);
if(!ReferenceEquals(classified,failure))return classified;
return await FlightJourneyFailure.ClassifyAsync(failure,cancellation=>SavingSceneRecovery.ReadAsync(processId,gameHash,cancellation),token);
}
public async Task EnsureReadyAsync(CancellationToken token)
{
RequireFlightBinding(token);
try
{
await NavigationRecovery.ReadWithRecoveryAsync(async cancellation=>
{
if(beforeInput is not null)await beforeInput(cancellation);
await ensureProtection(cancellation);return true;
},()=>Status?.Invoke("正在等待场景切换后的连续健康遥测,恢复后继续原航程。"),token);
}
catch(Exception error)when(NavigationRecovery.CanRetry(error,token))
{
var classified=await ClassifyFailureAsync(error,token);
if(!ReferenceEquals(classified,error))throw classified;
throw;
}
}
private async Task<NativeSystemFlightControlDriver> LocalAsync(CancellationToken token,bool cruise=true)
{
var driver=new NativeSystemFlightControlDriver(processId,gameHash,player,Inputs(),ensureProtection,useNativeSupercruiseAssist:false);
await driver.ConfigureOriginalDockingControlAsync(Path.Combine(pluginDirectory,"DockingControl"),token);
if(cruise)
{
try
{
await new NativeCruiseControlChannel(processId,gameHash).PrepareAsync(Path.Combine(pluginDirectory,"CruiseControl"),token);
await driver.ConfigureOriginalCruiseControlAsync(fuelReserve,token:token,diagnostics:cruiseDiagnostics);
}
catch(Exception error)when(!token.IsCancellationRequested&&error is IOException or OperationCanceledException)
{Status?.Invoke("SCO接口正在恢复,当前航段先使用普通超巡。");}
}
return driver;
}
public async Task DepartAsync(CancellationToken token)
{
await EnsureReadyAsync(token);var departure=await LocalAsync(token,cruise:false);
departure.DepartureProgress+=text=>Status?.Invoke(text);
if(cruiseDiagnostics is not null)departure.DepartureObserved+=cruiseDiagnostics;
await departure.DepartAsync(token);
}
public async Task EnsureDepartureClearanceAsync(long? localDestinationMarket,CancellationToken token)
{
await EnsureReadyAsync(token);var departure=await LocalAsync(token,cruise:false);
departure.DepartureProgress+=text=>Status?.Invoke(text);
if(cruiseDiagnostics is not null)departure.DepartureObserved+=cruiseDiagnostics;
await departure.EnsureDepartureClearanceAsync(localDestinationMarket,token);
}
public async Task<IFlightControlDriver> PlanInterstellarAsync(string destination,CancellationToken token)
{
var galaxy=await new NativeGalaxyCatalog(processId).FindAsync(destination,token);
var waypoint=new NavWaypoint(galaxy.SystemAddress??throw new InvalidDataException("目的星系身份缺失。"),galaxy.Name,galaxy.Position,galaxy.StarClass);
var interstellar=new NativeFlightControlDriver(processId,gameHash,player,fuelReserve,Inputs(),ensureProtection);
if(cruiseDiagnostics is not null)interstellar.TargetSelectionObserved+=cruiseDiagnostics;
try
{
await NavigationRecovery.PreflightWithRecoveryAsync(interstellar,()=>Status?.Invoke("正在连接星际导航驱动"),token);
await new GameRoutePlanner(player).PlanAsync(waypoint,interstellar,null,token);return interstellar;
}
catch{await interstellar.ReleaseAsync();throw;}
}
public async Task<(SystemNavigationTarget Target,ISystemFlightControlDriver Driver)> ResolveArrivalAsync(Station destination,CancellationToken token)
=>await ResolveArrivalAsync(destination,true,token);
public async Task<(SystemNavigationTarget Target,ISystemFlightControlDriver Driver)> ResolveFuelReturnAsync(Station origin,CancellationToken token)
{
var initial=player();
var arrival=await ResolveArrivalAsync(origin,false,token);
return(arrival.Target,new FuelReturnDriver(arrival.Driver,()=>
{
var current=player();
if(current.CommanderId!=initial.CommanderId||current.CurrentShipId!=initial.CurrentShipId||current.LoadGameAt!=initial.LoadGameAt
||current.Navigation.CurrentSystemAddress!=initial.Navigation.CurrentSystemAddress)throw new InvalidDataException("补油返站期间飞船或星系改变。");
NavigationFuelResupply.RequireSameCargo(initial,current);
var fuel=NavigationFuelResupply.CurrentFuel(current,DateTimeOffset.UtcNow);
if(fuel<fuelReserve()+.25)throw new NavigationFuelResupplyRequiredException(current.Navigation.CurrentSystemAddress??0,fuel,fuelReserve()+.25);
}));
}
private async Task<(SystemNavigationTarget Target,ISystemFlightControlDriver Driver)> ResolveArrivalAsync(Station destination,bool cruise,CancellationToken token)
{
using var source=new NativeCurrentSystemSource(processId,gameHash,player);
var catalogue=await NavigationRecovery.ReadWithRecoveryAsync(source.ReadLocationsAsync,null,token);
var row=catalogue.Locations.SingleOrDefault(value=>value.MarketId==destination.MarketId&&value.Name.Equals(destination.Name,StringComparison.OrdinalIgnoreCase))
??throw new InvalidDataException("当前游戏目录尚未确认目的站的完整身份。");
var target=new SystemNavigationTarget(catalogue.SystemAddress,row.WorldAddress,row.Name,SystemTargetKind.Station,row.MarketId)
{NativeKey=row.Key,NativeKind=row.NativeKind,NativeIdentity=row.Identity};
return(target,await LocalAsync(token,cruise));
}
private sealed class FuelReturnDriver(ISystemFlightControlDriver driver,Action check):ISystemFlightControlDriver
{
public Task<SystemNavigationCapabilities> InspectAsync(CancellationToken token){token.ThrowIfCancellationRequested();check();return driver.InspectAsync(token);}
public Task SelectAsync(SystemNavigationTarget target,CancellationToken token){token.ThrowIfCancellationRequested();check();return driver.SelectAsync(target,token);}
public Task PrepareAsync(CancellationToken token){token.ThrowIfCancellationRequested();check();return driver.PrepareAsync(token);}
public Task<SystemFlightFrame> ReadSystemFlightAsync(CancellationToken token){token.ThrowIfCancellationRequested();check();return driver.ReadSystemFlightAsync(token);}
public Task ApplySystemFlightAsync(SystemFlightDecision decision,SystemFlightFrame frame,SystemNavigationTarget target,DateTimeOffset expires,CancellationToken token)
{token.ThrowIfCancellationRequested();check();return driver.ApplySystemFlightAsync(decision,frame,target,expires,token);}
public Task ReleaseSystemFlightAsync()=>driver.ReleaseSystemFlightAsync();
public Task SuspendSystemFlightAsync()=>driver.SuspendSystemFlightAsync();
}
}
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Session;
using System.Diagnostics;
namespace XFEEDToolBox.Core.Navigation;
/// <summary>Native adapters for the shared journey pipeline; it owns no navigation runner.</summary>
public sealed class NativeFlightJourneyDriver(int processId,string gameHash,Func<PlayerState> player,string pluginDirectory,
Func<double> fuelReserve,Func<CancellationToken,Task> ensureProtection,Func<CancellationToken,Task>? beforeInput=null,Action<System.Text.Json.JsonElement>? cruiseDiagnostics=null,
string? serviceOperationDirectory=null,Func<decimal>? serviceReserveCredits=null,Action<NativeWalletObservation>? walletObserved=null,
Action<ConfirmedDockedServiceReceipt>? serviceConfirmed=null):IFlightJourneyDriver,IFlightJourneyFailureClassifier,IFlightFuelPreparationDriver
{
public event Action<string>? Status;
private PlayerState? flightSession;
private long processStartedUtcTicks;
private void RequireFlightBinding(CancellationToken token)
{
token.ThrowIfCancellationRequested();var current=player();
using var process=Process.GetProcessById(processId);var started=process.StartTime.ToUniversalTime().Ticks;
if(processStartedUtcTicks==0){processStartedUtcTicks=started;flightSession=current;}
var expected=flightSession!;
if(process.HasExited||started!=processStartedUtcTicks||current.CommanderId!=expected.CommanderId||current.CurrentShipId!=expected.CurrentShipId
||current.LoadGameAt!=expected.LoadGameAt||current.GameMode!=expected.GameMode||current.CargoCount!=expected.CargoCount)
throw new InvalidDataException("连接恢复前飞行实例、指挥官、飞船、会话、原模式或货物已改变。");
if(NavigationSafety.UnhealthyHull(current))throw new FlightHealthPauseException();
if(NavigationSafety.HasExternalThreat(current))throw new FlightThreatPauseException();
}
public async Task<bool> PrepareDepartureFuelAsync(bool interstellar,CancellationToken token,bool refuel=true)
{
var initial=player();var market=initial.MarketId;
if(!initial.Docked||market is not >0)throw new InvalidDataException("站内补油需要确认实际停靠市场。");
if(!initial.StationServicesKnown)throw new InvalidDataException("站点服务尚未确认,尚未离站。");
void RequireSameDockedCargo()
{
var current=player();
if(!current.InGame||!current.Docked||current.MarketId!=market||current.CommanderId!=initial.CommanderId
||current.CurrentShipId!=initial.CurrentShipId||current.LoadGameAt!=initial.LoadGameAt)
throw new InvalidDataException("补油期间飞船、会话或停靠市场改变,已停止离站。");
NavigationFuelResupply.RequireSameCargo(initial,current);
}
async Task Check(CancellationToken cancellation)
{
cancellation.ThrowIfCancellationRequested();RequireSameDockedCargo();
if(beforeInput is not null)await beforeInput(cancellation);
await ensureProtection(cancellation);cancellation.ThrowIfCancellationRequested();RequireSameDockedCargo();
}
var snapshot=await NavigationFuelResupply.ReadDockedBudgetAsync(player,Check,fuelReserve,interstellar,token);
var prepared=snapshot.Player;var required=snapshot.Required;var available=snapshot.Available;
var ship=prepared.Ships.Single(value=>value.ShipId==prepared.CurrentShipId);
if(ship.FuelMain is not >0||required>ship.FuelMain.Value)
throw new NavigationFuelResupplyRequiredException(initial.Navigation.CurrentSystemAddress??0,available,required);
DateTimeOffset? refuelConfirmedAt=null,refuelSubmittedAt=null;
var refuelServiceCompleted=false;
if(refuel&&initial.StationServices.Contains("refuel"))
{
Status?.Invoke("正在通过原版站内服务补满燃油并核对实际回执。");
var directory=serviceOperationDirectory??Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","flight-services");
var reserve=Math.Max(initial.Rebuy,serviceReserveCredits?.Invoke()??initial.Rebuy);
var services=new NativeTradeCommandDriver(new NativeCommerceChannel(processId),player,directory,
(_,_,_)=>throw new InvalidOperationException("站内补油服务不能启动另一笔驾驶或交易。"),ensureProtection,
batchPolicy:new NativeTradeBatchPolicy(Math.Max(1,initial.CargoCapacity),reserve));
if(walletObserved is not null)services.WalletObserved+=walletObserved;
services.ServiceConfirmed+=receipt=>
{
refuelConfirmedAt=receipt.ConfirmedAt;
serviceConfirmed?.Invoke(receipt);
};
services.Status+=text=>Status?.Invoke(text);
await services.PreflightAsync(token);await Check(token);
refuelSubmittedAt=DateTimeOffset.UtcNow;
await services.RefuelDockedAsync(new(market.Value,initial.CargoCapacity),Check,token);
// RefuelDockedAsync returns only after the original final quote
// confirms zero remaining fuel cost. This never submits a second payment.
refuelServiceCompleted=true;
}
using var deadline=CancellationTokenSource.CreateLinkedTokenSource(token);deadline.CancelAfter(TimeSpan.FromSeconds(10));
try
{
while(true)
{
var updated=await NavigationFuelResupply.ReadDockedBudgetAsync(player,Check,fuelReserve,interstellar,deadline.Token);
var current=updated.Player;var actual=updated.Available;required=updated.Required;
// A paid receipt needs a main or reservoir increase in a newly
// written/read Status file from this mutation and same ship.
// A same-second content timestamp is valid; a reread is not.
var postReceipt=refuelConfirmedAt is not {} confirmed
||refuelSubmittedAt is {} submitted&&NavigationFuelResupply.IsPostReceiptSample(prepared,current,submitted,confirmed,DateTimeOffset.UtcNow,refuelServiceCompleted);
if(actual>=required&&postReceipt)return refuelConfirmedAt is not null&&NavigationFuelResupply.FuelMassIncreased(prepared,current);
if(!refuel||!initial.StationServices.Contains("refuel"))throw new NavigationFuelResupplyRequiredException(current.Navigation.CurrentSystemAddress??0,actual,required);
await Task.Delay(100,deadline.Token);
}
}
catch(OperationCanceledException)when(!token.IsCancellationRequested)
{throw new InvalidDataException("补油回执已保留,但实际燃油状态尚未确认足以完成航程,未执行离站。");}
}
private NativeGameInputChannel Inputs()=>new(processId,gameHash,pluginDirectory);
public async Task<Exception> ClassifyFailureAsync(Exception failure,CancellationToken token)
{
token.ThrowIfCancellationRequested();
if(FlightConnectionRecovery.HasCancellationCause(failure)||!FlightJourneyFailure.HasTransientCause(failure,token))return failure;
RequireFlightBinding(token);var expected=flightSession!;
if(expected.CurrentShipId is not {} ship||expected.LoadGameAt is not {} loaded)throw new InvalidDataException("原飞行会话身份缺失。");
var binding=new FlightConnectionBinding(processId,processStartedUtcTicks,gameHash,expected.CommanderId,ship,expected.GameMode,loaded,expected.CargoCount);
var pipe=new NavigationPipeClient(processId);
var classified=await FlightConnectionRecovery.ClassifyAsync(failure,binding,async cancellation=>
(await pipe.CallAsync(new{op="session.status"},cancellation),await pipe.CallAsync(new{op="describe"},cancellation)),
cancellation=>{RequireFlightBinding(cancellation);return Task.CompletedTask;},token);
if(!ReferenceEquals(classified,failure))return classified;
return await FlightJourneyFailure.ClassifyAsync(failure,cancellation=>SavingSceneRecovery.ReadAsync(processId,gameHash,cancellation),token);
}
public async Task EnsureReadyAsync(CancellationToken token)
{
RequireFlightBinding(token);
try
{
await NavigationRecovery.ReadWithRecoveryAsync(async cancellation=>
{
if(beforeInput is not null)await beforeInput(cancellation);
await ensureProtection(cancellation);return true;
},()=>Status?.Invoke("正在等待场景切换后的连续健康遥测,恢复后继续原航程。"),token);
}
catch(Exception error)when(NavigationRecovery.CanRetry(error,token))
{
var classified=await ClassifyFailureAsync(error,token);
if(!ReferenceEquals(classified,error))throw classified;
throw;
}
}
private async Task<NativeSystemFlightControlDriver> LocalAsync(CancellationToken token,bool cruise=true)
{
var driver=new NativeSystemFlightControlDriver(processId,gameHash,player,Inputs(),ensureProtection,useNativeSupercruiseAssist:false);
await driver.ConfigureOriginalDockingControlAsync(Path.Combine(pluginDirectory,"DockingControl"),token);
if(cruise)
{
try
{
await new NativeCruiseControlChannel(processId,gameHash).PrepareAsync(Path.Combine(pluginDirectory,"CruiseControl"),token);
await driver.ConfigureOriginalCruiseControlAsync(fuelReserve,token:token,diagnostics:cruiseDiagnostics);
}
catch(Exception error)when(!token.IsCancellationRequested&&error is IOException or OperationCanceledException)
{Status?.Invoke("SCO接口正在恢复,当前航段先使用普通超巡。");}
}
return driver;
}
public async Task DepartAsync(CancellationToken token)
{
await EnsureReadyAsync(token);var departure=await LocalAsync(token,cruise:false);
departure.DepartureProgress+=text=>Status?.Invoke(text);
if(cruiseDiagnostics is not null)departure.DepartureObserved+=cruiseDiagnostics;
await departure.DepartAsync(token);
}
public async Task EnsureDepartureClearanceAsync(long? localDestinationMarket,CancellationToken token)
{
await EnsureReadyAsync(token);var departure=await LocalAsync(token,cruise:false);
departure.DepartureProgress+=text=>Status?.Invoke(text);
if(cruiseDiagnostics is not null)departure.DepartureObserved+=cruiseDiagnostics;
await departure.EnsureDepartureClearanceAsync(localDestinationMarket,token);
}
public async Task<IFlightControlDriver> PlanInterstellarAsync(string destination,CancellationToken token)
{
var galaxy=await new NativeGalaxyCatalog(processId).FindAsync(destination,token);
var waypoint=new NavWaypoint(galaxy.SystemAddress??throw new InvalidDataException("目的星系身份缺失。"),galaxy.Name,galaxy.Position,galaxy.StarClass);
var interstellar=new NativeFlightControlDriver(processId,gameHash,player,fuelReserve,Inputs(),ensureProtection);
if(cruiseDiagnostics is not null)interstellar.TargetSelectionObserved+=cruiseDiagnostics;
try
{
await NavigationRecovery.PreflightWithRecoveryAsync(interstellar,()=>Status?.Invoke("正在连接星际导航驱动"),token);
await new GameRoutePlanner(player).PlanAsync(waypoint,interstellar,null,token);return interstellar;
}
catch{await interstellar.ReleaseAsync();throw;}
}
public async Task<(SystemNavigationTarget Target,ISystemFlightControlDriver Driver)> ResolveArrivalAsync(Station destination,CancellationToken token)
=>await ResolveArrivalAsync(destination,true,token);
public async Task<(SystemNavigationTarget Target,ISystemFlightControlDriver Driver)> ResolveFuelReturnAsync(Station origin,CancellationToken token)
{
var initial=player();
var arrival=await ResolveArrivalAsync(origin,false,token);
return(arrival.Target,new FuelReturnDriver(arrival.Driver,()=>
{
var current=player();
if(current.CommanderId!=initial.CommanderId||current.CurrentShipId!=initial.CurrentShipId||current.LoadGameAt!=initial.LoadGameAt
||current.Navigation.CurrentSystemAddress!=initial.Navigation.CurrentSystemAddress)throw new InvalidDataException("补油返站期间飞船或星系改变。");
NavigationFuelResupply.RequireSameCargo(initial,current);
var fuel=NavigationFuelResupply.CurrentFuel(current,DateTimeOffset.UtcNow);
if(fuel<fuelReserve()+.25)throw new NavigationFuelResupplyRequiredException(current.Navigation.CurrentSystemAddress??0,fuel,fuelReserve()+.25);
}));
}
private async Task<(SystemNavigationTarget Target,ISystemFlightControlDriver Driver)> ResolveArrivalAsync(Station destination,bool cruise,CancellationToken token)
{
using var source=new NativeCurrentSystemSource(processId,gameHash,player);
var catalogue=await NavigationRecovery.ReadWithRecoveryAsync(source.ReadLocationsAsync,null,token);
var row=catalogue.Locations.SingleOrDefault(value=>value.MarketId==destination.MarketId&&value.Name.Equals(destination.Name,StringComparison.OrdinalIgnoreCase))
??throw new InvalidDataException("当前游戏目录尚未确认目的站的完整身份。");
var target=new SystemNavigationTarget(catalogue.SystemAddress,row.WorldAddress,row.Name,SystemTargetKind.Station,row.MarketId)
{NativeKey=row.Key,NativeKind=row.NativeKind,NativeIdentity=row.Identity};
return(target,await LocalAsync(token,cruise));
}
private sealed class FuelReturnDriver(ISystemFlightControlDriver driver,Action check):ISystemFlightControlDriver
{
public Task<SystemNavigationCapabilities> InspectAsync(CancellationToken token){token.ThrowIfCancellationRequested();check();return driver.InspectAsync(token);}
public Task SelectAsync(SystemNavigationTarget target,CancellationToken token){token.ThrowIfCancellationRequested();check();return driver.SelectAsync(target,token);}
public Task PrepareAsync(CancellationToken token){token.ThrowIfCancellationRequested();check();return driver.PrepareAsync(token);}
public Task<SystemFlightFrame> ReadSystemFlightAsync(CancellationToken token){token.ThrowIfCancellationRequested();check();return driver.ReadSystemFlightAsync(token);}
public Task ApplySystemFlightAsync(SystemFlightDecision decision,SystemFlightFrame frame,SystemNavigationTarget target,DateTimeOffset expires,CancellationToken token)
{token.ThrowIfCancellationRequested();check();return driver.ApplySystemFlightAsync(decision,frame,target,expires,token);}
public Task ReleaseSystemFlightAsync()=>driver.ReleaseSystemFlightAsync();
public Task SuspendSystemFlightAsync()=>driver.SuspendSystemFlightAsync();
}
}