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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using XFEEDToolBox.Core.Models;

namespace XFEEDToolBox.Core.Navigation;

/// <summary>A confirmed fuel shortfall is a station-service task, not a transient telemetry wait.</summary>
public sealed class NavigationFuelResupplyRequiredException(long systemAddress,double available,double required)
    :InvalidOperationException($"当前燃油 {available:F2} t 不足以覆盖已核对航程及储备 {required:F2} t,需要先在站点补油;原目的地已保留。")
{
    public long SystemAddress{get;}=systemAddress;
    public double Available{get;}=available;
    public double Required{get;}=required;
}

public interface IFlightFuelPreparationDriver
{
    Task<bool> PrepareDepartureFuelAsync(bool interstellar,CancellationToken token,bool refuel=true);
    Task<(SystemNavigationTarget Target,ISystemFlightControlDriver Driver)> ResolveFuelReturnAsync(Station origin,CancellationToken token);
}
public sealed record DockedFuelSnapshot(PlayerState Player,double Available,double Required);

public static class NavigationFuelResupply
{
    public static DateTimeOffset? FuelObservedAt(PlayerState state)=>state.Navigation.FuelAt>state.StatusAt
        ?state.Navigation.FuelAt:state.StatusAt??state.Navigation.FuelAt;
    public static NavigationFuelResupplyRequiredException? Required(FlightFrame frame)
    {
        var minimum=frame.MinimumFuelToNextRefuel??frame.FuelNeededToNextRefuel;
        if(frame.ScoopPowered||frame.Fsd==FsdPhase.Jumping||frame.FuelMain is not {} available||minimum is not {} required
            ||!double.IsFinite(available)||!double.IsFinite(required)||available<0||required<=0||available>=required)return null;
        return new(frame.SystemAddress,available,required);
    }

    public static double CurrentFuel(PlayerState state,DateTimeOffset now)
    {
        var journalNewer=state.Navigation.FuelAt>state.StatusAt;
        var fuel=journalNewer?state.Navigation.MainFuel:state.Status?.FuelMain??state.Navigation.MainFuel;
        var at=FuelObservedAt(state);
        if(fuel is not {} value||!double.IsFinite(value)||value<0||at is null
            ||GameJournalClock.Now(state,now)-at>TimeSpan.FromSeconds(10)&&!ReconfirmedDockedStatus(state,at.Value,now)
            ||at>GameJournalClock.Now(state,now).AddSeconds(5))
            throw new FlightTelemetryUnavailableException("等待本次飞船的连续燃油状态,尚未离站。");
        return value;
    }

    private static bool ReconfirmedDockedStatus(PlayerState state,DateTimeOffset fuelAt,DateTimeOffset now)
    {
        // An unchanged, stable Status file is reread by the monitor. Its old
        // content timestamp alone is not stale while the same ship is docked.
        // Never apply this exception to an old journal fuel event or in flight.
        return state.InGame&&state.Docked&&state.MarketId is >0&&state.LoadGameAt is {} loaded
            &&state.Status is {} status&&(status.Flags&1)!=0&&(status.Flags&16)==0&&state.StatusAt==fuelAt&&fuelAt>=loaded
            &&GameJournalClock.Offset(state,now) is {} offset&&state.StatusWrittenAt>=loaded-offset-TimeSpan.FromSeconds(1);
    }

    private static bool FuelAmounts(PlayerState state,out double main,out double reservoir)
    {
        main=state.Status?.FuelMain??double.NaN;reservoir=state.Status?.FuelReserve??double.NaN;
        return double.IsFinite(main)&&main>=0&&double.IsFinite(reservoir)&&reservoir>=0;
    }

    public static bool FuelMassIncreased(PlayerState before,PlayerState current)
        =>FuelAmounts(before,out var oldMain,out var oldReservoir)&&FuelAmounts(current,out var main,out var reservoir)
            &&main+reservoir>oldMain+oldReservoir;

    public static bool IsPostReceiptSample(PlayerState before,PlayerState current,DateTimeOffset submittedWall,DateTimeOffset confirmedWall,DateTimeOffset now,
        bool originalFinalQuoteZero)
    {
        // Game timestamps are second-resolution and their calibrated offset
        // changes slightly between writes. Bind this paid mutation to a new
        // stable file write/read in wall time, not to a fabricated next second.
        return originalFinalQuoteZero&&FuelAmounts(before,out var oldFuel,out var oldReservoir)&&FuelAmounts(current,out var fuel,out var reservoir)
            &&(fuel>oldFuel||reservoir>oldReservoir)
            &&current.InGame&&current.Docked&&current.MarketId==before.MarketId&&current.CommanderId==before.CommanderId
            &&current.CurrentShipId==before.CurrentShipId&&current.LoadGameAt==before.LoadGameAt
            &&current.Status is {} status&&(status.Flags&1)!=0&&(status.Flags&16)==0
            &&current.StatusAt is {} contentAt&&FuelObservedAt(current)==contentAt
            &&current.LoadGameAt is {} loaded&&contentAt>=loaded&&confirmedWall>=submittedWall&&confirmedWall<=now.AddMilliseconds(100)
            &&current.StatusWrittenAt is {} written&&written>=submittedWall&&written>before.StatusWrittenAt
            &&current.StatusReadAt is {} read&&read>=confirmedWall&&read>=written&&read<=now.AddMilliseconds(100)&&now-read<=TimeSpan.FromSeconds(2);
    }

