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 XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public sealed class StationDepartureTakeoverPolicyTests
{
    private static readonly DateTimeOffset Now=DateTimeOffset.UtcNow;
    private static readonly SystemNavigationTarget Origin=new(1,2,"Origin station",SystemTargetKind.Station,3);
    private static PlayerState Player=>new(){InGame=true,CommanderId="F1",CurrentShipId=7,LoadGameAt=Now.AddMinutes(-5),
        Navigation=new(){UndockedAt=Now.AddSeconds(-30)}};
    private static SystemFlightFrame Frame=>new(){Sequence=10,ObservedAt=Now,ShipEntity=4,ReferenceWorld=5,SystemAddress=1,SelectedTarget=Origin,
        GeometryVerified=true,InGame=true,Mode=LocalFlightMode.Normal,FsdReady=true,Position=new(0,0,9000),TargetPosition=Vector3d.Zero,Forward=new(0,0,1),Up=new(0,1,0)};
    private static StationDepartureAction Decide(SystemFlightFrame? frame=null,PlayerState? player=null,TimeSpan? elapsed=null,
        bool observed=true,bool active=false,bool requested=false,bool station=true,DateTimeOffset? requestedAt=null,bool duty=false)
        =>StationDepartureTakeoverPolicy.Decide(frame??Frame,Origin,player??Player,requestedAt??Now.AddMinutes(-1),elapsed??TimeSpan.FromSeconds(60),Now,observed,active,requested,duty,station);

    [Test]public void CompletedDepartureDoesNotRequireHavingSeenTheBriefNativeActiveFrame()
    {
        Check.Equal(StationDepartureAction.Completed,Decide());
        // The old docking object may be released after leaving the station;
        // actual native inactive flags and mass-lock release still establish completion.
        Check.Equal(StationDepartureAction.Completed,Decide(station:false));
        Check.Equal(StationDepartureAction.Completed,Decide(elapsed:StationDepartureWatchdog.RecoveryAfter));
    }
    [Test]public void AStillActiveOriginalDutyIsCancelledOnlyAfterTheStationIsClear()
    {
        Check.Equal(StationDepartureAction.WaitForNative,Decide(active:true));
        Check.Equal(StationDepartureAction.CancelNativeForTakeover,Decide(active:true,elapsed:StationDepartureTakeoverPolicy.TakeoverAfter));
        Check.Equal(StationDepartureAction.CancelNativeForTakeover,Decide(requested:true,elapsed:StationDepartureTakeoverPolicy.TakeoverAfter));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(Frame with{FsdReady=false},active:true,elapsed:StationDepartureTakeoverPolicy.TakeoverAfter));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(active:true,station:false,elapsed:StationDepartureTakeoverPolicy.TakeoverAfter));
        Check.That(StationDepartureTakeoverPolicy.TakeoverAfter<StationDepartureWatchdog.RecoveryAfter);
    }
    [Test]public void InsideTheStationNoRadialEscapeIsInventedAndRecoveryPrecedesThePenalty()
    {
        foreach(var frame in new[]{Frame with{FsdReady=false},Frame with{Mode=LocalFlightMode.Docked},Frame with{GeometryVerified=false},Frame with{ObservedAt=Now.AddSeconds(-1)}})
        {
            Check.Equal(StationDepartureAction.WaitForNative,Decide(frame,active:true,elapsed:StationDepartureTakeoverPolicy.TakeoverAfter));
            Check.Equal(StationDepartureAction.RecoverSession,Decide(frame,active:true,elapsed:StationDepartureWatchdog.RecoveryAfter));
        }
        Check.That(TimeSpan.FromMinutes(5)-StationDepartureWatchdog.RecoveryAfter>=TimeSpan.FromMinutes(1));
    }
    [Test]public void WrongTargetOldUndockAndUnverifiedNativeFlagsCannotClaimAuthority()
    {
        Check.Equal(StationDepartureAction.WaitForNative,Decide(Frame with{SelectedTarget=Origin with{MarketId=99}}));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(Frame with{SystemAddress=99}));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(Frame with{ShipEntity=0}));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(Frame with{Velocity=new(double.NaN,0,0)}));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(observed:false));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(player:Player with{InGame=false}));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(player:Player with{Docked=true}));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(player:Player with{Navigation=Player.Navigation with{UndockedAt=Now.AddMinutes(-2)}}));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(player:Player with{Navigation=Player.Navigation with{UndockedAt=Now.AddSeconds(6)}}));
    }
    [Test]public void JournalLeadUsesTheVerifiedClockAndKeepsNativeFreshnessOnWallTime()
    {
        var player=Player with{Status=new(16777232,0,32,1,0,null,null,null,null,[]),StatusAt=Now.AddSeconds(6.464),StatusWrittenAt=Now,StatusReadAt=Now,
            Navigation=Player.Navigation with{UndockedAt=Now.AddSeconds(6.464)}};
        Check.Equal(StationDepartureAction.Completed,Decide(player:player,requestedAt:Now.AddSeconds(5)));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(Frame with{ObservedAt=Now.AddSeconds(6)},player:player,requestedAt:Now.AddSeconds(5)));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(player:player with{StatusWrittenAt=null},requestedAt:Now.AddSeconds(5)));
        var behind=player with{StatusAt=Now.AddSeconds(-6.464),Navigation=Player.Navigation with{UndockedAt=Now.AddSeconds(-6.464)}};
        Check.Equal(StationDepartureAction.Completed,Decide(player:behind,requestedAt:Now.AddSeconds(-7)));
    }
    [Test]public void ManualTakeoverWinsEvenAtTheRecoveryDeadline()
    {
        Check.Equal(StationDepartureAction.ManualTakeover,Decide(Frame with{PlayerOverride=true},active:true,elapsed:StationDepartureWatchdog.RecoveryAfter));
    }
    private static StationDeparturePathEvidence ObserveOriginalExit(SystemFlightFrame end,PlayerState? player=null)
    {
        var proof=new StationDeparturePathEvidence();var state=player??Player;
        for(var index=0;index<4;index++)
        {
            var at=Now.AddMilliseconds(-800+index*200);
            var frame=end with{Sequence=index+1,ObservedAt=at,Position=end.Position-new Vector3d(0,0,100-index*30),Velocity=new(0,0,150)};
            proof.Observe(frame,Origin,state,Now.AddMinutes(-1),at,true,true,false,false);
        }
        proof.Observe(end,Origin,state,Now.AddMinutes(-1),Now,true,false,false,false);
        return proof;
    }
    private static StationDepartureAction WithPath(SystemFlightFrame frame,StationDeparturePathEvidence proof,TimeSpan? elapsed=null,PlayerState? player=null)
        =>StationDepartureTakeoverPolicy.Decide(frame,Origin,player??Player,Now.AddMinutes(-1),elapsed??TimeSpan.FromSeconds(80),Now,true,false,false,false,true,proof);
    [Test]public void OtherOriginalDutyCannotCompleteContinueOrBeCancelledAsAnOwnedLaunch()
    {
        Check.Equal(StationDepartureAction.WaitForNative,Decide(duty:true));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(duty:true,elapsed:StationDepartureTakeoverPolicy.TakeoverAfter));
        var frame=Frame with{Sequence=5,FsdReady=false,Position=new(0,0,4186),TargetExclusionRadius=3100};var proof=ObserveOriginalExit(frame);
        Check.Equal(StationDepartureAction.WaitForNative,StationDepartureTakeoverPolicy.Decide(frame,Origin,Player,Now.AddMinutes(-1),TimeSpan.FromSeconds(80),Now,true,false,false,true,true,proof));
        foreach(var ready in new[]{false,true})
            Check.Equal(StationDepartureAction.WaitForNative,StationDepartureTakeoverPolicy.Decide(frame with{FsdReady=ready},Origin,Player,Now.AddMinutes(-1),TimeSpan.FromSeconds(130),Now,false,false,false,false,true,proof));
    }

    [Test]public void OriginalNaturalExitAt4186MetresContinuesStraightWhileMassLockedAndOnlyCompletesWhenFsdReady()
    {
        var frame=Frame with{Sequence=5,FsdReady=false,Position=new(0,0,4186),TargetDistance=4186,Speed=0,Throttle=0,TargetExclusionRadius=3100};
        var state=Player with{Status=new(0x1010000,0,32,1,0,null,null,null,null,[])};
        var proof=ObserveOriginalExit(frame,state);
        Check.That(proof.OriginalDutyObserved&&proof.OriginalDutyNaturallyEnded&&proof.PathVerified);
        Check.Equal(StationDepartureAction.ContinueVerifiedDeparture,WithPath(frame,proof,player:state));
        Check.Equal(StationDepartureAction.Completed,WithPath(frame with{FsdReady=true},proof,player:state));
        Check.Equal(StationDepartureAction.RecoverSession,WithPath(frame,proof,StationDepartureWatchdog.RecoveryAfter,state));
        Check.Equal(.5,StationDeparturePathEvidence.ContinuationThrottle);
    }
    [Test]public void ContinuationUsesOriginalStationEnvelopeRatherThanAnArbitraryThreeKilometreRule()
    {
        var small=Frame with{Sequence=5,FsdReady=false,Position=new(0,0,500),TargetExclusionRadius=350};
        Check.Equal(StationDepartureAction.ContinueVerifiedDeparture,WithPath(small,ObserveOriginalExit(small)));
        var large=Frame with{Sequence=5,FsdReady=false,Position=new(0,0,4186),TargetExclusionRadius=5000};
        Check.Equal(StationDepartureAction.WaitForNative,WithPath(large,ObserveOriginalExit(large)));
        var unknown=small with{TargetExclusionRadius=null};
        Check.Equal(StationDepartureAction.WaitForNative,WithPath(unknown,ObserveOriginalExit(unknown)));
    }
    [Test]public void ColdInactiveCancelledAndUnobservedLaunchCannotInventAnExit()
    {
        var frame=Frame with{Sequence=5,FsdReady=false,Position=new(0,0,4186),TargetExclusionRadius=3100};
        var cold=new StationDeparturePathEvidence();cold.Observe(frame,Origin,Player,Now.AddMinutes(-1),Now,true,false,false,false);
        Check.Equal(StationDepartureAction.WaitForNative,WithPath(frame,cold));
        var cancelled=ObserveOriginalExit(frame);
        cancelled.Observe(frame with{Sequence=6,ObservedAt=Now.AddMilliseconds(80)},Origin,Player,Now.AddMinutes(-1),Now.AddMilliseconds(80),true,false,false,true);
        Check.That(!cancelled.CanContinue(frame,Now));
        Check.Equal(StationDepartureAction.WaitForNative,Decide(frame));
    }
    [Test]public void ProvenExitAuthorityIsWithdrawnOnAHeadingChangeReverseMotionEntityChangeOrStaleGeometry()
    {
        var end=Frame with{Sequence=5,FsdReady=false,Position=new(0,0,4186),TargetExclusionRadius=3100};
        foreach(var changed in new[]{end with{Forward=new(0,0,-1)},end with{Position=new(0,0,4180)},end with{Position=new(3,0,4188)},
            end with{ShipEntity=99},end with{GeometryVerified=false},end with{TargetExclusionRadius=null},end with{Velocity=new(0,0,-10)}})
        {
            var proof=ObserveOriginalExit(end);var next=changed with{Sequence=6,ObservedAt=Now.AddMilliseconds(80)};
            proof.Observe(next,Origin,Player,Now.AddMinutes(-1),next.ObservedAt,true,false,false,false);
            Check.That(!proof.CanContinue(next,next.ObservedAt));
        }
        var stale=ObserveOriginalExit(end);var gap=end with{Sequence=6,ObservedAt=Now.AddSeconds(2)};
        stale.Observe(gap,Origin,Player,Now.AddMinutes(-1),gap.ObservedAt,true,false,false,false);
        Check.That(!stale.CanContinue(gap,gap.ObservedAt));
        Check.Equal(StationDepartureAction.ManualTakeover,WithPath(end with{PlayerOverride=true},ObserveOriginalExit(end)));
    }
    private static (StationDeparturePathEvidence Proof,SystemFlightFrame Last) ActiveOutsideExit()
    {
        var proof=new StationDeparturePathEvidence();SystemFlightFrame last=Frame;
        for(var index=0;index<4;index++)
        {
            last=Frame with{Sequence=index+1,ObservedAt=Now.AddMilliseconds(-600+index*200),FsdReady=false,
                Position=new(0,0,3386.5+index*20),TargetDistance=3386.5+index*20,TargetExclusionRadius=3000,Speed=99,Velocity=new(0,0,99)};
            proof.Observe(last,Origin,Player,Now.AddMinutes(-1),last.ObservedAt,true,true,false,false);
        }
        Check.That(proof.PathVerified);return(proof,last);
    }
    [Test]public void RepeatedPhysicsPublicationDoesNotRevokeOrInventANaturalDutyTransition()
    {
        var (proof,last)=ActiveOutsideExit();
        for(var index=0;index<10;index++)proof.Observe(last,Origin,Player,Now.AddMinutes(-1),Now,true,true,false,false);
        proof.Observe(last,Origin,Player,Now.AddMinutes(-1),Now,true,false,false,false);
        Check.That(proof.PathVerified&&!proof.OriginalDutyNaturallyEnded);Check.Equal<string?>(null,proof.WithdrawalReason);
        var end=last with{Sequence=5,ObservedAt=Now.AddMilliseconds(200),Position=last.Position+new Vector3d(0,0,20),Velocity=Vector3d.Zero,Speed=0};
        proof.Observe(end,Origin,Player,Now.AddMinutes(-1),end.ObservedAt,true,false,false,false);
        Check.That(proof.OriginalDutyNaturallyEnded&&proof.CanContinue(end,end.ObservedAt));
    }
    [Test]public void BoundedGapDuringOriginalFlightRebuildsAContinuousSegmentBeforeTheNaturalExit()
    {
        var (proof,last)=ActiveOutsideExit();
        // Actual round-15 diagnostic endpoints were 3446.50 -> 3651.24 m,
        // with the original launch still active. The logger's two-second
        // cadence is not evidence that every underlying poll had this gap.
        var resumed=last with{Sequence=5,ObservedAt=Now.AddMilliseconds(2068),Position=new(0,0,3651.24),TargetDistance=3651.24};
        proof.Observe(resumed,Origin,Player,Now.AddMinutes(-1),resumed.ObservedAt,true,true,false,false);
        Check.That(!proof.PathVerified&&!proof.OriginalDutyNaturallyEnded);Check.Equal<string?>(null,proof.WithdrawalReason);
        for(var index=1;index<=3;index++)
        {
            var sample=resumed with{Sequence=5+index,ObservedAt=resumed.ObservedAt.AddMilliseconds(index*200),Position=resumed.Position+new Vector3d(0,0,index*20)};
            proof.Observe(sample,Origin,Player,Now.AddMinutes(-1),sample.ObservedAt,true,true,false,false);
            Check.Equal(index==3,proof.PathVerified);
        }
        var end=resumed with{Sequence=9,ObservedAt=resumed.ObservedAt.AddMilliseconds(800),Position=resumed.Position+new Vector3d(0,0,80),Speed=0,Velocity=Vector3d.Zero};
        proof.Observe(end,Origin,Player,Now.AddMinutes(-1),end.ObservedAt,true,false,false,false);
        Check.That(proof.CanContinue(end,end.ObservedAt));
    }
    [Test]public void MissingExitSamplesCannotBeReplacedByAStationaryInactiveEndpointOrALongGap()
    {
        foreach(var seconds in new[]{2d,6d})
        {
            var (proof,last)=ActiveOutsideExit();var resumed=last with{Sequence=5,ObservedAt=Now.AddSeconds(seconds),Position=last.Position+new Vector3d(0,0,100)};
            proof.Observe(resumed,Origin,Player,Now.AddMinutes(-1),resumed.ObservedAt,true,true,false,false);
            var end=resumed with{Sequence=6,ObservedAt=resumed.ObservedAt.AddMilliseconds(80),Speed=0,Velocity=Vector3d.Zero};
            proof.Observe(end,Origin,Player,Now.AddMinutes(-1),end.ObservedAt,true,false,false,false);
            Check.That(!proof.CanContinue(end,end.ObservedAt));Check.That(proof.WithdrawalReason is not null);
        }
    }
    [Test]public void AnOutsideGapWithReverseLateralOrHeadingDiscontinuityPermanentlyWithdrawsAuthority()
    {
        foreach(var cause in new[]{"reverse","lateral","heading","velocity"})
        {
            var (proof,last)=ActiveOutsideExit();var resumed=last with{Sequence=5,ObservedAt=Now.AddSeconds(2),Position=last.Position+new Vector3d(0,0,100)};
            resumed=cause switch
            {
                "reverse"=>resumed with{Position=last.Position-new Vector3d(0,0,2)},
                "lateral"=>resumed with{Position=resumed.Position+new Vector3d(3,0,0)},
                "heading"=>resumed with{Forward=new(0,0,-1)},
                _=>resumed with{Velocity=new(0,0,-10)}
            };
            proof.Observe(resumed,Origin,Player,Now.AddMinutes(-1),resumed.ObservedAt,true,true,false,false);
            Check.That(!proof.PathVerified);Check.Equal("unsafe-outside-path-across-observation-gap",proof.WithdrawalReason);
        }
    }
    [Test]public void DuplicateHandlingNeverMasksManualDamageIdentityOrConflictingPose()
    {
        foreach(var cause in new[]{"manual","damage","commander","ship","session","entity","target","conflicting-pose","reordered"})
        {
            var (proof,last)=ActiveOutsideExit();var state=Player;var sample=last;
            switch(cause)
            {
                case "manual":sample=sample with{PlayerOverride=true};break;
                case "damage":state=state with{Ships=[new(){ShipId=7,HullHealth=0}]};break;
                case "commander":state=state with{CommanderId="other"};break;
                case "ship":state=state with{CurrentShipId=8};break;
                case "session":state=state with{LoadGameAt=Now.AddMinutes(-2)};break;
                case "entity":sample=sample with{ShipEntity=99};break;
                case "target":sample=sample with{SelectedTarget=Origin with{MarketId=99}};break;
                case "conflicting-pose":sample=sample with{Position=sample.Position+new Vector3d(1,0,0)};break;
                case "reordered":sample=sample with{Sequence=3,ObservedAt=Now.AddMilliseconds(-80)};break;
            }
            proof.Observe(sample,Origin,state,Now.AddMinutes(-1),Now,true,true,false,false);
            Check.That(!proof.PathVerified&&proof.WithdrawalReason is not null);
        }
    }
}