using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class CruiseOverchargePolicyTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.FromUnixTimeMilliseconds(100000);
private static readonly string Hash=new('a',64);
private static ScoThermalInput Thermal=>new(){Verified=true,HeatCapacity=100,HeatDisplayScale=1,ScoInputUpper=10,OtherInputUpper=5,
CoolingPassive=10,CoolingMaximum=10,CoolingScale=1,CoolingHeatLower=0,CoolingHeatUpper=1,LeaseRemainingSeconds=.25,ResponseDelaySeconds=.01,AfterStopSeconds=.5};
private static SystemFlightFrame Flight=>new(){ObservedAt=Now,InGame=true,GeometryVerified=true,GameSha256=Hash,ShipEntity=7,ReferenceWorld=8,SystemAddress=1,
Mode=LocalFlightMode.Supercruise,Position=new(0,0,0),TargetPosition=new(0,0,1e12),Forward=new(0,0,1),TargetDistance=1e12,Speed=1e9,
SelectedTarget=new(1,9,"Test Station",SystemTargetKind.Station,10),SupercruiseAssist=NativeAssistState.Unavailable};
private static CruiseOverchargeObservation Observation=>new(){ObservedAt=Now,ShipEntity=7,ReferenceWorld=8,GameSha256=Hash,
OriginalInterfacesVerified=true,OriginalCanEngage=true,NativeFlightConstraintsVerified=true,NativeGeometryVerified=true,State=CruiseOverchargeState.Idle,HeatPercent=40,HeatRisePercentPerSecond=0,
FuelMainTonnes=32,FuelCapacityTonnes=32,FuelBurnTonnesPerSecond=0,FuelRateUpperTonnesPerSecond=1,ThermalInput=Thermal};
private static CruiseOverchargeDecision Decide(CruiseOverchargeObservation? sample=null,SystemFlightFrame? flight=null,double reserve=8)
=>new CruiseOverchargePolicy().Decide(flight??Flight,sample??Observation,reserve,Now);
[Test]public void VerifiedCoolAlignedDistantShipCanRequestBoost()=>Check.That(Decide().UseOvercharge);
[Test]public void NativeScoConstraintsStopOnlyBoostAndCanRecoverWithoutChangingThePrimaryFrame()
{
foreach(var blocked in new[]{Observation with{NativeCollisionCourse=true},Observation with{NativeGeometryVerified=false},Observation with{NativeAssistEnabled=true},Observation with{NativeFlightConstraintsVerified=false}})
Check.That(!Decide(blocked).UseOvercharge);
var policy=new CruiseOverchargePolicy();Check.Equal(CruiseOverchargeReason.Geometry,policy.Decide(Flight,Observation with{NativeCollisionCourse=true},8,Now).Reason);
Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
}
[Test]public void TheObserved785LightSecondCaseContinuesWithTheVerifiedOriginalApproachWindow()
{
const double ls=299792458;var distance=784.9268650541314*ls;
var flight=Flight with{TargetDistance=distance,TargetPosition=new(0,0,distance),Speed=26.417687132075883*ls,CruiseParametersVerified=true,CruiseApproachSeconds=6};
Check.That(Decide(Observation with{State=CruiseOverchargeState.Active,HeatPercent=29.8585},flight).UseOvercharge);
Check.Equal(CruiseOverchargeReason.TargetClose,Decide(Observation with{State=CruiseOverchargeState.Active},flight with{CruiseParametersVerified=false}).Reason);
Check.Equal(CruiseOverchargeReason.TargetClose,Decide(Observation with{State=CruiseOverchargeState.Active},flight with{TargetDistance=100*ls,TargetPosition=new(0,0,100*ls)}).Reason);
}
[Test]public void FinalApproachDoesNotReigniteWhenScoStopsAndTheDisplayedEtaGrowsAgain()
{
const double ls=299792458;var policy=new CruiseOverchargePolicy();var flight=Flight with{CruiseParametersVerified=true,CruiseApproachSeconds=6,TargetDistance=900*ls,TargetPosition=new(0,0,900*ls),Speed=20*ls};
Check.That(policy.Decide(flight,Observation,8,Now).UseOvercharge);
var at=Now.AddSeconds(1);flight=flight with{ObservedAt=at,TargetDistance=300*ls,TargetPosition=new(0,0,300*ls),Speed=30*ls};
Check.Equal(CruiseOverchargeReason.TargetClose,policy.Decide(flight,Observation with{ObservedAt=at,State=CruiseOverchargeState.Active},8,at).Reason);
at=at.AddSeconds(2);flight=flight with{ObservedAt=at,Speed=ls};
Check.Equal(CruiseOverchargeReason.TargetClose,policy.Decide(flight,Observation with{ObservedAt=at},8,at).Reason);
flight=flight with{SelectedTarget=new(1,10,"Other station",SystemTargetKind.Station,11)};
Check.That(policy.Decide(flight,Observation with{ObservedAt=at},8,at).UseOvercharge);
}
[Test]public void AnOwnedNativeApproachStopAlsoPreventsASecondShortBurst()
{
var policy=new CruiseOverchargePolicy();policy.ObserveNativeApproachStop(Flight);
Check.Equal(CruiseOverchargeReason.TargetClose,policy.Decide(Flight,Observation,8,Now).Reason);
Check.That(policy.Decide(Flight with{ShipEntity=8},Observation with{ShipEntity=8},8,Now).UseOvercharge);
}
[Test]public void RisingHeatStopsBeforeTheTemperatureCeiling()
{var value=Decide(Observation with{HeatPercent=99,HeatRisePercentPerSecond=-5});Check.That(!value.UseOvercharge);Check.Equal(CruiseOverchargeReason.Heat,value.Reason);}
[Test]public void EveryShipUsesItsActualBurnAndRouteReserve()
{
Check.That(Decide(Observation with{FuelCapacityTonnes=128,FuelMainTonnes=128,FuelRateUpperTonnesPerSecond=12},reserve:70).UseOvercharge);
Check.That(!Decide(Observation with{FuelMainTonnes=14,FuelBurnTonnesPerSecond=4}).UseOvercharge);
Check.That(!Decide(Observation with{FuelMainTonnes=12,FuelRateUpperTonnesPerSecond=2}).UseOvercharge);
}
[Test]public void MissingFuelBoundAndUnknownComponentModelCannotEnable()
{Check.That(!Decide(Observation with{FuelRateUpperTonnesPerSecond=double.NaN}).UseOvercharge);Check.That(!Decide(Observation with{ThermalInput=null}).UseOvercharge);}
[Test]public void FirstUnknownBurnWaitsForTelemetryWithoutInventingAFuelShortageOrCooldown()
{
var policy=new CruiseOverchargePolicy();
var first=policy.Decide(Flight,Observation with{FuelBurnTonnesPerSecond=double.NaN},8,Now);
Check.That(!first.UseOvercharge);Check.Equal(CruiseOverchargeReason.Telemetry,first.Reason);
var later=Now.AddMilliseconds(100);
var ready=policy.Decide(Flight with{ObservedAt=later},Observation with{ObservedAt=later,FuelBurnTonnesPerSecond=.1},8,later);
Check.That(ready.UseOvercharge);Check.Equal(CruiseOverchargeReason.Ready,ready.Reason);
Check.Equal(CruiseOverchargeReason.Fuel,Decide(Observation with{FuelMainTonnes=9,FuelBurnTonnesPerSecond=.1}).Reason);
}
[Test]public void TotalHeatSlopeDoesNotReplaceTheSourceAndRadiatorModel()
{
Check.That(Decide(Observation with{HeatPercent=65,HeatRisePercentPerSecond=50}).UseOvercharge);
Check.That(Decide(Observation with{HeatPercent=65,HeatRisePercentPerSecond=double.NaN}).UseOvercharge);
Check.That(!Decide(Observation with{ThermalInput=Thermal with{OtherInputUpper=double.NaN}}).UseOvercharge);
}
[Test]public void StaleFutureAndWrongShipObservationsCannotEnable()
{
Check.That(!Decide(Observation with{ObservedAt=Now.AddMilliseconds(-251)}).UseOvercharge);
Check.That(!Decide(Observation with{ObservedAt=Now.AddMilliseconds(1)}).UseOvercharge);
Check.That(!Decide(Observation with{ShipEntity=9}).UseOvercharge);
Check.That(!Decide(Observation with{ReferenceWorld=9}).UseOvercharge);
Check.That(!Decide(Observation with{GameSha256=new('b',64)}).UseOvercharge);
}
[Test]public void NativeAssistInterdictionAndPlayerOverrideRequestBoostOff()
{
Check.That(!Decide(flight:Flight with{SupercruiseAssist=NativeAssistState.Engaged}).UseOvercharge);
Check.That(!Decide(flight:Flight with{ExternalThreat=true}).UseOvercharge);
Check.That(!Decide(flight:Flight with{PlayerOverride=true}).UseOvercharge);
}
[Test]public void BoostStopsBeforeApproachAndWhenCourseIsLost()
{
Check.That(!Decide(Observation with{State=CruiseOverchargeState.Active},Flight with{TargetDistance=1e10,TargetPosition=new(0,0,1e10),Speed=1e9}).UseOvercharge);
Check.That(!Decide(Observation with{State=CruiseOverchargeState.Active},Flight with{Forward=new(1,0,0)}).UseOvercharge);
Check.That(!Decide(flight:Flight with{TargetObscured=true}).UseOvercharge);
Check.That(!Decide(flight:Flight with{CollisionCourse=true}).UseOvercharge);
Check.That(!Decide(flight:Flight with{TargetPosition=new(0,0,1e7)}).UseOvercharge);
}
[Test]public void NormalSpaceAndOrbitalCruiseCannotEnableSCO()
{Check.That(!Decide(flight:Flight with{Mode=LocalFlightMode.Normal}).UseOvercharge);Check.That(!Decide(flight:Flight with{Mode=LocalFlightMode.OrbitalCruise}).UseOvercharge);}
[Test]public void AcknowledgementCannotReplaceObservedActivation()
{
var policy=new CruiseOverchargePolicy();Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
var later=Now.AddSeconds(1.1);var value=policy.Decide(Flight with{ObservedAt=later},Observation with{ObservedAt=later},8,later);
Check.That(!value.UseOvercharge);Check.Equal(CruiseOverchargeReason.AwaitingNative,value.Reason);
}
[Test]public void HealthyAlignedDistantShipCanRenewContinuouslyForTwentySeconds()
{
var policy=new CruiseOverchargePolicy();Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
for(var tick=1;tick<=200;tick++)
{
var seconds=tick*.1;var at=Now.AddSeconds(seconds);var distance=Flight.TargetDistance-Flight.Speed*seconds;
var flight=Flight with{ObservedAt=at,Position=new(0,0,Flight.Speed*seconds),TargetDistance=distance};
var observed=Observation with{ObservedAt=at,State=tick==1?CruiseOverchargeState.Starting:CruiseOverchargeState.Active,
FuelMainTonnes=32-seconds*.1,FuelBurnTonnesPerSecond=.1};
var value=policy.Decide(flight,observed,8,at);
Check.That(value.UseOvercharge,$"Unexpected SCO stop after {seconds:F1}s: {value.Reason}");
}
}
[Test]public void ContinuousRenewalStillStopsForHeatFuelApproachAndNativeRefusal()
{
foreach(var trigger in new[]{CruiseOverchargeReason.Heat,CruiseOverchargeReason.Fuel,CruiseOverchargeReason.TargetClose,CruiseOverchargeReason.Unavailable})
{
var policy=new CruiseOverchargePolicy();
for(var second=0;second<=20;second++)
{
var at=Now.AddSeconds(second);
Check.That(policy.Decide(Flight with{ObservedAt=at},Observation with{ObservedAt=at,State=CruiseOverchargeState.Active},8,at).UseOvercharge);
}
var stoppedAt=Now.AddSeconds(20.1);var flight=Flight with{ObservedAt=stoppedAt};
var observed=Observation with{ObservedAt=stoppedAt,State=CruiseOverchargeState.Active};
if(trigger==CruiseOverchargeReason.Heat)observed=observed with{HeatPercent=99};
if(trigger==CruiseOverchargeReason.Fuel)observed=observed with{FuelMainTonnes=9};
if(trigger==CruiseOverchargeReason.TargetClose)flight=flight with{TargetDistance=3e10,TargetPosition=new(0,0,3e10)};
if(trigger==CruiseOverchargeReason.Unavailable)observed=observed with{OriginalCanEngage=false};
var value=policy.Decide(flight,observed,8,stoppedAt);
Check.That(!value.UseOvercharge);Check.Equal(trigger,value.Reason);
var retry=stoppedAt.AddMilliseconds(100);
Check.That(!policy.Decide(Flight with{ObservedAt=retry},Observation with{ObservedAt=retry},8,retry).UseOvercharge);
}
}
[Test]public void ActivationTimeoutUsesTheNewRequestAfterAnObservedActiveInterval()
{
var policy=new CruiseOverchargePolicy();Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
var activeAt=Now.AddSeconds(20);
Check.That(policy.Decide(Flight with{ObservedAt=activeAt},Observation with{ObservedAt=activeAt,State=CruiseOverchargeState.Active},8,activeAt).UseOvercharge);
var retryAt=activeAt.AddMilliseconds(100);
Check.That(policy.Decide(Flight with{ObservedAt=retryAt},Observation with{ObservedAt=retryAt},8,retryAt).UseOvercharge);
var timeoutAt=retryAt.AddSeconds(1.1);
var value=policy.Decide(Flight with{ObservedAt=timeoutAt},Observation with{ObservedAt=timeoutAt},8,timeoutAt);
Check.That(!value.UseOvercharge);Check.Equal(CruiseOverchargeReason.AwaitingNative,value.Reason);
}
[Test]public void HeatRecoveryUsesTheNewSourceBudgetAndFreshOriginalReadiness()
{
var policy=new CruiseOverchargePolicy();Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
Check.That(!policy.Decide(Flight,Observation with{HeatPercent=99},8,Now).UseOvercharge);
var later=Now.AddSeconds(2);Check.That(!policy.Decide(Flight with{ObservedAt=later},Observation with{ObservedAt=later,HeatPercent=90,OriginalCanEngage=false},8,later).UseOvercharge);
later=later.AddSeconds(1);Check.That(policy.Decide(Flight with{ObservedAt=later},Observation with{ObservedAt=later,HeatPercent=90},8,later).UseOvercharge);
}
[Test]public void InvalidNativeFuelAndEngineStateFailClosed()
{
Check.That(!Decide(Observation with{FuelMainTonnes=-1}).UseOvercharge);Check.That(!Decide(Observation with{FuelMainTonnes=33}).UseOvercharge);
Check.That(!Decide(Observation with{State=CruiseOverchargeState.Cooling}).UseOvercharge);Check.That(!Decide(Observation with{OriginalCanEngage=false}).UseOvercharge);
Check.That(!Decide(Observation with{State=(CruiseOverchargeState)99}).UseOvercharge);
Check.That(!Decide(Observation with{State=CruiseOverchargeState.Active,OriginalCanEngage=false}).UseOvercharge);
Check.That(!Decide(Observation with{OriginalInterfacesVerified=false}).UseOvercharge);Check.That(!Decide(reserve:0).UseOvercharge);
}
}
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class CruiseOverchargePolicyTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.FromUnixTimeMilliseconds(100000);
private static readonly string Hash=new('a',64);
private static ScoThermalInput Thermal=>new(){Verified=true,HeatCapacity=100,HeatDisplayScale=1,ScoInputUpper=10,OtherInputUpper=5,
CoolingPassive=10,CoolingMaximum=10,CoolingScale=1,CoolingHeatLower=0,CoolingHeatUpper=1,LeaseRemainingSeconds=.25,ResponseDelaySeconds=.01,AfterStopSeconds=.5};
private static SystemFlightFrame Flight=>new(){ObservedAt=Now,InGame=true,GeometryVerified=true,GameSha256=Hash,ShipEntity=7,ReferenceWorld=8,SystemAddress=1,
Mode=LocalFlightMode.Supercruise,Position=new(0,0,0),TargetPosition=new(0,0,1e12),Forward=new(0,0,1),TargetDistance=1e12,Speed=1e9,
SelectedTarget=new(1,9,"Test Station",SystemTargetKind.Station,10),SupercruiseAssist=NativeAssistState.Unavailable};
private static CruiseOverchargeObservation Observation=>new(){ObservedAt=Now,ShipEntity=7,ReferenceWorld=8,GameSha256=Hash,
OriginalInterfacesVerified=true,OriginalCanEngage=true,NativeFlightConstraintsVerified=true,NativeGeometryVerified=true,State=CruiseOverchargeState.Idle,HeatPercent=40,HeatRisePercentPerSecond=0,
FuelMainTonnes=32,FuelCapacityTonnes=32,FuelBurnTonnesPerSecond=0,FuelRateUpperTonnesPerSecond=1,ThermalInput=Thermal};
private static CruiseOverchargeDecision Decide(CruiseOverchargeObservation? sample=null,SystemFlightFrame? flight=null,double reserve=8)
=>new CruiseOverchargePolicy().Decide(flight??Flight,sample??Observation,reserve,Now);
[Test]public void VerifiedCoolAlignedDistantShipCanRequestBoost()=>Check.That(Decide().UseOvercharge);
[Test]public void NativeScoConstraintsStopOnlyBoostAndCanRecoverWithoutChangingThePrimaryFrame()
{
foreach(var blocked in new[]{Observation with{NativeCollisionCourse=true},Observation with{NativeGeometryVerified=false},Observation with{NativeAssistEnabled=true},Observation with{NativeFlightConstraintsVerified=false}})
Check.That(!Decide(blocked).UseOvercharge);
var policy=new CruiseOverchargePolicy();Check.Equal(CruiseOverchargeReason.Geometry,policy.Decide(Flight,Observation with{NativeCollisionCourse=true},8,Now).Reason);
Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
}
[Test]public void TheObserved785LightSecondCaseContinuesWithTheVerifiedOriginalApproachWindow()
{
const double ls=299792458;var distance=784.9268650541314*ls;
var flight=Flight with{TargetDistance=distance,TargetPosition=new(0,0,distance),Speed=26.417687132075883*ls,CruiseParametersVerified=true,CruiseApproachSeconds=6};
Check.That(Decide(Observation with{State=CruiseOverchargeState.Active,HeatPercent=29.8585},flight).UseOvercharge);
Check.Equal(CruiseOverchargeReason.TargetClose,Decide(Observation with{State=CruiseOverchargeState.Active},flight with{CruiseParametersVerified=false}).Reason);
Check.Equal(CruiseOverchargeReason.TargetClose,Decide(Observation with{State=CruiseOverchargeState.Active},flight with{TargetDistance=100*ls,TargetPosition=new(0,0,100*ls)}).Reason);
}
[Test]public void FinalApproachDoesNotReigniteWhenScoStopsAndTheDisplayedEtaGrowsAgain()
{
const double ls=299792458;var policy=new CruiseOverchargePolicy();var flight=Flight with{CruiseParametersVerified=true,CruiseApproachSeconds=6,TargetDistance=900*ls,TargetPosition=new(0,0,900*ls),Speed=20*ls};
Check.That(policy.Decide(flight,Observation,8,Now).UseOvercharge);
var at=Now.AddSeconds(1);flight=flight with{ObservedAt=at,TargetDistance=300*ls,TargetPosition=new(0,0,300*ls),Speed=30*ls};
Check.Equal(CruiseOverchargeReason.TargetClose,policy.Decide(flight,Observation with{ObservedAt=at,State=CruiseOverchargeState.Active},8,at).Reason);
at=at.AddSeconds(2);flight=flight with{ObservedAt=at,Speed=ls};
Check.Equal(CruiseOverchargeReason.TargetClose,policy.Decide(flight,Observation with{ObservedAt=at},8,at).Reason);
flight=flight with{SelectedTarget=new(1,10,"Other station",SystemTargetKind.Station,11)};
Check.That(policy.Decide(flight,Observation with{ObservedAt=at},8,at).UseOvercharge);
}
[Test]public void AnOwnedNativeApproachStopAlsoPreventsASecondShortBurst()
{
var policy=new CruiseOverchargePolicy();policy.ObserveNativeApproachStop(Flight);
Check.Equal(CruiseOverchargeReason.TargetClose,policy.Decide(Flight,Observation,8,Now).Reason);
Check.That(policy.Decide(Flight with{ShipEntity=8},Observation with{ShipEntity=8},8,Now).UseOvercharge);
}
[Test]public void RisingHeatStopsBeforeTheTemperatureCeiling()
{var value=Decide(Observation with{HeatPercent=99,HeatRisePercentPerSecond=-5});Check.That(!value.UseOvercharge);Check.Equal(CruiseOverchargeReason.Heat,value.Reason);}
[Test]public void EveryShipUsesItsActualBurnAndRouteReserve()
{
Check.That(Decide(Observation with{FuelCapacityTonnes=128,FuelMainTonnes=128,FuelRateUpperTonnesPerSecond=12},reserve:70).UseOvercharge);
Check.That(!Decide(Observation with{FuelMainTonnes=14,FuelBurnTonnesPerSecond=4}).UseOvercharge);
Check.That(!Decide(Observation with{FuelMainTonnes=12,FuelRateUpperTonnesPerSecond=2}).UseOvercharge);
}
[Test]public void MissingFuelBoundAndUnknownComponentModelCannotEnable()
{Check.That(!Decide(Observation with{FuelRateUpperTonnesPerSecond=double.NaN}).UseOvercharge);Check.That(!Decide(Observation with{ThermalInput=null}).UseOvercharge);}
[Test]public void FirstUnknownBurnWaitsForTelemetryWithoutInventingAFuelShortageOrCooldown()
{
var policy=new CruiseOverchargePolicy();
var first=policy.Decide(Flight,Observation with{FuelBurnTonnesPerSecond=double.NaN},8,Now);
Check.That(!first.UseOvercharge);Check.Equal(CruiseOverchargeReason.Telemetry,first.Reason);
var later=Now.AddMilliseconds(100);
var ready=policy.Decide(Flight with{ObservedAt=later},Observation with{ObservedAt=later,FuelBurnTonnesPerSecond=.1},8,later);
Check.That(ready.UseOvercharge);Check.Equal(CruiseOverchargeReason.Ready,ready.Reason);
Check.Equal(CruiseOverchargeReason.Fuel,Decide(Observation with{FuelMainTonnes=9,FuelBurnTonnesPerSecond=.1}).Reason);
}
[Test]public void TotalHeatSlopeDoesNotReplaceTheSourceAndRadiatorModel()
{
Check.That(Decide(Observation with{HeatPercent=65,HeatRisePercentPerSecond=50}).UseOvercharge);
Check.That(Decide(Observation with{HeatPercent=65,HeatRisePercentPerSecond=double.NaN}).UseOvercharge);
Check.That(!Decide(Observation with{ThermalInput=Thermal with{OtherInputUpper=double.NaN}}).UseOvercharge);
}
[Test]public void StaleFutureAndWrongShipObservationsCannotEnable()
{
Check.That(!Decide(Observation with{ObservedAt=Now.AddMilliseconds(-251)}).UseOvercharge);
Check.That(!Decide(Observation with{ObservedAt=Now.AddMilliseconds(1)}).UseOvercharge);
Check.That(!Decide(Observation with{ShipEntity=9}).UseOvercharge);
Check.That(!Decide(Observation with{ReferenceWorld=9}).UseOvercharge);
Check.That(!Decide(Observation with{GameSha256=new('b',64)}).UseOvercharge);
}
[Test]public void NativeAssistInterdictionAndPlayerOverrideRequestBoostOff()
{
Check.That(!Decide(flight:Flight with{SupercruiseAssist=NativeAssistState.Engaged}).UseOvercharge);
Check.That(!Decide(flight:Flight with{ExternalThreat=true}).UseOvercharge);
Check.That(!Decide(flight:Flight with{PlayerOverride=true}).UseOvercharge);
}
[Test]public void BoostStopsBeforeApproachAndWhenCourseIsLost()
{
Check.That(!Decide(Observation with{State=CruiseOverchargeState.Active},Flight with{TargetDistance=1e10,TargetPosition=new(0,0,1e10),Speed=1e9}).UseOvercharge);
Check.That(!Decide(Observation with{State=CruiseOverchargeState.Active},Flight with{Forward=new(1,0,0)}).UseOvercharge);
Check.That(!Decide(flight:Flight with{TargetObscured=true}).UseOvercharge);
Check.That(!Decide(flight:Flight with{CollisionCourse=true}).UseOvercharge);
Check.That(!Decide(flight:Flight with{TargetPosition=new(0,0,1e7)}).UseOvercharge);
}
[Test]public void NormalSpaceAndOrbitalCruiseCannotEnableSCO()
{Check.That(!Decide(flight:Flight with{Mode=LocalFlightMode.Normal}).UseOvercharge);Check.That(!Decide(flight:Flight with{Mode=LocalFlightMode.OrbitalCruise}).UseOvercharge);}
[Test]public void AcknowledgementCannotReplaceObservedActivation()
{
var policy=new CruiseOverchargePolicy();Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
var later=Now.AddSeconds(1.1);var value=policy.Decide(Flight with{ObservedAt=later},Observation with{ObservedAt=later},8,later);
Check.That(!value.UseOvercharge);Check.Equal(CruiseOverchargeReason.AwaitingNative,value.Reason);
}
[Test]public void HealthyAlignedDistantShipCanRenewContinuouslyForTwentySeconds()
{
var policy=new CruiseOverchargePolicy();Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
for(var tick=1;tick<=200;tick++)
{
var seconds=tick*.1;var at=Now.AddSeconds(seconds);var distance=Flight.TargetDistance-Flight.Speed*seconds;
var flight=Flight with{ObservedAt=at,Position=new(0,0,Flight.Speed*seconds),TargetDistance=distance};
var observed=Observation with{ObservedAt=at,State=tick==1?CruiseOverchargeState.Starting:CruiseOverchargeState.Active,
FuelMainTonnes=32-seconds*.1,FuelBurnTonnesPerSecond=.1};
var value=policy.Decide(flight,observed,8,at);
Check.That(value.UseOvercharge,$"Unexpected SCO stop after {seconds:F1}s: {value.Reason}");
}
}
[Test]public void ContinuousRenewalStillStopsForHeatFuelApproachAndNativeRefusal()
{
foreach(var trigger in new[]{CruiseOverchargeReason.Heat,CruiseOverchargeReason.Fuel,CruiseOverchargeReason.TargetClose,CruiseOverchargeReason.Unavailable})
{
var policy=new CruiseOverchargePolicy();
for(var second=0;second<=20;second++)
{
var at=Now.AddSeconds(second);
Check.That(policy.Decide(Flight with{ObservedAt=at},Observation with{ObservedAt=at,State=CruiseOverchargeState.Active},8,at).UseOvercharge);
}
var stoppedAt=Now.AddSeconds(20.1);var flight=Flight with{ObservedAt=stoppedAt};
var observed=Observation with{ObservedAt=stoppedAt,State=CruiseOverchargeState.Active};
if(trigger==CruiseOverchargeReason.Heat)observed=observed with{HeatPercent=99};
if(trigger==CruiseOverchargeReason.Fuel)observed=observed with{FuelMainTonnes=9};
if(trigger==CruiseOverchargeReason.TargetClose)flight=flight with{TargetDistance=3e10,TargetPosition=new(0,0,3e10)};
if(trigger==CruiseOverchargeReason.Unavailable)observed=observed with{OriginalCanEngage=false};
var value=policy.Decide(flight,observed,8,stoppedAt);
Check.That(!value.UseOvercharge);Check.Equal(trigger,value.Reason);
var retry=stoppedAt.AddMilliseconds(100);
Check.That(!policy.Decide(Flight with{ObservedAt=retry},Observation with{ObservedAt=retry},8,retry).UseOvercharge);
}
}
[Test]public void ActivationTimeoutUsesTheNewRequestAfterAnObservedActiveInterval()
{
var policy=new CruiseOverchargePolicy();Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
var activeAt=Now.AddSeconds(20);
Check.That(policy.Decide(Flight with{ObservedAt=activeAt},Observation with{ObservedAt=activeAt,State=CruiseOverchargeState.Active},8,activeAt).UseOvercharge);
var retryAt=activeAt.AddMilliseconds(100);
Check.That(policy.Decide(Flight with{ObservedAt=retryAt},Observation with{ObservedAt=retryAt},8,retryAt).UseOvercharge);
var timeoutAt=retryAt.AddSeconds(1.1);
var value=policy.Decide(Flight with{ObservedAt=timeoutAt},Observation with{ObservedAt=timeoutAt},8,timeoutAt);
Check.That(!value.UseOvercharge);Check.Equal(CruiseOverchargeReason.AwaitingNative,value.Reason);
}
[Test]public void HeatRecoveryUsesTheNewSourceBudgetAndFreshOriginalReadiness()
{
var policy=new CruiseOverchargePolicy();Check.That(policy.Decide(Flight,Observation,8,Now).UseOvercharge);
Check.That(!policy.Decide(Flight,Observation with{HeatPercent=99},8,Now).UseOvercharge);
var later=Now.AddSeconds(2);Check.That(!policy.Decide(Flight with{ObservedAt=later},Observation with{ObservedAt=later,HeatPercent=90,OriginalCanEngage=false},8,later).UseOvercharge);
later=later.AddSeconds(1);Check.That(policy.Decide(Flight with{ObservedAt=later},Observation with{ObservedAt=later,HeatPercent=90},8,later).UseOvercharge);
}
[Test]public void InvalidNativeFuelAndEngineStateFailClosed()
{
Check.That(!Decide(Observation with{FuelMainTonnes=-1}).UseOvercharge);Check.That(!Decide(Observation with{FuelMainTonnes=33}).UseOvercharge);
Check.That(!Decide(Observation with{State=CruiseOverchargeState.Cooling}).UseOvercharge);Check.That(!Decide(Observation with{OriginalCanEngage=false}).UseOvercharge);
Check.That(!Decide(Observation with{State=(CruiseOverchargeState)99}).UseOvercharge);
Check.That(!Decide(Observation with{State=CruiseOverchargeState.Active,OriginalCanEngage=false}).UseOvercharge);
Check.That(!Decide(Observation with{OriginalInterfacesVerified=false}).UseOvercharge);Check.That(!Decide(reserve:0).UseOvercharge);
}
}