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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Data;
using System.Text.Json;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public sealed class NavigationFuelResupplyTests
{
    private static PlayerState StaticDocked(DateTimeOffset now)=>new(){InGame=true,Docked=true,MarketId=3,CommanderId="fuel-test",CurrentShipId=8,
        LoadGameAt=now.AddMinutes(-20),CargoReliable=true,CargoCapacity=1200,Status=new(1,0,24.524504,1,0,null,null,null,null,[]),
        StatusAt=now.AddMinutes(-15),StatusWrittenAt=now.AddMinutes(-15),StatusReadAt=now};

    [Test]public void ReconfirmedStaticFuelIsAllowedOnlyForTheSameDockedSessionAndCannotCompleteAPaidRefuel()
    {
        var now=DateTimeOffset.UtcNow;var state=StaticDocked(now);
        Check.Equal(24.524504,NavigationFuelResupply.CurrentFuel(state,now));
        foreach(var rejected in new[]{state with{Docked=false},state with{StatusReadAt=now.AddSeconds(-3)},state with{LoadGameAt=now.AddMinutes(-1)},
            state with{StatusWrittenAt=now.AddSeconds(2)},state with{Status=state.Status! with{Flags=16}},state with{Navigation=new(){MainFuel=40,FuelAt=now.AddMinutes(-14)}}})
            Check.Throws<FlightTelemetryUnavailableException>(()=>NavigationFuelResupply.CurrentFuel(rejected,now));
        var reread=state with{StatusReadAt=now.AddSeconds(1)};
        Check.That(!NavigationFuelResupply.IsPostReceiptSample(state,reread,now.AddMilliseconds(-500),now,now.AddSeconds(1),true));
        var newSample=reread with{StatusAt=now.AddSeconds(2),StatusWrittenAt=now.AddMilliseconds(200),StatusReadAt=now.AddMilliseconds(300),Status=state.Status! with{FuelMain=128}};
        Check.That(NavigationFuelResupply.IsPostReceiptSample(state,newSample,now.AddMilliseconds(-500),now,now.AddMilliseconds(300),true));
    }

    [Test]public void ARealSameSecondFuelWriteAfterTheReceiptIsConfirmedButRereadingOrTouchingOldContentIsNot()
    {
        var receipt=DateTimeOffset.Parse("2026-10-26T18:52:07.4761144Z");var submitted=receipt.AddMilliseconds(-300);var now=receipt.AddSeconds(1);
        var before=StaticDocked(now) with{StatusAt=DateTimeOffset.Parse("2026-10-26T18:52:13Z"),StatusWrittenAt=submitted.AddMilliseconds(-100),StatusReadAt=submitted};
        var updated=before with{Status=before.Status! with{FuelMain=128},StatusWrittenAt=DateTimeOffset.Parse("2026-10-26T18:52:07.8714283Z"),StatusReadAt=now};
        Check.Equal(before.StatusAt,updated.StatusAt);Check.That(NavigationFuelResupply.IsPostReceiptSample(before,updated,submitted,receipt,now,true));
        Check.That(!NavigationFuelResupply.IsPostReceiptSample(before,before with{StatusReadAt=now},submitted,receipt,now,true));
        Check.That(!NavigationFuelResupply.IsPostReceiptSample(before,before with{StatusWrittenAt=updated.StatusWrittenAt,StatusReadAt=now},submitted,receipt,now,true));
        Check.That(!NavigationFuelResupply.IsPostReceiptSample(before,updated with{CurrentShipId=9},submitted,receipt,now,true));
        Check.That(!NavigationFuelResupply.IsPostReceiptSample(before,updated with{StatusReadAt=receipt.AddMilliseconds(-1)},submitted,receipt,now,true));
        Check.That(!NavigationFuelResupply.IsPostReceiptSample(before,updated with{StatusWrittenAt=submitted.AddMilliseconds(-1)},submitted,receipt,now,true));
        // The original game can publish Status before the host receives its
        // confirmed reply. It still belongs to the same submitted mutation.
        Check.That(NavigationFuelResupply.IsPostReceiptSample(before,updated with{StatusWrittenAt=receipt.AddMilliseconds(-50)},submitted,receipt,now,true));
    }

    [Test]public void AReservoirOnlyPaidRefuelConfirmsTheServiceAndReplansOnlyWhenTotalFuelMassIncreases()
    {
        var now=DateTimeOffset.UtcNow;var submitted=now.AddSeconds(-1);var receipt=now.AddMilliseconds(-500);
        var before=StaticDocked(now) with{StatusAt=now,StatusWrittenAt=submitted.AddSeconds(-1),StatusReadAt=submitted.AddMilliseconds(-10),
            Status=StaticDocked(now).Status! with{FuelMain=128,FuelReserve=1}};
        var reservoirOnly=before with{Status=before.Status! with{FuelReserve=1.0625},StatusWrittenAt=now.AddMilliseconds(-200),StatusReadAt=now};
        Check.Equal(128d,NavigationFuelResupply.CurrentFuel(reservoirOnly,now));
        Check.That(NavigationFuelResupply.IsPostReceiptSample(before,reservoirOnly,submitted,receipt,now,true));
        Check.That(NavigationFuelResupply.FuelMassIncreased(before,reservoirOnly));
        // The main tank can feed the reservoir before the next status sample.
        // Its new reservoir value proves the paid result, but unchanged total
        // fuel mass does not require another original route calculation.
        var noMassIncrease=reservoirOnly with{Status=reservoirOnly.Status! with{FuelMain=127.9375}};
        Check.That(NavigationFuelResupply.IsPostReceiptSample(before,noMassIncrease,submitted,receipt,now,true));
        Check.That(!NavigationFuelResupply.FuelMassIncreased(before,noMassIncrease));
        Check.That(!NavigationFuelResupply.IsPostReceiptSample(before,reservoirOnly,submitted,receipt,now,false));
        Check.That(!NavigationFuelResupply.IsPostReceiptSample(before,before with{StatusWrittenAt=reservoirOnly.StatusWrittenAt,StatusReadAt=now},submitted,receipt,now,true));
        Check.That(!NavigationFuelResupply.IsPostReceiptSample(before,reservoirOnly with{Status=reservoirOnly.Status! with{FuelReserve=null}},submitted,receipt,now,true));
    }

    [Test]public async Task DockedFuelReadWaitsForAReconfirmedSampleBeforeReturningTheBudget()
    {
        var state=StaticDocked(DateTimeOffset.UtcNow) with{StatusReadAt=null};var initial=state;var checks=0;var paid=false;
        var snapshot=await NavigationFuelResupply.ReadDockedBudgetAsync(()=>state,cancellation=>
        {
            cancellation.ThrowIfCancellationRequested();NavigationFuelResupply.RequireSameCargo(initial,state);checks++;Check.That(!paid);
            if(checks==2)state=state with{StatusReadAt=DateTimeOffset.UtcNow};return Task.CompletedTask;
        },()=>.5,false,default,TimeSpan.FromSeconds(1),TimeSpan.FromMilliseconds(1));
        paid=true;Check.Equal(2,checks);Check.Equal(24.524504,snapshot.Available);Check.Equal(.75,snapshot.Required);
    }

    [Test]public async Task FuelWaitIsBoundedAndNeverRetriesIdentitySafetyOrUserCancellation()
    {
        var stale=StaticDocked(DateTimeOffset.UtcNow) with{StatusReadAt=null};var checks=0;var bounded=false;
        try{await NavigationFuelResupply.ReadDockedBudgetAsync(()=>stale,_=>{checks++;return Task.CompletedTask;},()=>.5,false,default,TimeSpan.FromMilliseconds(30),TimeSpan.FromMilliseconds(1));}
        catch(InvalidDataException error){bounded=error.InnerException is FlightTelemetryUnavailableException;}
        Check.That(bounded&&checks>0);
        foreach(var cause in new[]{"identity","cargo","safety","cancel"})
        {
            using var stop=new CancellationTokenSource();checks=0;Exception? actual=null;
            try
            {
                await NavigationFuelResupply.ReadDockedBudgetAsync(()=>stale,cancellation=>
                {
                    checks++;if(checks==1)return Task.CompletedTask;
                    if(cause=="cancel"){stop.Cancel();cancellation.ThrowIfCancellationRequested();}
                    if(cause=="safety")throw new FlightHealthPauseException();
                    if(cause=="cargo")NavigationFuelResupply.RequireSameCargo(stale,stale with{CargoCount=1});
                    throw new InvalidDataException("identity changed");
                },()=>.5,false,stop.Token,TimeSpan.FromSeconds(1),TimeSpan.FromMilliseconds(1));
            }
            catch(Exception error){actual=error;}
            Check.Equal(2,checks);Check.That(cause switch{"cancel"=>actual is OperationCanceledException,"safety"=>actual is FlightHealthPauseException,_=>actual is InvalidDataException});
        }
    }

    [Test]public async Task ARealUnchangedDockedStatusFileRetainsItsFuelAndTimestampAcrossMonitorReconfirmation()
    {
        using var directory=new TemporaryDirectory();var eventAt=DateTimeOffset.UtcNow.AddMinutes(-15);
        await File.WriteAllTextAsync(directory.File("Journal.01.log"),string.Join('\n',new object[]{
            new{@event="LoadGame",timestamp=eventAt.AddMinutes(-1),FID="fuel-test",Commander="Pilot",ShipID=8,GameMode="Solo"},
            new{@event="Location",timestamp=eventAt.AddSeconds(-1),Docked=true,MarketID=3,StationName="Origin",StarSystem="Here",SystemAddress=1},
            new{@event="Cargo",timestamp=eventAt.AddSeconds(-1),Vessel="Ship",Inventory=Array.Empty<object>()}}
            .Select(value=>JsonSerializer.Serialize(value)))+"\n");
        var status=directory.File("Status.json");var original=JsonSerializer.Serialize(new{timestamp=eventAt,@event="Status",Flags=1,Fuel=new{FuelMain=24.524504,FuelReservoir=1},Cargo=0});
        await File.WriteAllTextAsync(status,original);File.SetLastWriteTimeUtc(status,eventAt.UtcDateTime);
        await using var monitor=new JournalMonitor(directory.Path);await monitor.StartAsync();var first=monitor.State.StatusReadAt;
        Check.Equal(24.524504,NavigationFuelResupply.CurrentFuel(monitor.State,DateTimeOffset.UtcNow));await Task.Delay(1100);
        Check.That(monitor.State.StatusReadAt>first);Check.Equal(eventAt,monitor.State.StatusAt!.Value);
        Check.Equal(original,await File.ReadAllTextAsync(status));Check.Equal(24.524504,NavigationFuelResupply.CurrentFuel(monitor.State,DateTimeOffset.UtcNow));
        var now=DateTimeOffset.UtcNow;
        Check.That(!NavigationFuelResupply.IsPostReceiptSample(monitor.State,monitor.State,now.AddSeconds(-1),now.AddMilliseconds(-500),now,true));
    }
    [Test]public void ActualUnscoopableTwoLegBudgetIncludesTheReserveInsteadOfDemandingAFull128TonneTank()
    {
        var now=DateTimeOffset.UtcNow;
        var ship=new ShipRecord{ShipId=8,UnladenMass=1494.5,FuelMain=128,ConfigurationAt=now,
            Modules=[new(){Slot="FrameShiftDrive",Item="int_hyperdrive_overcharge_size7_class5",Enabled=true,
                Engineering=new(){Modifiers=[new("FSDOptimalMass",3949.799805,null,false)]}}]};
        var player=new PlayerState{CurrentShipId=8,Ships=[ship],CargoReliable=true,Status=new(0,0,30,1.104355,0,null,null,null,null,[]),StatusAt=now,
            Navigation=new(){CurrentSystemAddress=1,Route=[new(1,"HR 8061",new(33.46875,-37.9375,39.9375),"K"),
                new(2,"LB 3303",new(22.9375,-22.9375,6.5625),"T"),new(3,"LHS 1731",new(15.71875,-15.46875,-20.40625),"M")]}};
        Check.That(!NavigationPreviewBuilder.HasScoop(ship));
        Check.That(Math.Abs(NavigationFuelResupply.DepartureBudget(player,.5,true,now)-26.95)<.00001);
        Check.Equal(.75,NavigationFuelResupply.DepartureBudget(player,.5,false,now));
    }
    [Test]public void OnlyTheMinimumBudgetCanRequestStationFuelAndAScoopRemainsUsable()
    {
        var frame=new FlightFrame{SystemAddress=1,FuelMain=30,FuelRequiredForJump=13.1,FuelNeededToNextRefuel=127.99,MinimumFuelToNextRefuel=26.95};
        Check.That(NavigationFuelResupply.Required(frame) is null);
        var shortage=NavigationFuelResupply.Required(frame with{FuelMain=24.524504});
        Check.That(shortage is not null);Check.Equal(26.95,shortage!.Required);Check.Equal(1L,shortage.SystemAddress);
        Check.That(NavigationFuelResupply.Required(frame with{FuelMain=1,ScoopPowered=true}) is null);
        Check.That(NavigationFuelResupply.Required(frame with{FuelMain=1,Fsd=FsdPhase.Jumping}) is null);
        Check.That(NavigationFuelResupply.Required(frame with{FuelMain=double.NaN}) is null);
    }
    [Test]public void FuelReturnRequiresThisJourneysOriginalDockAndTheSameSystem()
    {
        var initial=new PlayerState{InGame=true,Docked=true,MarketId=3,StationName="Known origin",StationServicesKnown=true,StationServices=["refuel"],SystemName="Here",Navigation=new(){CurrentSystemAddress=1}};
        var origin=NavigationFuelResupply.CaptureOrigin(initial);var flying=initial with{Docked=false};var shortage=new NavigationFuelResupplyRequiredException(1,1,2);
        Check.That(origin is not null&&NavigationFuelResupply.CanReturn(initial,flying,origin,shortage,false));
        Check.That(!NavigationFuelResupply.CanReturn(initial,flying,origin,shortage,true));
        Check.That(!NavigationFuelResupply.CanReturn(initial,flying,null,shortage,false));
        Check.That(!NavigationFuelResupply.CanReturn(initial,flying with{Navigation=new(){CurrentSystemAddress=2}},origin,shortage,false));
        Check.That(NavigationFuelResupply.CaptureOrigin(initial with{StationServices=[]}) is null);
        Check.That(NavigationFuelResupply.CaptureOrigin(initial with{StationName=""}) is null);
    }
    [Test]public void FuelPreparationRequiresFreshFiniteFuelAndTheActualCargoIdentity()
    {
        var now=DateTimeOffset.UtcNow;var player=new PlayerState{CargoReliable=true,CargoCapacity=1200,CargoCount=1,Inventory=[new("water","Water",1)],Navigation=new(){MainFuel=30,FuelAt=now}};
        Check.Equal(30d,NavigationFuelResupply.CurrentFuel(player,now));
        Check.Throws<FlightTelemetryUnavailableException>(()=>NavigationFuelResupply.CurrentFuel(player with{Navigation=player.Navigation with{FuelAt=now.AddSeconds(-11)}},now));
        Check.Throws<FlightTelemetryUnavailableException>(()=>NavigationFuelResupply.CurrentFuel(player with{Navigation=player.Navigation with{MainFuel=double.NaN}},now));
        NavigationFuelResupply.RequireSameCargo(player,player);
        Check.Throws<InvalidDataException>(()=>NavigationFuelResupply.RequireSameCargo(player,player with{Inventory=[new("gold","Gold",1)]}));
        Check.Throws<InvalidDataException>(()=>NavigationFuelResupply.RequireSameCargo(player,player with{CargoReliable=false}));
    }
}