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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

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

namespace XFEEDToolBox.Test;

[TestFixture]
public class NativeFrameProjectorTests
{
    private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-21T08:00:00Z");
    private static PlayerState Player(double fuel=5,bool scoop=true)=>new(){InGame=true,CurrentShipId=7,StatusAt=Now,
        Navigation=new(){CurrentSystemAddress=1},Status=new(16|(scoop?2048:0),0,fuel,1,0,null,null,null,null,[]),
        Ships=[new(){ShipId=7,Type="anaconda",ConfigurationAt=Now,FuelMain=32,UnladenMass=500,
            Modules=[new(){Slot="FrameShiftDrive",Item="int_hyperdrive_size6_class5",Enabled=true},new(){Slot="Slot01_Size6",Item="int_fuelscoop_size6_class5",Enabled=true}]}]};
    // Synthetic transport fixture, deliberately independent of game allocations and accounts.
    private static System.Text.Json.JsonElement Raw(double radius=1500,bool fresh=true,string fsdEntity="42",bool cooling=true,double heat=30,double nativePeak=88)=>Check.Event(new{
        sequence=1,observedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds(),entityId="42",localPlayerShipVerified=true,heatNormalizationVerified=true,heatDisplayScale=1d,
        position=new[]{radius,0d,0d},forward=new[]{0d,1d,0d},up=new[]{0d,0d,1d},heatPercent=heat,internalHeatPercent=heat,speedMetresPerSecond=30000d,playerOverride=false,
        fsd=new{fresh,entityId=fsdEntity,targetValid=true,targetAddress="2",targetDirection=new[]{-1d,0d,0d},targetDistance=1e16,state=2,duration=60d,clock=0d,speedFraction=.01,
            driveParametersValid=true,driveChargeSeconds=15d,driveHeatBase=37d,driveHeatRamp=0d,heatCapacity=334d,coolingConfigurationValid=true,coolingPassive=0d,coolingMaximum=67.15,coolingHeatLower=0d,coolingHeatUpper=1d},
        geometry=new{fresh=true,worldAddress="1",bodyAddress="1",bodyKind=1,starType=4,exclusionRadius=1000d},
        thermal=new{fresh=true,coolingActive=cooling,coolingScale=1d,idleInputUpper=20d,allowsCharging=true,fullChargeAndHoldPeak=nativePeak,evidence="Synthetic input/cooling fixture"}});
    [Test] public void ConvertsNativeHeatUnitsAndSeparatesFuelBudgetFromActualFuel()
    {
        var observation=new NativeFrameProjector("build").Project(Raw(),Player(),null,20,Now);
        Check.Equal(5d,observation.Frame.FuelMain!.Value);Check.That(observation.Frame.FuelNeededToNextRefuel>20);
        Check.That(Math.Abs(observation.Thermal!.ChargingGainUpperPerSecond-3700d/334)<1e-10);
        Check.Equal(100d,observation.Thermal.CoolingCurve!.HeatUpper);Check.Equal(2d,observation.Thermal.CoolingCurve.Exponent);
    }
    [Test]public void AShipWithoutAScoopNeverPromotesItsMinimumFuelBudgetToAnUnreachableScoopFillTarget()
    {
        var node=System.Text.Json.Nodes.JsonNode.Parse(Raw().GetRawText())!;node["adaptiveFlightControl"]=true;node["referenceWorldAddress"]="1";
        node["geometry"]!["referenceFrameVerified"]=true;node["geometry"]!["position"]=node["position"]!.DeepClone();
        node["geometry"]!["forward"]=node["forward"]!.DeepClone();node["geometry"]!["up"]=node["up"]!.DeepClone();
        node["geometry"]!["nearbyBodies"]=System.Text.Json.Nodes.JsonNode.Parse("[{\"bodyAddress\":\"1\",\"centre\":[0,0,0],\"exclusionRadius\":1000}]");
        var player=Player(5,false);player=player with{Ships=player.Ships.Select(ship=>ship with{Modules=ship.Modules.Where(module=>!module.Item.Contains("fuelscoop")).ToArray()}).ToArray()};
        var frame=new NativeFrameProjector("build").Project(Check.Json(node.ToJsonString()),player,null,.5,Now).Frame;
        Check.That(!frame.ScoopPowered&&!frame.RefuellingToFull);
        Check.Equal(frame.MinimumFuelToNextRefuel!.Value,frame.FuelNeededToNextRefuel!.Value);
        Check.That(frame.FuelNeededToNextRefuel<31.9);
    }
    [Test] public void HudNormalizationScalesBothHeatingAndCoolingWithoutChangingTheObservedTemperature()
    {
        var node=System.Text.Json.Nodes.JsonNode.Parse(Raw(heat:49).GetRawText())!;node["heatDisplayScale"]=1.5;
        var result=new NativeFrameProjector("build").Project(Check.Json(node.ToJsonString()),Player(),null,.5,Now);
        Check.Equal(49d,result.Frame.HeatPercent!.Value);
        Check.That(Math.Abs(result.Thermal!.ChargingGainUpperPerSecond-3700d/(334*1.5))<1e-10);
        Check.That(Math.Abs(result.Thermal.CoolingCurve!.HeatUpper-100/1.5)<1e-10);
    }
    [Test] public void RejectsStaleOrDifferentShipFsdBeforeProducingControlFrame()
    {
        Check.Throws<FlightTelemetryUnavailableException>(()=>new NativeFrameProjector("build").Project(Raw(fresh:false),Player(),null,.5,Now));
        Check.Throws<FlightTelemetryUnavailableException>(()=>new NativeFrameProjector("build").Project(Raw(fsdEntity:"NPC"),Player(),null,.5,Now));
        var cold=new NativeFrameProjector("build").Project(Raw(cooling:false),Player(),null,.5,Now);
        Check.That(cold.Thermal is null);
    }
    [Test] public void FirstIdleObservationWaitsForThePhysicsSnapshotToAgreeBeforeCharging()
    {
        foreach(var snapshot in new[]{0,1,2,3,4})
        {
            var node=System.Text.Json.Nodes.JsonNode.Parse(Raw().GetRawText())!;node["rawFsdState"]=snapshot;
            var frame=new NativeFrameProjector("build").Project(Check.Json(node.ToJsonString()),Player(),null,.5,Now).Frame;
            Check.Equal(snapshot==2,frame.CanBeginCharge);
            Check.Equal(snapshot==2?FsdPhase.Idle:FsdPhase.Cooldown,frame.Fsd);
        }
    }
    [Test] public void ReacquiresActualScoopBandAfterOutwardTrackingDriftUntilBudgetFilled()
    {
        var projector=new NativeFrameProjector("build");var first=projector.Project(Raw(),Player(),null,20,Now);
        var second=projector.Project(Raw(1520),Player(scoop:false) with{StatusAt=Now.AddSeconds(2)},null,20,Now.AddSeconds(2));
        Check.That(second.Frame.PreferredOrbitRadius<first.Frame.PreferredOrbitRadius);
        var full=projector.Project(Raw(1520),Player(32,false) with{StatusAt=Now.AddSeconds(4)},null,20,Now.AddSeconds(4));
        Check.Equal(second.Frame.PreferredOrbitRadius,full.Frame.PreferredOrbitRadius);
    }
    [Test] public void NativeNinetyPercentProtectionAlsoConstrainsManagedChargePermission()
    {
        Check.That(!new NativeFrameProjector("build").Project(Raw(nativePeak:92),Player(),null,.5,Now).Frame.NativeAllowsCharging!.Value);
        Check.That(!new NativeFrameProjector("build").Project(Raw(heat:90,nativePeak:92),Player(),null,.5,Now).Frame.NativeAllowsCharging!.Value);
    }
    [Test] public void AdaptiveFuelShortageLatchesAFullTankUntilTheActualTankIsFull()
    {
        var projector=new NativeFrameProjector("build");
        System.Text.Json.JsonElement Input(int index,DateTimeOffset at)
        {
            var node=System.Text.Json.Nodes.JsonNode.Parse(Raw().GetRawText())!;
            node["adaptiveFlightControl"]=true;node["sequence"]=index;node["observedAtUnixMilliseconds"]=at.ToUnixTimeMilliseconds();
            node["angularSpeedDegreesPerSecond"]=0;
            node["referenceWorldAddress"]="1";
            node["geometry"]!["referenceFrameVerified"]=true;
            node["geometry"]!["position"]=node["position"]!.DeepClone();node["geometry"]!["forward"]=node["forward"]!.DeepClone();node["geometry"]!["up"]=node["up"]!.DeepClone();
            node["geometry"]!["nearbyBodies"]=System.Text.Json.Nodes.JsonNode.Parse("[{\"bodyAddress\":\"1\",\"centre\":[0,0,0],\"exclusionRadius\":1000}]");
            node["geometry"]!["scoop"]=System.Text.Json.Nodes.JsonNode.Parse("{\"verified\":true,\"active\":true,\"fullRateRadius\":1100,\"outerRadius\":1500,\"maximumRate\":0.878,\"efficiency\":1}");
            return Check.Json(node.ToJsonString());
        }
        var first=projector.Project(Input(1,Now),Player(5),null,.5,Now).Frame;
        Check.That(first.RefuellingToFull&&first.FuelNeededToNextRefuel>31.9);
        Check.That(Math.Abs(first.PreferredOrbitRadius!.Value-1080)<1e-8,"The full-rate circle is selected before any measured inward search.");
        Check.Equal(1100d,first.ScoopFullRateRadius!.Value);Check.Equal(.878,first.ScoopRate!.Value);
        var at=Now.AddSeconds(1);
        var partial=projector.Project(Input(2,at),Player(12) with{StatusAt=at},null,.5,at).Frame;
        Check.That(partial.RefuellingToFull&&partial.FuelNeededToNextRefuel>31.9,"Being able to afford one jump must not stop a refill already in progress.");
        at=Now.AddSeconds(2);
        var full=projector.Project(Input(3,at),Player(32) with{StatusAt=at},null,.5,at).Frame;
        Check.That(!full.RefuellingToFull&&full.FuelNeededToNextRefuel<32);
    }
    [Test] public void NewerFuelScoopJournalTotalWinsOverAnOlderStatusFile()
    {
        var player=Player(5) with{Navigation=new(){CurrentSystemAddress=1,MainFuel=12,FuelAt=Now.AddMilliseconds(200)}};
        var frame=new NativeFrameProjector("build").Project(Raw(),player,null,.5,Now).Frame;
        Check.Equal(12d,frame.FuelMain!.Value);
    }
    [Test] public void AdaptiveDriverRejectsLegacySingleStarFallbackWhenPlanetGeometryFailed()
    {
        var node=System.Text.Json.Nodes.JsonNode.Parse(Raw().GetRawText())!;node["adaptiveFlightControl"]=true;
        node["geometry"]!["referenceFailureStage"]=7;
        Check.Throws<FlightTelemetryUnavailableException>(()=>new NativeFrameProjector("build").Project(Check.Json(node.ToJsonString()),Player(),null,.5,Now));
    }
    [Test] public void SupercruiseTransitionEventsDoNotPretendToBeHyperspaceArrival()
    {
        var projector=new NavigationProjector();
        projector.Apply(Check.Json("""{"event":"StartJump","timestamp":"2026-10-21T08:00:00Z","JumpType":"Supercruise"}"""));
        Check.Equal(Now,projector.State.SupercruiseTransitionAt!.Value);Check.That(!projector.State.JumpInProgress);
        projector.Apply(Check.Json("""{"event":"SupercruiseEntry","timestamp":"2026-10-21T08:00:05Z","SystemAddress":1}"""));
        Check.Equal(Now.AddSeconds(5),projector.State.SupercruiseEnteredAt!.Value);Check.That(projector.State.LastJumpAt is null);
    }
    [Test] public void SeparatesBinaryStarBodyIndexFromJournalSystemAddress()
    {
        Check.Equal(11666607908257L,NativeFrameProjector.SystemAddressFromBody(36040463626872225UL));
        Check.Equal(388451289451L,NativeFrameProjector.SystemAddressFromBody(388451289451UL));
        Check.Equal(11666607908257L,NativeFrameProjector.SystemAddressFromBody((511UL<<55)|11666607908257UL));
    }
    private static System.Text.Json.JsonElement StationFrame(bool matching=true,bool primary=true)
    {
        var node=System.Text.Json.Nodes.JsonNode.Parse(Raw(1e6).GetRawText())!;
        node["referenceWorldAddress"]="2630102182384369665";
        node["position"]=System.Text.Json.Nodes.JsonNode.Parse("[12,34,56]");
        node["fsd"]!["targetDirection"]=System.Text.Json.Nodes.JsonNode.Parse("[-0.1,0.99498743710662,0]");
        node["geometry"]=System.Text.Json.Nodes.JsonNode.Parse(System.Text.Json.JsonSerializer.Serialize(new{
            fresh=true,referenceFrameVerified=true,referenceBodyKind=6,worldAddress=matching?"2630102182384369665":"1",bodyAddress="1",bodyKind=1,starType=4,exclusionRadius=1000d,
            position=new[]{1e6,0d,0d},forward=new[]{-.1,.99498743710662,0d},up=new[]{0d,0d,1d},
            nearbyBodies=primary?new object[]{new{bodyAddress="1",bodyKind=1,starType=4,exclusionRadius=1000d,centre=new[]{0d,0d,0d}},
                new{bodyAddress="396316767208603649",bodyKind=2,starType=0,exclusionRadius=100d,centre=new[]{1e6,10000d,0d}}}:
                new object[]{new{bodyAddress="396316767208603649",bodyKind=2,starType=0,exclusionRadius=100d,centre=new[]{1e6,1000d,0d}}}}));
        return Check.Json(node.ToJsonString());
    }
    [Test] public void StationReferenceUsesLiveInertialPoseAndIncludesPlanetObstacles()
    {
        var state=Player(32,false) with{Status=Player(32,false).Status! with{Flags=8}};
        var observation=new NativeFrameProjector("build").Project(StationFrame(),state,null,.5,Now);
        var frame=observation.Frame;
        Check.Equal(new Vector3d(1e6,0,0),frame.Position);Check.Equal(1,frame.Obstacles.Count);
        Check.Equal(1L,frame.SystemAddress);Check.That(!frame.InSupercruise&&!frame.OutwardDepartureRequired);
        Check.That(Vector3d.Dot(frame.Position,frame.TargetPosition-frame.Position)<0);
        var decision=new FlightNavigator().Decide(frame,new(2,"Next",new(1,0,0),"K"),observation.Thermal,Now);
        Check.That(decision.BeginCharging,decision.Reason);Check.Equal(1d,decision.Throttle);
    }
    [Test] public void ChangedStationReferenceOrMissingPrimaryCannotProduceAControlFrame()
    {
        Check.Throws<FlightTelemetryUnavailableException>(()=>new NativeFrameProjector("build").Project(StationFrame(matching:false),Player(),null,.5,Now));
        Check.Throws<InvalidDataException>(()=>new NativeFrameProjector("build").Project(StationFrame(primary:false),Player(),null,.5,Now));
    }
    [Test] public void LegacyPlanetGeometryReportsMissingPluginSupportInsteadOfPoseLoss()
    {
        var node=System.Text.Json.Nodes.JsonNode.Parse(Raw().GetRawText())!;
        node["geometry"]!["bodyKind"]=2;node["geometry"]!["fresh"]=false;
        try{new NativeFrameProjector("build").Project(Check.Json(node.ToJsonString()),Player(),null,.5,Now);}
        catch(InvalidOperationException error){Check.That(error.Message.Contains("旧版导航插件")&&error.Message.Contains("参考系"));return;}
        throw new InvalidOperationException("Expected an explicit unsupported-reference error.");
    }
}