using System; using System.Collections.Generic; using System.Reflection; using Sandbox.ModAPI.Ingame; using VRage; using VRage.Game.ModAPI.Ingame; using VRage.Game.ModAPI.Ingame.Utilities; using VRageMath; using Xunit; using P=AutoMiningScript.Program; using FC=AutoMiningScript.Program.FlightController; namespace AutoMiningScript.Tests { public class FlightResourceTests { static void Near(Vector3D a,Vector3D b) {Assert.True(Vector3D.Distance(a,b)<1e-6,a+" vs "+b);} [Fact] public void RotationErrorTracksWorldAxesAtArbitraryOrientations() { Near(FC.RotationError(MatrixD.Identity,MatrixD.Identity),Vector3D.Zero); for(int i=0;i<80;i++) { Vector3D axis=Vector3D.Normalize(new Vector3D(Math.Sin(i*.41)+.1,Math.Cos(i*.37)+.2,Math.Sin(i*.29)+.3)); double angle=.01+i*.038;MatrixD current=MatrixD.CreateFromAxisAngle(Vector3D.Normalize(new Vector3D(1,2,3)),.77); Near(FC.RotationError(current,current*MatrixD.CreateFromAxisAngle(axis,angle)),axis*angle); } Near(FC.RotationError(MatrixD.Identity,MatrixD.CreateFromAxisAngle(Vector3D.Up,Math.PI)),Vector3D.Up*Math.PI); } [Fact] public void OffsetAndRotatedConnectorMapsToTheActualContact() { MatrixD local=MatrixD.CreateFromYawPitchRoll(.7,.4,.2),port=MatrixD.CreateFromYawPitchRoll(-.4,.8,-.3); MatrixD reference=FC.ReferenceOrientationForConnector(local.Forward,local.Up,port.Forward,port.Up); Near(Vector3D.TransformNormal(local.Forward,reference),-port.Forward);Near(Vector3D.TransformNormal(local.Up,reference),port.Up); Vector3D contact=new Vector3D(100,20,-50),offset=new Vector3D(2,-3,4); Near(FC.ReferencePositionForPoint(contact,offset,reference)+Vector3D.TransformNormal(offset,reference),contact); } [Fact] public void RotatingMotherUsesPointVelocityAndRejectsDegenerateAxes() { Near(FC.PointVelocity(new Vector3D(1,2,3),new Vector3D(0,1,0),new Vector3D(3,0,0),Vector3D.Zero),new Vector3D(1,2,0)); MatrixD invalid;Assert.False(FC.Frame(Vector3D.Forward,Vector3D.Forward,out invalid));Assert.False(FC.Frame(new Vector3D(double.NaN,0,0),Vector3D.Up,out invalid)); } [Fact] public void WeightedBatteriesAndHydrogenDoNotAveragePercentages() { var p=TestRig.Program();var h=new P.ShipHardware(p);h.Connector=TestRig.Connector(p,true).Value; h.Batteries.Add(TestRig.Battery(p,1,1).Value);h.Batteries.Add(TestRig.Battery(p,0,9).Value); h.UsesHydrogen=true;h.Tanks.Add(new Stub().Set("CubeGrid",p.Me.CubeGrid).Set("IsFunctional",true).Set("Capacity",10f).Set("FilledRatio",1d).Value); h.Tanks.Add(new Stub().Set("CubeGrid",p.Me.CubeGrid).Set("IsFunctional",true).Set("Capacity",90f).Set("FilledRatio",0d).Value); var r=new P.Resources(p,h);r.Update(.5,P.FlightState.Servicing,0,0); Assert.Equal(.1,r.Battery,6);Assert.Equal(.1,r.Hydrogen,6); } [Fact] public void BaseBatteriesAreExcludedEvenWhenInTheCachedList() { var p=TestRig.Program();var h=new P.ShipHardware(p);h.Connector=TestRig.Connector(p,true).Value; h.Batteries.Add(TestRig.Battery(p,.5f,1).Value); h.Batteries.Add(TestRig.Battery(p,1000,1000).Set("CubeGrid",new Stub().Value).Value); var r=new P.Resources(p,h);r.Update(.5,P.FlightState.Servicing,0,0);Assert.Equal(.5,r.Battery,6);Assert.Equal(1,r.CapacityMWh,6); } [Fact] public void UnloadingNeedsTwoCompleteScansAndNeverUsesTime() { var p=TestRig.Program();var h=new P.ShipHardware(p);h.Connector=TestRig.Connector(p,true).Value;h.Batteries.Add(TestRig.Battery(p,1,1).Value); var r=new P.Resources(p,h);r.Update(.5,P.FlightState.Servicing,0,0);Assert.False(r.Empty); p.Now=10000;r.Service(.5);Assert.False(r.Empty); r.Update(.5,P.FlightState.Servicing,0,0);Assert.True(r.Empty); h.Connector=TestRig.Connector(p,false).Value;r.Update(.5,P.FlightState.Transit,0,0);Assert.False(r.Empty); } [Fact] public void ChargeLatchRequiresTargetAndWholeSortieBudget() { var p=TestRig.Program();var h=new P.ShipHardware(p);h.Connector=TestRig.Connector(p,true).Value;var battery=TestRig.Battery(p,.89f,1);h.Batteries.Add(battery.Value); h.InventoryBlocks.Add(Inventory(p,new List(),0)); var r=new P.Resources(p,h);r.NeedsCharge=true;r.Update(.5,P.FlightState.Servicing,0,0);r.Update(.5,P.FlightState.Servicing,0,0); string why;Assert.False(r.ReadyForSortie(10,1,out why));Assert.True(r.NeedsCharge); battery.Set("CurrentStoredPower",.91f);r.Update(.5,P.FlightState.Servicing,0,0);Assert.True(r.ReadyForSortie(10,1,out why));Assert.False(r.NeedsCharge); Assert.False(r.ReadyForSortie(1000000,1000,out why));Assert.Equal(P.L.MinerSortieOutOfRange,why); } [Fact] public void SufficientEnergyCanDepartWithoutWaitingForNinetyPercent() { var p=TestRig.Program();var h=new P.ShipHardware(p);h.Connector=TestRig.Connector(p,true).Value;h.Batteries.Add(TestRig.Battery(p,.6f,1).Value); h.InventoryBlocks.Add(Inventory(p,new List(),0)); var r=new P.Resources(p,h);r.Update(.5,P.FlightState.Servicing,0,0);r.Update(.5,P.FlightState.Servicing,0,0); string why;Assert.False(r.NeedsCharge);Assert.True(r.ReadyForSortie(10,1,out why)); } [Fact] public void DeepDrillingRequiresMoreReturnReserveThanTheEntrance() { var p=TestRig.Program();var h=new P.ShipHardware(p);h.Connector=TestRig.Connector(p,false).Value;h.Batteries.Add(TestRig.Battery(p,1,1).Value); var r=new P.Resources(p,h);r.Update(.5,P.FlightState.Drilling,500,0);double shallow=r.ReturnBudgetMWh;r.Update(.5,P.FlightState.Drilling,500,100);Assert.True(r.ReturnBudgetMWh>shallow); } [Fact] public void EnergyStorageRetainsLatchButRequiresNewEmptyScan() { var p=TestRig.Program();var h=new P.ShipHardware(p);var r=new P.Resources(p,h) {NeedsCharge=true,NeedsHydrogen=true,Empty=true};var ini=new MyIni();r.Save(ini); var restored=new P.Resources(p,h);restored.Load(ini);Assert.True(restored.NeedsCharge);Assert.True(restored.NeedsHydrogen);Assert.False(restored.Empty); } static MyInventoryItem Ore(string type,double amount) { object boxed=default(MyInventoryItem); typeof(MyInventoryItem).GetField("Type").SetValue(boxed,MyItemType.MakeOre(type)); typeof(MyInventoryItem).GetField("Amount").SetValue(boxed,(MyFixedPoint)amount); return (MyInventoryItem)boxed; } static IMyTerminalBlock Inventory(P p,List items,double fill=0.1) { var inventory=new Stub().Set("CurrentVolume",(MyFixedPoint)fill).Set("MaxVolume",(MyFixedPoint)1) .Method("GetItems",a=> {((List)a[0]).AddRange(items);return null;}); return new Stub().Set("CubeGrid",p.Me.CubeGrid).Set("HasInventory",true).Set("InventoryCount",1).Method("GetInventory",a=>inventory.Value).Value; } [Fact] public void StoneAndIceInTransportInventoriesPreventEmptyUntilBothAreGone() { var p=TestRig.Program();var h=new P.ShipHardware(p);h.Connector=TestRig.Connector(p,true).Value;h.Batteries.Add(TestRig.Battery(p,1,1).Value); var drill=new List {Ore("Stone",5)};var connector=new List {Ore("Ice",3)}; h.InventoryBlocks.Add(Inventory(p,drill));h.InventoryBlocks.Add(Inventory(p,connector));var r=new P.Resources(p,h); r.Update(.5,P.FlightState.Servicing,0,0);Assert.Equal(8,r.OreAmount);Assert.False(r.Empty); drill.Clear();r.Update(.5,P.FlightState.Servicing,0,0);Assert.Equal(3,r.OreAmount);Assert.False(r.Empty); connector.Clear();r.Update(.5,P.FlightState.Servicing,0,0);Assert.False(r.Empty);r.Update(.5,P.FlightState.Servicing,0,0);Assert.True(r.Empty); } [Fact] public void InternalTransferDoesNotCreateOreAndInvalidSamplesStayLatched() { var p=TestRig.Program();var h=new P.ShipHardware(p);h.Connector=TestRig.Connector(p,false).Value;h.Batteries.Add(TestRig.Battery(p,1,1).Value);h.SamplingIsolated=true; var drill=new List {Ore("Iron",10)};var cargo=new List();h.InventoryBlocks.Add(Inventory(p,drill));h.InventoryBlocks.Add(Inventory(p,cargo)); var r=new P.Resources(p,h);r.Update(.5,P.FlightState.Drilling,0,0);r.ResetSampleValidation(); drill.Clear();cargo.Add(Ore("Iron",10));r.Update(.5,P.FlightState.Drilling,0,0);Assert.Equal(10,r.Ores["Iron"]);Assert.True(r.SamplingValid); cargo.Clear();cargo.Add(Ore("Iron",5));r.Update(.5,P.FlightState.Drilling,0,0);Assert.False(r.SamplingValid); r.Update(.5,P.FlightState.Drilling,0,0);Assert.False(r.SamplingValid);r.ResetSampleValidation();r.Update(.5,P.FlightState.Drilling,0,0);Assert.True(r.SamplingValid); } [Fact] public void MovingMotherBudgetUsesInterceptTimeAndRejectsUnreachableVelocity() { Assert.Equal(10,P.Data.InterceptTime(new Vector3D(100,0,0),Vector3D.Zero,10),6); Assert.Equal(20,P.Data.InterceptTime(new Vector3D(100,0,0),new Vector3D(5,0,0),10),6); Assert.Equal(-1,P.Data.InterceptTime(new Vector3D(100,0,0),new Vector3D(20,0,0),10)); Assert.Equal(0,P.Data.InterceptTime(Vector3D.Zero,Vector3D.Zero,10)); } sealed class CargoRig { public readonly P Program; public readonly P.ShipHardware Hardware; public readonly P.Resources Resources; public readonly Stub Cargo; public readonly Stub Connector; public readonly Stub Battery; public readonly List Items=new List {Ore("Iron",10)}; public CargoRig(string config="") { Program=TestRig.Program(config);Hardware=new P.ShipHardware(Program); Connector=TestRig.Connector(Program,true);Hardware.Connector=Connector.Value; Battery=TestRig.Battery(Program,1,1);Hardware.Batteries.Add(Battery.Value); Cargo=Store(5,100,Items);Add(Cargo,Program.Me.CubeGrid);Resources=new P.Resources(Program,Hardware); } public static Stub Store(double volume,double capacity,List contents) { return new Stub().Set("CurrentMass",(MyFixedPoint)1).Set("CurrentVolume",(MyFixedPoint)volume).Set("MaxVolume",(MyFixedPoint)capacity) .Method("get_ItemCount",a=>contents.Count).Method("GetItems",a=> {((List)a[0]).AddRange(contents);return null;}); } public void Add(Stub inventory,IMyCubeGrid grid,bool drill=false) { if(drill) Hardware.InventoryBlocks.Add(new Stub().Set("CubeGrid",grid).Set("HasInventory",true).Set("InventoryCount",1).Method("GetInventory",a=>inventory.Value).Value); else Hardware.InventoryBlocks.Add(new Stub().Set("CubeGrid",grid).Set("HasInventory",true).Set("InventoryCount",1).Method("GetInventory",a=>inventory.Value).Value); } public void Scan() {Program.Now+=.5;Resources.Update(.5,P.FlightState.Servicing,0,0);} public bool CanDepart(out string reason) {return Resources.ReadyForSortie(10,1,out reason);} } [Fact] public void DefaultFivePercentDepartureNeedsTwoScansAndDoesNotClaimOreIsEmpty() { var r=new CargoRig();Assert.Equal(.05,r.Resources.DepartureFill,8); r.Scan();Assert.False(r.Resources.DepartureReady);Assert.Equal(P.L.MinerWaitDepartureScans,r.Resources.DepartureWaitReason); r.Scan();Assert.True(r.Resources.DepartureReady);Assert.False(r.Resources.Empty);Assert.Equal(10,r.Resources.OreAmount); string reason;Assert.True(r.CanDepart(out reason)); } [Theory][InlineData(5.0001,false)][InlineData(5,true)][InlineData(4.9999,true)][InlineData(0,true)] public void DepartureVolumeThresholdIncludesItsBoundary(double volume,bool expected) { var r=new CargoRig();r.Cargo.Set("CurrentVolume",(MyFixedPoint)volume);r.Scan();r.Scan();Assert.Equal(expected,r.Resources.DepartureReady); } [Theory][InlineData("NaN")][InlineData("Infinity")][InlineData("-0.01")][InlineData("0.95")][InlineData("1")] public void InvalidDepartureFillIsRejectedWhileConfigurationLoads(string value) {Assert.Throws(()=>new CargoRig("[Cargo]\nDepartureFill="+value));} [Fact] public void DepartureFillMustStayBelowTheConfiguredReturnFill() { Assert.Throws(()=>new CargoRig("[Cargo]\nReturnFill=0.2\nDepartureFill=0.2")); var r=new CargoRig("[Cargo]\nReturnFill=0.2\nDepartureFill=0.1999");Assert.Equal(.1999,r.Resources.DepartureFill,8); } [Fact] public void RisingCargoRevokesPermissionAndNeedsTwoNewScans() { var r=new CargoRig();r.Scan();r.Scan();Assert.True(r.Resources.DepartureReady); r.Cargo.Set("CurrentVolume",(MyFixedPoint)6);r.Scan();Assert.False(r.Resources.DepartureReady); r.Cargo.Set("CurrentVolume",(MyFixedPoint)5);r.Scan();Assert.False(r.Resources.DepartureReady);r.Scan();Assert.True(r.Resources.DepartureReady); } [Fact] public void AllMinerInventoryVolumeCountsIncludingSuppliesButMotherCargoIsExcluded() { var r=new CargoRig();r.Items.Clear(); object component=default(MyInventoryItem);typeof(MyInventoryItem).GetField("Type").SetValue(component,MyItemType.MakeComponent("SteelPlate")); typeof(MyInventoryItem).GetField("Amount").SetValue(component,(MyFixedPoint)1);r.Items.Add((MyInventoryItem)component); var supplies=CargoRig.Store(10,100,new List());r.Add(supplies,r.Program.Me.CubeGrid); r.Add(CargoRig.Store(100000,100000,new List{Ore("Gold",100)}),new Stub().Value); r.Scan();r.Scan();Assert.True(r.Resources.Empty);Assert.False(r.Resources.DepartureReady);Assert.Equal(.075,r.Resources.Cargo,8);Assert.Equal(0,r.Resources.OreAmount); supplies.Set("CurrentVolume",(MyFixedPoint)0);r.Scan();r.Scan();Assert.True(r.Resources.DepartureReady);Assert.Equal(.025,r.Resources.Cargo,8); } [Fact] public void ZeroDepartureFillRestoresOreOnlyEmptyRuleAndKeepsSupplyVolume() { var r=new CargoRig("[Cargo]\nDepartureFill=0");r.Cargo.Set("CurrentVolume",(MyFixedPoint)50);r.Scan();r.Scan();Assert.False(r.Resources.DepartureReady); r.Items.Clear();r.Scan();Assert.False(r.Resources.DepartureReady);r.Scan();Assert.True(r.Resources.Empty);Assert.True(r.Resources.DepartureReady); Assert.Equal(.5,r.Resources.Cargo,8);string reason;Assert.True(r.CanDepart(out reason)); } [Fact] public void StrictOreUnloadCannotLaunchAtTheReturnThresholdWithSupplies() { var r=new CargoRig("[Cargo]\nDepartureFill=0");r.Items.Clear();r.Cargo.Set("CurrentVolume",(MyFixedPoint)95);r.Scan();r.Scan(); Assert.True(r.Resources.Empty);Assert.True(r.Resources.DepartureReady);string reason;Assert.False(r.CanDepart(out reason));Assert.Equal(P.L.MinerCargoFull,reason); } [Fact] public void ScanFaultRescanReloadAndStaleDataCannotKeepDeparturePermission() { var r=new CargoRig();r.Scan();r.Scan();Assert.True(r.Resources.DepartureReady); r.Cargo.Set("ItemCount",513);r.Scan();Assert.False(r.Resources.DepartureReady);Assert.NotEmpty(r.Resources.ScanProblem); r.Cargo.Set("ItemCount",1);r.Resources.ResetInventoryScan();r.Scan();Assert.False(r.Resources.DepartureReady);r.Scan();Assert.True(r.Resources.DepartureReady); var stored=new MyIni();r.Resources.Save(stored);r.Resources.Load(stored);Assert.False(r.Resources.DepartureReady); r.Scan();Assert.False(r.Resources.DepartureReady);r.Scan();Assert.True(r.Resources.DepartureReady); r.Program.Now+=1;string reason;Assert.False(r.CanDepart(out reason));Assert.False(r.Resources.DepartureReady); r.Scan();Assert.False(r.Resources.DepartureReady);r.Scan();Assert.True(r.Resources.DepartureReady); } [Fact] public void DisconnectAndInventoryExceptionsImmediatelyRevokeDeparturePermission() { var r=new CargoRig();r.Scan();r.Scan();r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);r.Resources.Service(.1);Assert.False(r.Resources.DepartureReady); r.Connector.Set("Status",MyShipConnectorStatus.Connected);r.Scan();Assert.False(r.Resources.DepartureReady);r.Scan();Assert.True(r.Resources.DepartureReady); r.Cargo.Method("GetItems",a=>{throw new InvalidOperationException("broken inventory");});r.Scan();Assert.False(r.Resources.DepartureReady);Assert.Equal("broken inventory",r.Resources.ScanProblem); } [Fact] public void InvalidInventoryMassOrBlockedDrillCannotPassTheSmallCargoGate() { var r=new CargoRig();r.Scan();r.Scan();r.Cargo.Set("CurrentMass",(MyFixedPoint)(-1));r.Scan();Assert.False(r.Resources.DepartureReady); r.Cargo.Set("CurrentMass",(MyFixedPoint)1);var drill=CargoRig.Store(.99,1,new List());r.Add(drill,r.Program.Me.CubeGrid,true); r.Cargo.Set("CurrentVolume",(MyFixedPoint)0);r.Scan();r.Scan();Assert.True(r.Resources.Cargo<.05);Assert.False(r.Resources.DepartureReady);Assert.Equal(P.L.MinerDrillBlocked,r.Resources.DepartureWaitReason); } [Fact] public void FivePercentCargoDoesNotBypassBatteryOrHydrogenService() { var r=new CargoRig();r.Resources.NeedsCharge=true;r.Battery.Set("CurrentStoredPower",.5f);r.Scan();r.Scan();Assert.True(r.Resources.DepartureReady); string reason;Assert.False(r.CanDepart(out reason));Assert.Equal(P.L.MinerWaitChargeTarget,reason); r.Battery.Set("CurrentStoredPower",1f);r.Hardware.UsesHydrogen=true; r.Hardware.Tanks.Add(new Stub().Set("CubeGrid",r.Program.Me.CubeGrid).Set("IsFunctional",true).Set("Capacity",1000f).Set("FilledRatio",.5d).Value); r.Resources.NeedsHydrogen=true;r.Scan();Assert.False(r.CanDepart(out reason));Assert.Equal(P.L.MinerWaitHydrogenTarget,reason); } [Theory][InlineData(float.NaN)][InlineData(float.PositiveInfinity)][InlineData(-1f)] public void InvalidBatteryObservationsRevokeBothScanConfirmations(float stored) { var r=new CargoRig();r.Scan();r.Scan();Assert.True(r.Resources.DepartureReady);r.Battery.Set("CurrentStoredPower",stored); Assert.Throws(()=>r.Scan());Assert.False(r.Resources.DepartureReady); r.Battery.Set("CurrentStoredPower",1f);r.Scan();Assert.False(r.Resources.DepartureReady);r.Scan();Assert.True(r.Resources.DepartureReady); } [Fact] public void InvalidHydrogenRatioCannotBypassDepartureEnergyChecks() { var r=new CargoRig();r.Scan();r.Scan();r.Hardware.UsesHydrogen=true; r.Hardware.Tanks.Add(new Stub().Set("CubeGrid",r.Program.Me.CubeGrid).Set("IsFunctional",true).Set("Capacity",1000f).Set("FilledRatio",double.NaN).Value); Assert.Throws(()=>r.Scan());Assert.False(r.Resources.DepartureReady);string reason;Assert.False(r.CanDepart(out reason)); } } }