using XFEEDToolBox.Core.Data; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Session; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class ShipProtectionTests { private static readonly DateTimeOffset Now=DateTimeOffset.Parse("2026-10-22T00:10:00Z"); private static readonly GameSessionTarget Target=new(12,Now.AddMinutes(-2).UtcTicks,"game","launcher",true); private static PlayerState Player(double health=1,bool shield=true)=>new(){CommanderId="F1",CurrentShipId=7,InGame=true,LoadGameAt=Now.AddMinutes(-1),StatusAt=Now, Status=new((1L<<24)|(shield?8:0),0,1,1,0,null,null,null,null,[]),Ships=[new(){ShipId=7,HullHealth=health,HullHealthAt=Now.AddSeconds(-2),ConfigurationAt=Now.AddSeconds(-2),Modules=[new(){Item="int_shieldgenerator_size6_class5",Enabled=true}]}]}; private static ShieldRestartGuard Guard(PlayerState? player=null) {var guard=new ShieldRestartGuard();guard.SetEnabled(true,Now.AddSeconds(-1));guard.Prime(player??Player(),Target);return guard;} private static ObservedJournalEvent Hull(long sequence,double health,bool pilot=true,bool fighter=false,DateTimeOffset? at=null) =>new(sequence,Now,Check.Event(new{timestamp=at??Now,@event="HullDamage",Health=health,PlayerPilot=pilot,Fighter=fighter})); private static ShipHealthSample Sample(long sequence,double hull,double? shield,bool damage=false)=>new(sequence,Now,Target.ProcessId,Target.StartedUtcTicks,100,7,hull,shield,damage); [Test] public void HullDamageBelowHalfTriggersOnceAndFiftyPercentDoesNot() { var guard=Guard();Check.That(!guard.Observe(Hull(1,.5),Player(),Target)); Check.That(guard.Observe(Hull(2,.499),Player(),Target));Check.Equal(ShipProtectionReason.HullBelowHalf,guard.LastTrigger!.Reason); Check.That(guard.TestingBlocked);Check.That(!guard.Observe(Hull(3,.4),Player(),Target)); } [Test] public void FirstLoadBelowHalfIsAHistoricalBaselineRatherThanAPermanentTestingBlock() { var player=Player(.4,false);var guard=Guard(player);Check.That(!guard.TestingBlocked); Check.That(!guard.Observe(new(1,Now,Check.Event(new{timestamp=Now,@event="Status",Flags=1L<<24})),player,Target)); Check.That(!guard.ObserveHealth(Sample(1,.4,0),player,Target,Now)); Check.That(!guard.ObserveHealth(Sample(2,.4,0),player,Target,Now)); guard.SetEnabled(true,Now.AddSeconds(-1));guard.Prime(player,Target); Check.That(!guard.TestingBlocked);Check.That(!guard.ObserveHealth(Sample(3,.4,0),player,Target,Now)); } [Test] public void NativeDamageCrossingThresholdUsesContinuousHealthAndTriggersWithoutJournalStep() { var guard=Guard();Check.That(!guard.ObserveHealth(Sample(1,.501,1),Player(),Target,Now)); Check.That(guard.ObserveHealth(Sample(2,.499,.9),Player(),Target,Now));Check.That(guard.TestingBlocked); } [Test] public void DockedBaselineStillProtectsTheFirstDamageFrameAfterLaunch() { var flying=Player();var docked=flying with{Docked=true,Status=flying.Status! with{Flags=flying.Status.Flags|1}};var guard=Guard(docked); Check.That(!guard.ObserveHealth(Sample(1,1,1),docked,Target,Now)); Check.That(guard.ObserveHealth(Sample(2,1,0),flying,Target,Now));Check.Equal(ShipProtectionReason.ShieldsLost,guard.LastTrigger!.Reason); var noShield=Player(1,false) with{Docked=true,Status=docked.Status! with{Flags=docked.Status.Flags&~8L}};guard=Guard(noShield); Check.That(!guard.ObserveHealth(Sample(1,1,0),noShield,Target,Now));Check.That(!guard.ObserveHealth(Sample(2,1,0),Player(1,false),Target,Now)); } [Test] public void InitialZeroShieldDoesNotTriggerButFurtherHullDamageDoes() { var guard=Guard(Player(1,false));Check.That(!guard.ObserveHealth(Sample(1,1,0),Player(1,false),Target,Now)); Check.That(!guard.ObserveHealth(Sample(2,1,0),Player(1,false),Target,Now)); Check.That(guard.ObserveHealth(Sample(3,.99,0),Player(1,false),Target,Now));Check.Equal(ShipProtectionReason.ShieldsLost,guard.LastTrigger!.Reason); } [Test] public void LostShieldUsesDamageEdgeAndIgnoresRepeatedZeroSamples() { var guard=Guard();Check.That(!guard.ObserveHealth(Sample(1,1,.01),Player(),Target,Now)); Check.That(guard.ObserveHealth(Sample(2,1,0),Player(1,false),Target,Now)); Check.That(!guard.ObserveHealth(Sample(3,1,0),Player(1,false),Target,Now)); } [Test] public void ShipOrProcessChangeCannotTurnBaselineIntoDamage() { var guard=Guard();Check.That(!guard.ObserveHealth(Sample(1,1,1),Player(),Target,Now)); Check.That(!guard.ObserveHealth(Sample(2,.1,0) with{EntityId=200},Player(),Target,Now)); Check.That(!guard.ObserveHealth(Sample(3,.09,0) with{ProcessId=99},Player(),Target,Now)); Check.That(!guard.ObserveHealth(Sample(3,.09,0) with{ProcessStartedUtcTicks=Target.StartedUtcTicks+1},Player(),Target,Now)); Check.That(!guard.ObserveHealth(Sample(3,.09,0) with{ShipId=99},Player(),Target,Now)); } [Test] public void DuplicateHistoricalInvalidAndFighterDamageAreRejected() { foreach(var e in new[]{Hull(1,.1,false),Hull(1,.1,true,true),Hull(1,.1,at:Now.AddMinutes(-1)),Hull(1,-1),Hull(1,2)}) Check.That(!Guard().Observe(e,Player(),Target)); var guard=Guard();Check.That(!guard.ObserveHealth(Sample(1,1,1),Player(),Target,Now)); foreach(var s in new[]{Sample(1,.1,0),Sample(2,.1,0) with{At=Now.AddSeconds(-1)},Sample(2,.1,0) with{At=Now.AddMilliseconds(1)},Sample(2,double.NaN,0),Sample(2,.1,double.NaN)}) Check.That(!guard.ObserveHealth(s,Player(),Target,Now)); } [Test] public void HistoricalLowHullAndReprimingDoNotCreateADamageEpisode() { var guard=Guard(Player(.4,false));Check.That(!guard.TestingBlocked); guard.Prime(Player(.4,false),Target);Check.That(!guard.TestingBlocked); guard.Prime(Player(.9) with{Ships=[Player(.9).Ships[0] with{HullHealthAt=Now}]},Target);Check.That(!guard.TestingBlocked); } [Test]public void FreshStableDamageObservationsReleaseTheBlockAfterThreeSecondsWithoutRepairingOrRestarting() { var player=Player(.4,false);var guard=Guard(player); Check.That(!guard.ObserveHealth(Sample(1,.4,0),player,Target,Now)); var damaged=Now.AddMilliseconds(100); Check.That(guard.ObserveHealth(Sample(2,.39,0) with{At=damaged},player,Target,damaged));Check.That(guard.TestingBlocked); for(var tick=1;tick<=30;tick++) { var at=damaged.AddMilliseconds(tick*100); Check.That(!guard.ObserveHealth(Sample(tick+2,.39,0) with{At=at},player,Target,at)); Check.Equal(tick<30,guard.TestingBlocked); } var later=damaged.AddSeconds(3.1); Check.That(guard.ObserveHealth(Sample(33,.38,0) with{At=later},player,Target,later));Check.That(guard.TestingBlocked); } [Test]public void DuplicateStaleAndInterruptedHealthCannotCountAsThreeStableSeconds() { var player=Player(.4,false);var guard=Guard(player);guard.ObserveHealth(Sample(1,.4,0),player,Target,Now); guard.ObserveHealth(Sample(2,.39,0),player,Target,Now);Check.That(guard.TestingBlocked); var later=Now.AddSeconds(4); Check.That(!guard.ObserveHealth(Sample(2,.39,0),player,Target,later));Check.That(guard.TestingBlocked); guard.ObserveHealth(Sample(3,.39,0) with{At=later},player,Target,later);Check.That(guard.TestingBlocked); for(var tick=1;tick<=29;tick++) {var at=later.AddMilliseconds(tick*100);guard.ObserveHealth(Sample(tick+3,.39,0) with{At=at},player,Target,at);Check.That(guard.TestingBlocked);} var finished=later.AddSeconds(3);guard.ObserveHealth(Sample(33,.39,0) with{At=finished},player,Target,finished);Check.That(!guard.TestingBlocked); guard.ObserveHealth(Sample(34,0,0) with{At=finished.AddMilliseconds(100)},player,Target,finished.AddMilliseconds(100));Check.That(guard.TestingBlocked); } [Test]public void ANewEntityOrSessionStartsANewBaselineWithoutRetainingThePreviousDamageLatch() { var player=Player(.4,false);var guard=Guard(player);guard.ObserveHealth(Sample(1,.4,0),player,Target,Now); guard.ObserveHealth(Sample(2,.39,0),player,Target,Now);Check.That(guard.TestingBlocked); Check.That(!guard.ObserveHealth(Sample(3,.39,0) with{EntityId=200},player,Target,Now));Check.That(!guard.TestingBlocked); guard.ObserveHealth(Sample(4,.38,0) with{EntityId=200},player,Target,Now);Check.That(guard.TestingBlocked); guard.Prime(player with{LoadGameAt=Now.AddSeconds(-2)},Target);Check.That(!guard.TestingBlocked); } [Test] public void LiveDamageProjectionTracksMainShipWithoutChangingEquipmentSnapshot() { var projector=new PlayerProjector();projector.Apply(Check.Event(new{timestamp=Now,@event="Loadout",ShipID=7,Ship="anaconda",HullHealth=.9,Modules=new object[0]})); var config=projector.State.Ships[0].ConfigurationAt; projector.Apply(Hull(1,.4).Data);Check.Equal(.4,projector.State.Ships[0].HullHealth);Check.Equal(config,projector.State.Ships[0].ConfigurationAt); projector.Apply(Hull(2,.1,true,true).Data);Check.Equal(.4,projector.State.Ships[0].HullHealth); } }