using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class CruiseOverchargeSampleHistoryTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.FromUnixTimeMilliseconds(1700000000000);
private static readonly string Hash=new('a',64);
private static readonly SystemFlightFrame Flight=new(){ShipEntity=12,ReferenceWorld=24,GameSha256=Hash};
private static JsonElement Sample(DateTimeOffset at,double heat=40,double reserve=.4,ulong entity=12,ulong world=24,int rawFsd=2,bool bound=true,bool ready=true,int state=0,bool canEngage=true)
=>JsonSerializer.SerializeToElement(new{enabled=true,sampleReady=ready,gameSha256=Hash,sample=new{verified=true,entityId=entity.ToString(),referenceWorldAddress=world.ToString(),
observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),inSupercruise=true,stableFsd=true,rawFsdState=rawFsd,state,nativeCanEngage=canEngage,heatPercent=heat,
geometryVerified=true,collisionCourse=false,nativeAssistEnabled=false,
fuelVerified=true,fuelMainTonnes=32,fuelCapacityTonnes=32,reserveFuelTonnes=reserve,fuelBoundVerified=bound,fuelRateUpperTonnesPerSecond=1}});
[Test]public void FirstSampleCannotInventHeatOrFuelSlopes()
{var value=new CruiseOverchargeSampleHistory(Hash).Read(Sample(Now),Flight,Now);Check.That(value.OriginalInterfacesVerified&&double.IsNaN(value.HeatRisePercentPerSecond)&&double.IsNaN(value.FuelBurnTonnesPerSecond));}
[Test]public void NativeCollisionAndAssistFactsSurviveProjectionInsteadOfBeingReplacedByThePrimaryDefaults()
{
var node=JsonNode.Parse(Sample(Now).GetRawText())!;node["sample"]!["collisionCourse"]=true;node["sample"]!["nativeAssistEnabled"]=true;
var projected=new CruiseOverchargeSampleHistory(Hash).Read(JsonSerializer.SerializeToElement(node),Flight,Now);
Check.That(projected.NativeFlightConstraintsVerified&&projected.NativeGeometryVerified&&projected.NativeCollisionCourse&&projected.NativeAssistEnabled);
node["sample"]!.AsObject().Remove("geometryVerified");
Check.That(!new CruiseOverchargeSampleHistory(Hash).Read(JsonSerializer.SerializeToElement(node),Flight,Now).NativeFlightConstraintsVerified);
}
[Test]public void TwoSamplesUseBothTheMainAndReservoirFuel()
{
var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now),Flight,Now);var later=Now.AddMilliseconds(100);
var value=history.Read(Sample(later,42,.3),Flight,later);Check.That(Math.Abs(value.HeatRisePercentPerSecond-20)<1e-6&&Math.Abs(value.FuelBurnTonnesPerSecond-1)<1e-6);
}
[Test]public void DuplicateBackwardStaleAndFutureFramesCannotEnable()
{
var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now),Flight,Now);
foreach(var at in new[]{Now,Now.AddMilliseconds(-1),Now.AddMilliseconds(-251),Now.AddMilliseconds(1)})
Check.That(!history.Read(Sample(at),Flight,Now).OriginalInterfacesVerified);
}
[Test]public void OwnedFreshExactDuplicateKeepsItsOriginalTimeAndMeasuredSlopes()
{
var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now,state:1),Flight,Now);
var at=Now.AddMilliseconds(100);var sample=Sample(at,42,.3,state:2);
var previous=history.Read(sample,Flight,at);
var repeated=history.Read(sample,Flight,at.AddMilliseconds(80),allowRepeated:true);
Check.Equal(previous,repeated);Check.Equal(at,repeated.ObservedAt);
Check.That(double.IsFinite(repeated.HeatRisePercentPerSecond)&&double.IsFinite(repeated.FuelBurnTonnesPerSecond));
Check.That(!history.Read(sample,Flight,at.AddMilliseconds(80)).OriginalInterfacesVerified);
Check.That(!history.Read(sample,Flight,at.AddMilliseconds(251),allowRepeated:true).OriginalInterfacesVerified);
}
[Test]public void OwnedDuplicatePermissionCannotAcceptChangedBackwardFutureOrWrongIdentitySamples()
{
var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now,state:1),Flight,Now);
var at=Now.AddMilliseconds(100);history.Read(Sample(at,42,.3,state:2),Flight,at);
foreach(var sample in new[]{Sample(at,43,.3,state:2),Sample(at,42,.2,state:2),Sample(at,42,.3,state:1),Sample(at,42,.3,state:2,canEngage:false),
Sample(at.AddMilliseconds(-1),42,.3,state:2),Sample(at.AddMilliseconds(81),42,.3,state:2),Sample(at,42,.3,entity:13,state:2),Sample(at,42,.3,world:25,state:2)})
Check.That(!history.Read(sample,Flight,at.AddMilliseconds(80),allowRepeated:true).OriginalInterfacesVerified);
}
[Test]public void SceneAndShipChangesRequireNewSlopes()
{
var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now),Flight,Now);var later=Now.AddMilliseconds(100);
Check.That(!history.Read(Sample(later,entity:13),Flight,later).OriginalInterfacesVerified);
var value=history.Read(Sample(later,entity:13,world:25),Flight with{ShipEntity=13,ReferenceWorld=25},later);
Check.That(value.OriginalInterfacesVerified&&double.IsNaN(value.HeatRisePercentPerSecond));
}
[Test]public void UnknownFuelBoundWrongFsdAndNoSampleRemainUnusable()
{
var history=new CruiseOverchargeSampleHistory(Hash);Check.That(double.IsNaN(history.Read(Sample(Now,bound:false),Flight,Now).FuelRateUpperTonnesPerSecond));
Check.That(!history.Read(Sample(Now,rawFsd:3),Flight,Now).OriginalInterfacesVerified);
Check.That(!history.Read(Sample(Now,ready:false),Flight,Now).OriginalInterfacesVerified);
}
private static JsonElement WithThermal(ulong entity=12,ulong world=24,long sourceAgeMilliseconds=100,long heatAgeMilliseconds=0)
{
var value=JsonNode.Parse(Sample(Now).GetRawText())!;
value["sample"]!["thermal"]=JsonSerializer.SerializeToNode(new
{
verified=true,entityId=entity.ToString(),referenceWorldAddress=world.ToString(),
sourceObservedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds()-sourceAgeMilliseconds,
heatObservedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds()-heatAgeMilliseconds,
heatCapacity=100,heatDisplayScale=1.5,scoInputUpper=30,otherInputUpper=3,
coolingPassive=5,coolingMaximum=20,coolingScale=1,coolingHeatLower=.2,coolingHeatUpper=1,
responseDelaySeconds=.02
});
return JsonSerializer.SerializeToElement(value);
}
[Test]public void NativeSourceAgeDoesNotBecomeTheAgeOfFreshlyObservedHeat()
{
var observed=new CruiseOverchargeSampleHistory(Hash).Read(WithThermal(),Flight,Now);
var thermal=observed.ThermalInput??throw new InvalidOperationException("Native source model was lost.");
Check.That(thermal.Verified);Check.Equal(.1,thermal.SourceAgeSeconds);Check.Equal(0d,thermal.HeatAgeSeconds);
var forecast=ScoThermalPredictor.Predict(thermal);
Check.That(forecast.Known&&forecast.ScoOnNetRisePercentPerSecond>0&&forecast.ScoOffNetRisePercentPerSecond<0);
}
[Test]public void ThermalSourcesFromAnotherShipOrReferenceWorldCannotAuthorizeSCO()
{
foreach(var sample in new[]{WithThermal(entity:13),WithThermal(world:25)})
{
var thermal=new CruiseOverchargeSampleHistory(Hash).Read(sample,Flight,Now).ThermalInput!;
Check.That(!thermal.Verified&&!ScoThermalPredictor.Predict(thermal).Known);
}
}
[Test]public void FutureOrExpiredNativeSourcesCannotAuthorizeSCOFromAFreshFlightFrame()
{
foreach(var age in new long[]{-1,201})
{
var thermal=new CruiseOverchargeSampleHistory(Hash).Read(WithThermal(sourceAgeMilliseconds:age),Flight,Now).ThermalInput!;
Check.That(!ScoThermalPredictor.Predict(thermal).Known);
}
}
[Test]public void IndependentHeatTimestampSurvivesTheNewerControlSampleTimestamp()
{
var thermal=new CruiseOverchargeSampleHistory(Hash).Read(WithThermal(heatAgeMilliseconds:1),Flight,Now).ThermalInput!;
Check.Equal(.001,thermal.HeatAgeSeconds);Check.Equal(.1,thermal.SourceAgeSeconds);
Check.That(ScoThermalPredictor.Predict(thermal).Known);
}
[Test]public void UnknownFutureOrExpiredHeatCannotBeRenewedByAFreshControlSample()
{
foreach(var age in new long[]{-1,201})
{
var thermal=new CruiseOverchargeSampleHistory(Hash).Read(WithThermal(heatAgeMilliseconds:age),Flight,Now).ThermalInput!;
Check.That(!ScoThermalPredictor.Predict(thermal).Known);
}
foreach(var unknown in new JsonNode?[]{null,JsonValue.Create("unknown"),JsonValue.Create(0L)})
{
var node=JsonNode.Parse(WithThermal().GetRawText())!;node["sample"]!["thermal"]!["heatObservedAtUnixMilliseconds"]=unknown;
var thermal=new CruiseOverchargeSampleHistory(Hash).Read(JsonSerializer.SerializeToElement(node),Flight,Now).ThermalInput!;
Check.That(!ScoThermalPredictor.Predict(thermal).Known);
}
var missing=JsonNode.Parse(WithThermal().GetRawText())!;missing["sample"]!["thermal"]!.AsObject().Remove("heatObservedAtUnixMilliseconds");
Check.That(!ScoThermalPredictor.Predict(new CruiseOverchargeSampleHistory(Hash).Read(JsonSerializer.SerializeToElement(missing),Flight,Now).ThermalInput!).Known);
}
}
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class CruiseOverchargeSampleHistoryTests
{
private static readonly DateTimeOffset Now=DateTimeOffset.FromUnixTimeMilliseconds(1700000000000);
private static readonly string Hash=new('a',64);
private static readonly SystemFlightFrame Flight=new(){ShipEntity=12,ReferenceWorld=24,GameSha256=Hash};
private static JsonElement Sample(DateTimeOffset at,double heat=40,double reserve=.4,ulong entity=12,ulong world=24,int rawFsd=2,bool bound=true,bool ready=true,int state=0,bool canEngage=true)
=>JsonSerializer.SerializeToElement(new{enabled=true,sampleReady=ready,gameSha256=Hash,sample=new{verified=true,entityId=entity.ToString(),referenceWorldAddress=world.ToString(),
observedAtUnixMilliseconds=at.ToUnixTimeMilliseconds(),inSupercruise=true,stableFsd=true,rawFsdState=rawFsd,state,nativeCanEngage=canEngage,heatPercent=heat,
geometryVerified=true,collisionCourse=false,nativeAssistEnabled=false,
fuelVerified=true,fuelMainTonnes=32,fuelCapacityTonnes=32,reserveFuelTonnes=reserve,fuelBoundVerified=bound,fuelRateUpperTonnesPerSecond=1}});
[Test]public void FirstSampleCannotInventHeatOrFuelSlopes()
{var value=new CruiseOverchargeSampleHistory(Hash).Read(Sample(Now),Flight,Now);Check.That(value.OriginalInterfacesVerified&&double.IsNaN(value.HeatRisePercentPerSecond)&&double.IsNaN(value.FuelBurnTonnesPerSecond));}
[Test]public void NativeCollisionAndAssistFactsSurviveProjectionInsteadOfBeingReplacedByThePrimaryDefaults()
{
var node=JsonNode.Parse(Sample(Now).GetRawText())!;node["sample"]!["collisionCourse"]=true;node["sample"]!["nativeAssistEnabled"]=true;
var projected=new CruiseOverchargeSampleHistory(Hash).Read(JsonSerializer.SerializeToElement(node),Flight,Now);
Check.That(projected.NativeFlightConstraintsVerified&&projected.NativeGeometryVerified&&projected.NativeCollisionCourse&&projected.NativeAssistEnabled);
node["sample"]!.AsObject().Remove("geometryVerified");
Check.That(!new CruiseOverchargeSampleHistory(Hash).Read(JsonSerializer.SerializeToElement(node),Flight,Now).NativeFlightConstraintsVerified);
}
[Test]public void TwoSamplesUseBothTheMainAndReservoirFuel()
{
var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now),Flight,Now);var later=Now.AddMilliseconds(100);
var value=history.Read(Sample(later,42,.3),Flight,later);Check.That(Math.Abs(value.HeatRisePercentPerSecond-20)<1e-6&&Math.Abs(value.FuelBurnTonnesPerSecond-1)<1e-6);
}
[Test]public void DuplicateBackwardStaleAndFutureFramesCannotEnable()
{
var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now),Flight,Now);
foreach(var at in new[]{Now,Now.AddMilliseconds(-1),Now.AddMilliseconds(-251),Now.AddMilliseconds(1)})
Check.That(!history.Read(Sample(at),Flight,Now).OriginalInterfacesVerified);
}
[Test]public void OwnedFreshExactDuplicateKeepsItsOriginalTimeAndMeasuredSlopes()
{
var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now,state:1),Flight,Now);
var at=Now.AddMilliseconds(100);var sample=Sample(at,42,.3,state:2);
var previous=history.Read(sample,Flight,at);
var repeated=history.Read(sample,Flight,at.AddMilliseconds(80),allowRepeated:true);
Check.Equal(previous,repeated);Check.Equal(at,repeated.ObservedAt);
Check.That(double.IsFinite(repeated.HeatRisePercentPerSecond)&&double.IsFinite(repeated.FuelBurnTonnesPerSecond));
Check.That(!history.Read(sample,Flight,at.AddMilliseconds(80)).OriginalInterfacesVerified);
Check.That(!history.Read(sample,Flight,at.AddMilliseconds(251),allowRepeated:true).OriginalInterfacesVerified);
}
[Test]public void OwnedDuplicatePermissionCannotAcceptChangedBackwardFutureOrWrongIdentitySamples()
{
var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now,state:1),Flight,Now);
var at=Now.AddMilliseconds(100);history.Read(Sample(at,42,.3,state:2),Flight,at);
foreach(var sample in new[]{Sample(at,43,.3,state:2),Sample(at,42,.2,state:2),Sample(at,42,.3,state:1),Sample(at,42,.3,state:2,canEngage:false),
Sample(at.AddMilliseconds(-1),42,.3,state:2),Sample(at.AddMilliseconds(81),42,.3,state:2),Sample(at,42,.3,entity:13,state:2),Sample(at,42,.3,world:25,state:2)})
Check.That(!history.Read(sample,Flight,at.AddMilliseconds(80),allowRepeated:true).OriginalInterfacesVerified);
}
[Test]public void SceneAndShipChangesRequireNewSlopes()
{
var history=new CruiseOverchargeSampleHistory(Hash);history.Read(Sample(Now),Flight,Now);var later=Now.AddMilliseconds(100);
Check.That(!history.Read(Sample(later,entity:13),Flight,later).OriginalInterfacesVerified);
var value=history.Read(Sample(later,entity:13,world:25),Flight with{ShipEntity=13,ReferenceWorld=25},later);
Check.That(value.OriginalInterfacesVerified&&double.IsNaN(value.HeatRisePercentPerSecond));
}
[Test]public void UnknownFuelBoundWrongFsdAndNoSampleRemainUnusable()
{
var history=new CruiseOverchargeSampleHistory(Hash);Check.That(double.IsNaN(history.Read(Sample(Now,bound:false),Flight,Now).FuelRateUpperTonnesPerSecond));
Check.That(!history.Read(Sample(Now,rawFsd:3),Flight,Now).OriginalInterfacesVerified);
Check.That(!history.Read(Sample(Now,ready:false),Flight,Now).OriginalInterfacesVerified);
}
private static JsonElement WithThermal(ulong entity=12,ulong world=24,long sourceAgeMilliseconds=100,long heatAgeMilliseconds=0)
{
var value=JsonNode.Parse(Sample(Now).GetRawText())!;
value["sample"]!["thermal"]=JsonSerializer.SerializeToNode(new
{
verified=true,entityId=entity.ToString(),referenceWorldAddress=world.ToString(),
sourceObservedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds()-sourceAgeMilliseconds,
heatObservedAtUnixMilliseconds=Now.ToUnixTimeMilliseconds()-heatAgeMilliseconds,
heatCapacity=100,heatDisplayScale=1.5,scoInputUpper=30,otherInputUpper=3,
coolingPassive=5,coolingMaximum=20,coolingScale=1,coolingHeatLower=.2,coolingHeatUpper=1,
responseDelaySeconds=.02
});
return JsonSerializer.SerializeToElement(value);
}
[Test]public void NativeSourceAgeDoesNotBecomeTheAgeOfFreshlyObservedHeat()
{
var observed=new CruiseOverchargeSampleHistory(Hash).Read(WithThermal(),Flight,Now);
var thermal=observed.ThermalInput??throw new InvalidOperationException("Native source model was lost.");
Check.That(thermal.Verified);Check.Equal(.1,thermal.SourceAgeSeconds);Check.Equal(0d,thermal.HeatAgeSeconds);
var forecast=ScoThermalPredictor.Predict(thermal);
Check.That(forecast.Known&&forecast.ScoOnNetRisePercentPerSecond>0&&forecast.ScoOffNetRisePercentPerSecond<0);
}
[Test]public void ThermalSourcesFromAnotherShipOrReferenceWorldCannotAuthorizeSCO()
{
foreach(var sample in new[]{WithThermal(entity:13),WithThermal(world:25)})
{
var thermal=new CruiseOverchargeSampleHistory(Hash).Read(sample,Flight,Now).ThermalInput!;
Check.That(!thermal.Verified&&!ScoThermalPredictor.Predict(thermal).Known);
}
}
[Test]public void FutureOrExpiredNativeSourcesCannotAuthorizeSCOFromAFreshFlightFrame()
{
foreach(var age in new long[]{-1,201})
{
var thermal=new CruiseOverchargeSampleHistory(Hash).Read(WithThermal(sourceAgeMilliseconds:age),Flight,Now).ThermalInput!;
Check.That(!ScoThermalPredictor.Predict(thermal).Known);
}
}
[Test]public void IndependentHeatTimestampSurvivesTheNewerControlSampleTimestamp()
{
var thermal=new CruiseOverchargeSampleHistory(Hash).Read(WithThermal(heatAgeMilliseconds:1),Flight,Now).ThermalInput!;
Check.Equal(.001,thermal.HeatAgeSeconds);Check.Equal(.1,thermal.SourceAgeSeconds);
Check.That(ScoThermalPredictor.Predict(thermal).Known);
}
[Test]public void UnknownFutureOrExpiredHeatCannotBeRenewedByAFreshControlSample()
{
foreach(var age in new long[]{-1,201})
{
var thermal=new CruiseOverchargeSampleHistory(Hash).Read(WithThermal(heatAgeMilliseconds:age),Flight,Now).ThermalInput!;
Check.That(!ScoThermalPredictor.Predict(thermal).Known);
}
foreach(var unknown in new JsonNode?[]{null,JsonValue.Create("unknown"),JsonValue.Create(0L)})
{
var node=JsonNode.Parse(WithThermal().GetRawText())!;node["sample"]!["thermal"]!["heatObservedAtUnixMilliseconds"]=unknown;
var thermal=new CruiseOverchargeSampleHistory(Hash).Read(JsonSerializer.SerializeToElement(node),Flight,Now).ThermalInput!;
Check.That(!ScoThermalPredictor.Predict(thermal).Known);
}
var missing=JsonNode.Parse(WithThermal().GetRawText())!;missing["sample"]!["thermal"]!.AsObject().Remove("heatObservedAtUnixMilliseconds");
Check.That(!ScoThermalPredictor.Predict(new CruiseOverchargeSampleHistory(Hash).Read(JsonSerializer.SerializeToElement(missing),Flight,Now).ThermalInput!).Known);
}
}