    public static async Task<DockedFuelSnapshot> ReadDockedBudgetAsync(Func<PlayerState> player,Func<CancellationToken,Task> check,
        Func<double> reserve,bool interstellar,CancellationToken token,TimeSpan? timeout=null,TimeSpan? pollInterval=null)
    {
        using var deadline=CancellationTokenSource.CreateLinkedTokenSource(token);deadline.CancelAfter(timeout??TimeSpan.FromSeconds(10));
        FlightTelemetryUnavailableException? last=null;
        try
        {
            while(true)
            {
                // Safety, identity, cargo and user cancellation are outside the
                // narrow fuel-read retry: those errors must remain terminal.
                await check(deadline.Token);deadline.Token.ThrowIfCancellationRequested();var current=player();
                try
                {
                    var available=CurrentFuel(current,DateTimeOffset.UtcNow);
                    var required=DepartureBudget(current,reserve(),interstellar,DateTimeOffset.UtcNow);
                    return new(current,available,required);
                }
                catch(FlightTelemetryUnavailableException error){last=error;}
                await Task.Delay(pollInterval??TimeSpan.FromMilliseconds(100),deadline.Token);
            }
        }
        catch(OperationCanceledException)when(!token.IsCancellationRequested)
        {throw new InvalidDataException("站内燃油状态未在期限内确认,已保留任务及既有补给回执,未执行离站。",last);}
    }

    public static double DepartureBudget(PlayerState state,double reserve,bool interstellar,DateTimeOffset now)
    {
        if(!double.IsFinite(reserve)||reserve<0||reserve>100)throw new InvalidDataException("自动导航燃油储备无效。");
        if(!interstellar)return reserve+.25;
        var ship=state.Ships.SingleOrDefault(value=>value.ShipId==state.CurrentShipId)??throw new InvalidDataException("当前飞船身份缺失,无法核对燃油预算。");
        var drive=NavigationPreviewBuilder.Drive(ship)??throw new InvalidDataException("当前 FSD 装配未确认,无法核对燃油预算。");
        var route=state.Navigation.Route;var index=route.ToList().FindIndex(point=>point.SystemAddress==state.Navigation.CurrentSystemAddress);
        if(index<0||index>=route.Count-1)throw new InvalidDataException("当前原版航线不完整,尚未离站。");
        return NavigationFuelBudget.Calculate(state,ship,drive,CurrentFuel(state,now),reserve).UntilRefuel;
    }

    public static Station? CaptureOrigin(PlayerState state)=>state is {Docked:true,MarketId:>0,StationServicesKnown:true}
        &&state.Navigation.CurrentSystemAddress is >0&&!string.IsNullOrWhiteSpace(state.StationName)&&!string.IsNullOrWhiteSpace(state.SystemName)
        &&state.StationServices.Contains("refuel")
            ?new(){MarketId=state.MarketId.Value,Name=state.StationName,SystemName=state.SystemName,Position=state.Position}:null;

    public static bool CanReturn(PlayerState initial,PlayerState current,Station? origin,NavigationFuelResupplyRequiredException shortage,bool alreadyReturned)
        =>!alreadyReturned&&origin is not null&&!current.Docked&&current.InGame
            &&current.Navigation.CurrentSystemAddress==initial.Navigation.CurrentSystemAddress&&shortage.SystemAddress==initial.Navigation.CurrentSystemAddress
            &&current.SystemName.Equals(origin.SystemName,StringComparison.OrdinalIgnoreCase);

    public static void RequireSameCargo(PlayerState initial,PlayerState current)
    {
        if(!initial.CargoReliable||!current.CargoReliable||initial.CargoCapacity!=current.CargoCapacity||initial.CargoCount!=current.CargoCount
            ||!initial.Inventory.Select(item=>(item.Key,item.Count,item.Stolen,item.MissionId)).OrderBy(item=>item.Key).ThenBy(item=>item.MissionId).ThenBy(item=>item.Stolen)
                .SequenceEqual(current.Inventory.Select(item=>(item.Key,item.Count,item.Stolen,item.MissionId)).OrderBy(item=>item.Key).ThenBy(item=>item.MissionId).ThenBy(item=>item.Stolen)))
            throw new InvalidDataException("补油期间实际货舱或货物身份改变,已保留原任务并停止驾驶。");
    }
}