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);
}
}
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);
}
}