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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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();
    }
}