using System; using System.Collections.Generic; using System.Reflection; using Sandbox.ModAPI.Ingame; using VRage.Game; using VRage.Game.ModAPI.Ingame; using VRageMath; using VRage.Game.ModAPI.Ingame.Utilities; using Xunit; using P = AutoMiningScript.Program; namespace AutoMiningScript.Tests { public class MinerStateTests { static readonly BindingFlags Private = BindingFlags.Instance | BindingFlags.NonPublic; static T Read(object instance, string name) { return (T)instance.GetType().GetField(name, Private).GetValue(instance); } static void Write(object instance, string name, object value) { instance.GetType().GetField(name, Private).SetValue(instance, value); } static void Call(object instance, string name, params object[] args) { instance.GetType().GetMethod(name, Private).Invoke(instance, args); } internal sealed class MinerRig { public readonly P Program; public readonly P.MinerController Miner; public readonly P.ShipHardware Hardware; public readonly P.Resources Resources; public readonly Stub Drill; public readonly Stub Controller; public readonly Stub Camera; public readonly Stub Connector; public Stub CargoInventory; public Vector3D Position; public MyDetectedEntityInfo CameraHit; public readonly List Sent = new List(); public MinerRig() { Program = TestRig.Program("[Flight]\nDepartureMass=1000\n[Mining]\nFootprintWidth=10\nFootprintHeight=10\n"); TestRig.SetBase(Program,"GridTerminalSystem",new Stub().Value); var listener = new Stub(); var unicast = new Stub(); var igc = new Stub().Set("UnicastListener",unicast.Value) .Method("RegisterBroadcastListener",a=>listener.Value) .Method("SendBroadcastMessage",a=> { P.Packet packet; if(P.Packet.TryDecode((string)a[1],Program.Config.FleetId,out packet)) Sent.Add(packet);return null; }); typeof(MyGridProgram).GetField("m_IGC_ContextGetter",Private).SetValue(Program,(Func)(()=>igc.Value)); Program.Bus = new P.Wire(Program); Miner = new P.MinerController(Program); Write(Program,"role",Miner); Hardware = Read(Miner,"ship"); Resources = Read(Miner,"resources"); Controller = new Stub().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true) .Method("get_WorldMatrix",a=>MatrixD.CreateWorld(Position,Vector3D.Forward,Vector3D.Up)) .Method("get_CenterOfMass",a=>Position).Method("GetPosition",a=>Position) .Method("CalculateShipMass",a=>new MyShipMass(1000,1000,1000)); Hardware.Controller = Controller.Value; Connector = TestRig.Connector(Program,false).Set("EntityId",100L) .Method("GetPosition",a=>Position).Method("get_WorldMatrix",a=>MatrixD.CreateTranslation(Position)); Hardware.Connector = Connector.Value; Read>(Hardware,"connectors").Add(Connector.Value); Drill = new Stub().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("WorldMatrix",MatrixD.Identity); Hardware.Drills.Add(Drill.Value); Hardware.Gyros.Add(new Stub().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("WorldMatrix",MatrixD.Identity).Value); Camera = new Stub().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("IsWorking",true).Set("AvailableScanRange",10000d).Set("Enabled",true).Set("EnableRaycast",true) .Method("CanScan",a=>true).Method("Raycast",a=>CameraHit).Method("GetPosition",a=>Position); Hardware.Cameras.Add(Camera.Value); Hardware.Batteries.Add(TestRig.Battery(Program,10,10).Value); Hardware.Radius=2; Hardware.BodyHalfSize=new Vector3D(1); Hardware.BodyMin=new Vector3D(-1);Hardware.BodyMax=new Vector3D(1); Hardware.DrillForwardLocal=Vector3D.Forward;Hardware.DrillUpLocal=Vector3D.Up; foreach(var direction in new[]{Vector3D.Right,Vector3D.Left,Vector3D.Up,Vector3D.Down,Vector3D.Forward,Vector3D.Backward}) { var orientation=MatrixD.CreateWorld(Vector3D.Zero,-direction,Math.Abs(direction.Y)>.9?Vector3D.Forward:Vector3D.Up); Hardware.Thrusters.Add(new Stub().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("Enabled",true) .Set("MaxEffectiveThrust",100000f).Set("WorldMatrix",orientation).Value); } Read>(Hardware,"localThrusters").AddRange(Hardware.Thrusters); Resources.Battery=1;Resources.StoredMWh=10;Resources.CapacityMWh=10;Resources.Empty=true; Write(Miner,"healthy",true);Write(Miner,"running",true);Write(Miner,"leaseConfirmed",true); foreach(string timer in new[]{"nextHeartbeat","nextTelemetry","healthAt","nextResources"})Write(Miner,timer,double.MaxValue); TestRig.SetBase(Program,"GridTerminalSystem",new Stub().Method("GetBlocksOfType",a=>{ var cameras=a[0] as List;if(cameras!=null)cameras.AddRange(Hardware.Cameras);return null; }).Value); } public void Task(double progress=0) { Write(Miner,"job",new P.Job {Id="hole",Owner=Program.Config.Id,Kind=P.JobKind.Probe,Entry=Vector3D.Zero,Direction=Vector3D.Forward,Up=Vector3D.Up,Depth=40,Progress=progress,EntityId=42}); } public void State(P.FlightState state) { Call(Miner,"Set",state,"test",false); } public P.FlightState StateNow { get { return Read(Miner,"state"); } } public void PrepareDeparture(string token) { Task();Program.Now=3;Connector.Set("Status",MyShipConnectorStatus.Connected);State(P.FlightState.Servicing); var dock=new P.DockFrame {BaseId=Program.Config.BaseId,Owner=Program.Config.Id,Token=token,ReceivedAt=3}; Write(Miner,"home",dock);Write(Miner,"dock",dock);Write(Miner,"lane","valid-lease");Write(Miner,"laneKind","depart"); Write(Miner,"calibrated",true);Write(Miner,"stableSince",0d);Write(Miner,"nextRequest",double.MaxValue); Write(Miner,"calibratedForward",Vector3D.Forward);Write(Miner,"calibratedUp",Vector3D.Up); var inventory=new Stub().Set("CurrentVolume",(VRage.MyFixedPoint)0).Set("MaxVolume",(VRage.MyFixedPoint)10); CargoInventory=inventory; Hardware.InventoryBlocks.Add(new Stub().Set("CubeGrid",Program.Me.CubeGrid).Set("HasInventory",true).Set("InventoryCount",1).Method("GetInventory",a=>inventory.Value).Value); Resources.Update(.5,P.FlightState.Servicing,0,0);Resources.Update(.5,P.FlightState.Servicing,0,0); } public static MyDetectedEntityInfo Hit(MyDetectedEntityType type) { return new MyDetectedEntityInfo(42,"fixture",type,Vector3D.Zero,MatrixD.Identity,Vector3.Zero,MyRelationsBetweenPlayerAndBlock.NoOwnership,new BoundingBoxD(new Vector3D(-1),new Vector3D(1)),1); } } [Fact] public void AdvancingThroughTheApproachDoesNotTimeOutBeforeTheEntrance() { var r=new MinerRig();r.Task();r.Position=Vector3D.Backward*22;r.CameraHit=MinerRig.Hit(MyDetectedEntityType.Asteroid);r.State(P.FlightState.Align); for(int ray=0;ray<4;ray++) {Call(r.Miner,"Align",.1);Assert.Equal(P.FlightState.Align,r.StateNow);Assert.False(r.Drill.Value.Enabled);} Call(r.Miner,"Align",.1);Assert.Equal(P.FlightState.Drilling,r.StateNow); // The drill advances at its normal 0.5 m/s but needs 44 seconds just to reach the entrance. for(int second=1;second<=31;second++) {r.Program.Now=second;r.Position=Vector3D.Backward*(22-second*.5);Call(r.Miner,"Drill",.1);} Assert.Equal(P.FlightState.Drilling,r.StateNow); Assert.Equal("",Read(r.Miner,"completion")); } [Fact] public void ResumedHoleRejectsAShipBlockingThePreviouslyDrilledAxis() { var r=new MinerRig();r.Task(12);r.Position=Vector3D.Backward*22;r.CameraHit=MinerRig.Hit(MyDetectedEntityType.LargeGrid);r.State(P.FlightState.Align); Call(r.Miner,"Align",.1); Assert.NotEqual(P.FlightState.Drilling,r.StateNow); Assert.False(r.Drill.Value.Enabled); } [Theory] [InlineData(false,true)] [InlineData(false,false)] [InlineData(true,true)] [InlineData(true,false)] public void EntryAndSurveyTryOtherCamerasAfterASelfHitAndDoNotReportUnseenSpaceAsEmpty(bool survey,bool otherCamera) { var r=new MinerRig();r.Task();r.CameraHit=ReturnNavigationTests.Rig.Detection(r.Program.Me.CubeGrid.EntityId); if(otherCamera) r.Hardware.Cameras.Add(new Stub().Set("CubeGrid",r.Program.Me.CubeGrid).Set("IsWorking",true) .Method("CanScan",a=>true).Method("Raycast",a=>MinerRig.Hit(MyDetectedEntityType.Asteroid)).Value); r.Position=survey?Vector3D.Zero:Vector3D.Backward*22; if(survey)Read(r.Miner,"job").Kind=P.JobKind.Survey; r.State(survey?P.FlightState.Survey:P.FlightState.Align); for(int n=0;n<(survey?1:5);n++)Call(r.Miner,survey?"Survey":"Align",.1); if(otherCamera) { Assert.Equal(survey?P.FlightState.Survey:P.FlightState.Drilling,r.StateNow); if(survey)Assert.Contains(r.Sent,packet=>packet.Kind=="RESULT" && P.JobReport.FromIni(packet.Body).Outcome=="SurveyHit"); } else { Assert.Equal(survey?P.FlightState.Survey:P.FlightState.Align,r.StateNow); Assert.NotNull(Read(r.Miner,"job"));Assert.DoesNotContain(r.Sent,packet=>packet.Kind=="RESULT"); Assert.Equal(survey?P.L.MinerWaitSurveyScan:P.L.MinerWaitEntryScan,Read(r.Miner,"reason")); } } [Fact] public void LostMotherHeartbeatDuringFinalApproachUsesTheExitCorridor() { var r=new MinerRig();r.Task();r.Program.Now=6;r.Position=Vector3D.Backward*4; var home=new P.DockFrame {ReceivedAt=0,Position=Vector3D.Zero,Forward=Vector3D.Backward,Up=Vector3D.Up}; var dock=new P.DockFrame {ReceivedAt=6,Position=Vector3D.Zero,Forward=Vector3D.Backward,Up=Vector3D.Up,Owner=r.Program.Config.Id,Token="lease",GridId=50,ConnectorId=51}; Write(r.Miner,"home",home);Write(r.Miner,"dock",dock);Write(r.Miner,"lane","lease");Write(r.Miner,"laneKind","dock");r.State(P.FlightState.DockApproach); r.Miner.Tick(.1); Assert.Equal(P.FlightState.DockRetreat,r.StateNow); Vector3D abort=Read(r.Miner,"abortPoint");Assert.True(Vector3D.Dot(abort-r.Position,dock.Forward)>0); } [Fact] public void PauseInsideAHoleRetainsProgressAndRetreatsWithoutDrilling() { var r=new MinerRig();r.Task(12);r.Position=Vector3D.Forward*12;Write(r.Miner,"insideBore",true);r.State(P.FlightState.Drilling); r.Drill.Set("Enabled",true);r.Miner.Command("pause");Assert.Equal(P.FlightState.Retreat,r.StateNow); Call(r.Miner,"Retreat",.1);Assert.False(r.Drill.Value.Enabled);Assert.Equal(12,Read(r.Miner,"job").Progress); Assert.True(Read(r.Miner,"manualStop"));Assert.True(Read(r.Miner,"pausedAfterRetreat")); } [Fact] public void AStalePortPoseCannotUnlockAValidDepartureLease() { var r=new MinerRig();r.PrepareDeparture("");int disconnects=0; r.Connector.Method("Disconnect",a=>{disconnects++;r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);return null;}); Call(r.Miner,"DockedTick",.1);Assert.Equal(0,disconnects);Assert.Equal(MyShipConnectorStatus.Connected,r.Connector.Value.Status); } [Fact] public void UnloadedAndPoweredMinerDepartsThroughItsConfirmedLease() { var r=new MinerRig();r.PrepareDeparture("valid-lease");int disconnects=0; r.Connector.Method("Disconnect",a=>{disconnects++;r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);return null;}); Call(r.Miner,"DockedTick",.1);Assert.Equal(1,disconnects);Assert.Equal(P.FlightState.Departing,r.StateNow); } [Fact] public void MissingDepartureCameraReportsTheBlockerAndKeepsItsLaneAndTask() { var r=new MinerRig();r.PrepareDeparture("valid-lease");r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);r.State(P.FlightState.Departing); r.Camera.Method("CanScan",a=>false);Call(r.Miner,"Departure",.1); Assert.Equal(P.L.MinerCameraUnavailable,Read(r.Miner,"reason")); Assert.Equal(P.FlightState.Departing,r.StateNow);Assert.Equal("valid-lease",Read(r.Miner,"lane")); Assert.NotNull(Read(r.Miner,"job"));Assert.DoesNotContain(r.Sent,p=>p.Kind=="LANE_RELEASE"); } [Fact] public void DepartureAttitudeWaitAndFlightMeasurementsAreVisible() { var r=new MinerRig();r.PrepareDeparture("valid-lease");r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);r.State(P.FlightState.Departing); Write(r.Miner,"calibratedForward",Vector3D.Right);Call(r.Miner,"Departure",.1); Assert.Contains(P.L.F(P.L.MinerDepartureAttitude,90d),Read(r.Miner,"reason")); var flight=Read(r.Miner,"flight"); Assert.Contains(P.L.F(P.L.MinerFlightDiagnostics,90d,flight.ScanClearance,flight.Problem),r.Miner.Diagnostics); Assert.Equal(P.FlightState.Departing,r.StateNow);Assert.DoesNotContain(r.Sent,p=>p.Kind=="LANE_RELEASE"); } [Fact] public void ClearDepartureCorridorReachesTransitWithItsTaskAndReleasesOnce() { // Ideal translational response checks the phase transition; this does not simulate game physics. var r=new MinerRig();r.PrepareDeparture("valid-lease");r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);r.State(P.FlightState.Departing); var dock=Read(r.Miner,"dock");Vector3D velocity=Vector3D.Zero; r.Controller.Method("GetShipVelocities",a=>new MyShipVelocities(velocity,Vector3D.Zero)); const double dt=1d/60; for(int step=0;step<4800&&r.StateNow==P.FlightState.Departing;step++) { r.Program.Now+=dt;dock.ReceivedAt=r.Program.Now;Call(r.Miner,"Departure",dt); Vector3D acceleration=Vector3D.Zero; foreach(var thruster in r.Hardware.Thrusters) acceleration+=thruster.WorldMatrix.Backward*thruster.MaxEffectiveThrust*thruster.ThrustOverridePercentage/1000; velocity+=acceleration*dt;r.Position+=velocity*dt; } Assert.Equal(P.FlightState.Transit,r.StateNow);Assert.Equal("hole",Read(r.Miner,"job").Id); Assert.Equal("",Read(r.Miner,"lane"));Assert.Single(r.Sent,p=>p.Kind=="LANE_RELEASE"); Assert.Equal(P.L.MinerTravelTaskEntry,Read(r.Miner,"reason")); } static P.DockFrame PrepareHolding(MinerRig r,bool atWaitingPoint=true) { r.PrepareDeparture("valid-lease");r.Connector.Set("Status",MyShipConnectorStatus.Unconnected); Write(r.Miner,"laneKind","dock");Write(r.Miner,"stableSince",-1d);r.State(P.FlightState.Holding); var dock=Read(r.Miner,"dock");dock.WaitingLocal=new Vector3D(0,4,-80); if(atWaitingPoint)r.Position=Vector3D.Transform(dock.WaitingLocal,dock.Predicted(r.Program.Now)); return dock; } [Fact] public void CancelledMiningTaskCanStillEnterDockWithItsIndependentLaneGrant() { var r=new MinerRig();var dock=PrepareHolding(r);r.Miner.Command("cancel-through 10"); Assert.False(Read(r.Miner,"leaseConfirmed"));Assert.True(Read(r.Miner,"canceling")); Call(r.Miner,"Waiting",.1);Assert.Equal(P.FlightState.DockAlign,r.StateNow); Assert.Contains(P.L.MinerDockOuterEntry,Read(r.Miner,"reason")); Assert.Contains(P.L.F(P.L.MinerDockPermitDiagnostics,P.L.MinerDockGrantValid),r.Miner.Diagnostics); r.Program.Now+=1;dock.ReceivedAt=r.Program.Now; r.Miner.Receive(new P.Packet {Kind="DOCK_GRANT",From=r.Program.Config.BaseId,Body=dock.ToIni()}); r.Position=Vector3D.Forward*20;Call(r.Miner,"DockAlign",.1);Assert.Equal(P.FlightState.DockApproach,r.StateNow); Assert.Equal("valid-lease",Read(r.Miner,"lane")); Assert.DoesNotContain(r.Sent,p=>p.Kind=="LANE_RELEASE"); } [Fact] public void HoldingTimeoutPreservesCameraBlockerAndDoesNotBlameStaticMothership() { var r=new MinerRig();var dock=PrepareHolding(r,false);Write(r.Miner,"lane","");r.Camera.Method("CanScan",a=>false); r.Program.Now+=121;dock.ReceivedAt=r.Program.Now;Call(r.Miner,"Waiting",.1); string reason=Read(r.Miner,"reason");Assert.StartsWith(P.L.MinerCameraUnavailable,reason); Assert.Contains(P.L.MinerDockWaitBudgetExceeded,reason); Assert.Contains(P.L.F(P.L.MinerDockWaitingPoint,dock.WaitingLocal.Length(),0d,0d),reason); Assert.Equal(P.FlightState.Holding,r.StateNow);Assert.Equal("",Read(r.Miner,"lane")); Assert.DoesNotContain(r.Sent,p=>p.Kind=="LANE_RELEASE"); } [Fact] public void AValidGrantDoesNotAuthorizeTravelToEntryWithoutFreshScans() { var r=new MinerRig();var dock=PrepareHolding(r,false);r.Camera.Method("CanScan",a=>false); r.Position=Vector3D.Forward*80;Call(r.Miner,"Waiting",.1); for(int n=0;n<40;n++) {r.Program.Now+=.1;dock.ReceivedAt=r.Program.Now;Call(r.Miner,"DockAlign",.1);} Assert.Equal(P.FlightState.DockAlign,r.StateNow); Assert.Equal(0,Read(r.Miner,"flight").ScanClearance); Assert.All(r.Hardware.Thrusters,t=>Assert.Equal(0,t.ThrustOverridePercentage)); Assert.Contains(P.L.MinerCameraUnavailable,Read(r.Miner,"reason")); } [Theory] [InlineData("missing")][InlineData("direction")][InlineData("owner")][InlineData("changed")][InlineData("occupied")][InlineData("motion")] public void DockGrantProblemsAreDiagnosedSeparatelyAndCannotStartApproach(string problem) { var r=new MinerRig();var dock=PrepareHolding(r);string expected; switch(problem) { case "missing":Write(r.Miner,"lane","");expected=P.L.MinerDockGrantMissing;break; case "direction":Write(r.Miner,"laneKind","depart");expected=P.L.MinerDockGrantDirection;break; case "owner":dock.Owner="other-miner";expected=P.L.MinerDockGrantOwner;break; case "changed":dock.Token="revoked";expected=P.L.MinerDockGrantChanged;break; case "occupied":dock.Connected=true;expected=P.L.MinerDockBerthOccupied;break; default:dock.Acceleration=new Vector3D(.2,0,0);expected=P.L.F(P.L.MinerDockMotionLimit,0d,0d,.2);break; } Call(r.Miner,"Waiting",.1);Assert.Contains(expected,Read(r.Miner,"reason"));Assert.Contains(expected,r.Miner.Diagnostics); Assert.Equal(P.FlightState.Holding,r.StateNow);Assert.Equal(-1d,Read(r.Miner,"stableSince")); } [Fact] public void StaleDockTelemetryPreventsDirectEntryAndAbortsAnActiveApproach() { var r=new MinerRig();var dock=PrepareHolding(r); r.Program.Now+=1;Call(r.Miner,"Waiting",.1); Assert.Equal(P.L.F(P.L.MinerDockPoseStale,1d),Read(r.Miner,"reason"));Assert.Equal(-1d,Read(r.Miner,"stableSince")); Assert.Equal(P.FlightState.Holding,r.StateNow); r.Position=Vector3D.Forward*20;dock.ReceivedAt=r.Program.Now;Call(r.Miner,"Waiting",.1);Assert.Equal(P.FlightState.DockApproach,r.StateNow); r.Program.Now+=1;Call(r.Miner,"DockApproach",.1);Assert.Equal(P.FlightState.DockRetreat,r.StateNow); Assert.Equal(P.L.F(P.L.MinerDockPoseStale,1d),Read(r.Miner,"reason")); } [Fact] public void ExplicitResumeAfterManualRecoveryConfirmsCancelledTaskAtTheDock() { var r=new MinerRig();PrepareHolding(r);r.Miner.Command("cancel-through 10");r.Miner.Command("release"); r.Connector.Set("Status",MyShipConnectorStatus.Connected);r.Miner.Tick(.1); Assert.Equal(P.FlightState.Manual,r.StateNow);Assert.True(Read(r.Miner,"canceling")); r.Miner.Command("resume");r.Miner.Tick(.1); Assert.Equal(P.FlightState.Servicing,r.StateNow);Assert.False(Read(r.Miner,"canceling")); Assert.Null(Read(r.Miner,"job"));Assert.False(Read(r.Miner,"running")); var telemetry=r.Miner.GetTelemetry()[0];Assert.Equal(10,telemetry.CancelledThrough);Assert.Equal("",telemetry.JobId);Assert.True(telemetry.Connected); } [Theory] [InlineData(.05,true)] [InlineData(.051,false)] public void ActualDepartureUsesFivePercentLimitEvenWithRemainingOre(double fill,bool departs) { var r=new MinerRig();r.PrepareDeparture("valid-lease");object boxed=default(MyInventoryItem); typeof(MyInventoryItem).GetField("Type").SetValue(boxed,MyItemType.MakeOre("Stone")); typeof(MyInventoryItem).GetField("Amount").SetValue(boxed,(VRage.MyFixedPoint)1); r.CargoInventory.Set("CurrentVolume",(VRage.MyFixedPoint)(fill*10)).Method("GetItems",a=>{((List)a[0]).Add((MyInventoryItem)boxed);return null;}); r.Resources.Update(.5,P.FlightState.Servicing,0,0);r.Resources.Update(.5,P.FlightState.Servicing,0,0);Assert.False(r.Resources.Empty); int disconnects=0;r.Connector.Method("Disconnect",a=>{disconnects++;r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);return null;}); Call(r.Miner,"DockedTick",.1);Assert.Equal(departs?1:0,disconnects); } [Fact] public void DepartureWaitsForAnActiveChargeLatchEvenAfterUnloading() { var r=new MinerRig();r.PrepareDeparture("valid-lease");r.Resources.NeedsCharge=true;r.Resources.Battery=.5;r.Resources.StoredMWh=5; int disconnects=0;r.Connector.Method("Disconnect",a=>{disconnects++;return null;}); Call(r.Miner,"DockedTick",.1);Assert.Equal(0,disconnects);Assert.True(r.Resources.NeedsCharge); } [Fact] public void HoverKeepsThirtyPercentThrustInAdditionToBrakingAcceleration() { var r=new MinerRig();var flight=Read(r.Miner,"flight"); r.Controller.Method("GetNaturalGravity",a=>new Vector3D(0,-71,0)); Assert.False(flight.CanSupport(Vector3D.Forward,Vector3D.Up,.3)); r.Controller.Method("GetNaturalGravity",a=>new Vector3D(0,-69,0)); Assert.True(flight.CanSupport(Vector3D.Forward,Vector3D.Up,.3)); Assert.True(flight.ThrustMargin>.3); } [Fact] public void InventoryScanFaultStillExecutesRetreatFlightOnFollowingTicks() { var r=new MinerRig();r.Task(12);r.Position=Vector3D.Forward*12;Write(r.Miner,"insideBore",true);r.State(P.FlightState.Drilling); r.Resources.ScanProblem=P.L.MinerInventoryScanLimit;r.Drill.Set("Enabled",true); r.Miner.Tick(.1);Assert.Equal(P.FlightState.Retreat,r.StateNow);Assert.False(r.Drill.Value.Enabled); // Reset all outputs to prove the next invocation reaches flight control again. foreach(var thrust in r.Hardware.Thrusters)thrust.ThrustOverridePercentage=0; r.Program.Now=.1;r.Miner.Tick(.1); Assert.Equal(P.FlightState.Retreat,r.StateNow);Assert.False(r.Drill.Value.Enabled); Assert.Contains(r.Hardware.Thrusters,thrust=>thrust.ThrustOverridePercentage>0); Assert.Equal(P.L.MinerInventoryScanLimit,r.Resources.ScanProblem); } [Fact] public void CancellingPendingJobsKeepsUnrelatedSafetyMessages() { var r=new MinerRig();r.Program.Bus.Send("JOB","one",new MyIni(),true);r.Program.Bus.Send("JOB","two",new MyIni(),true);r.Program.Bus.Send("LANE_RELEASE","one",new MyIni(),true); Assert.Equal(3,r.Program.Bus.PendingCount);r.Program.Bus.CancelPending("JOB","one");Assert.Equal(2,r.Program.Bus.PendingCount); r.Program.Bus.CancelPending("JOB");Assert.Equal(1,r.Program.Bus.PendingCount); } [Fact] public void ClearInsideBorePreservesAxisUntilDockAndNeverReportsCompletion() { var r=new MinerRig();r.Task(12);r.Position=Vector3D.Forward*12;Write(r.Miner,"insideBore",true);r.State(P.FlightState.Drilling); Write(r.Miner,"completion","Complete");r.Drill.Set("Enabled",true); r.Miner.Command("cancel-through 10"); Assert.Equal(P.FlightState.Retreat,r.StateNow);Assert.False(r.Drill.Value.Enabled); Assert.Equal(12,Read(r.Miner,"job").Progress);Assert.Equal("",Read(r.Miner,"completion")); r.Miner.Command("resume");Assert.Equal(P.FlightState.Retreat,r.StateNow); r.Position=Vector3D.Backward*22;Call(r.Miner,"Retreat",.1); Assert.Equal(P.FlightState.Returning,r.StateNow);Assert.NotNull(Read(r.Miner,"job")); Assert.DoesNotContain(r.Sent,p=>p.Kind=="RESULT"); r.Connector.Set("Status",MyShipConnectorStatus.Connected);Call(r.Miner,"DockedTick",.1); Assert.Null(Read(r.Miner,"job"));Assert.False(Read(r.Miner,"running")); } [Fact] public void ClearWatermarkPersistsImmediatelyAndBlocksDelayedJobsAfterReload() { var r=new MinerRig();r.PrepareDeparture("valid-lease");r.Miner.Command("cancel-through 10"); var saved=new MyIni();Assert.True(saved.TryParse(r.Program.Storage)); var reload=new MinerRig();reload.Connector.Set("Status",MyShipConnectorStatus.Connected);reload.Miner.Load(saved); Action receive=serial=>reload.Miner.Receive(new P.Packet {Kind="JOB",From=reload.Program.Config.BaseId,Body=new P.Job {Id="base-job-"+serial,Owner=reload.Program.Config.Id,Depth=10}.ToIni()}); receive(9);Assert.Null(Read(reload.Miner,"job")); receive(11);Assert.Equal("base-job-11",Read(reload.Miner,"job").Id); reload.Miner.Command("cancel-through 10");Assert.Equal("base-job-11",Read(reload.Miner,"job").Id); Assert.Equal(10,reload.Miner.GetTelemetry()[0].CancelledThrough); } [Fact] public void CancelledBoreReloadAndResumeOnlyRetreats() { var r=new MinerRig();r.Task(12);r.State(P.FlightState.Drilling);Write(r.Miner,"insideBore",true); r.Miner.Command("cancel-through 10");var saved=new MyIni();Assert.True(saved.TryParse(r.Program.Storage)); var reload=new MinerRig();reload.Miner.Load(saved);reload.Miner.Command("resume"); Assert.Equal(P.FlightState.Retreat,reload.StateNow);Assert.False(reload.Drill.Value.Enabled); Assert.Equal(12,Read(reload.Miner,"job").Progress);Assert.False(Read(reload.Miner,"leaseConfirmed")); } [Theory] [InlineData(P.FlightState.Manual)] [InlineData(P.FlightState.DockApproach)] public void CancellationPreservesManualControlAndAnExistingReturnApproach(P.FlightState state) { var r=new MinerRig();r.Task();r.State(state);r.Miner.Command("cancel-through 10");Assert.Equal(state,r.StateNow); Assert.True(Read(r.Miner,"canceling"));Assert.False(Read(r.Miner,"running")); } [Theory] [InlineData(P.FlightState.DockAlign)] [InlineData(P.FlightState.DockApproach)] [InlineData(P.FlightState.DockRetreat)] [InlineData(P.FlightState.Holding)] [InlineData(P.FlightState.Returning)] public void ResumeAfterClearPreservesTheReturnCorridor(P.FlightState state) { var r=new MinerRig();r.Task();r.State(state);r.Miner.Command("cancel-through 10");r.Miner.Command("resume"); Assert.Equal(state,r.StateNow);Assert.True(Read(r.Miner,"canceling"));Assert.False(Read(r.Miner,"running")); } [Fact] public void CancellationDuringDepartureUsesDockExitCorridor() { var r=new MinerRig();r.PrepareDeparture("valid-lease");r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);r.State(P.FlightState.Departing); r.Miner.Command("cancel-through 10");Assert.Equal(P.FlightState.DockRetreat,r.StateNow);Assert.Equal("valid-lease",Read(r.Miner,"lane")); } [Fact] public void FaultedMinerWithoutControllerStillPersistsCancellation() { var r=new MinerRig();r.Hardware.Controller=null;Write(r.Miner,"healthy",false);r.State(P.FlightState.Fault); r.Miner.Command("cancel-through 10");Assert.Equal(P.FlightState.Fault,r.StateNow);Assert.Equal(10,r.Miner.GetTelemetry()[0].CancelledThrough); } [Fact] public void InventoryScanFaultBlocksDepartureDespiteAnOldEmptyFlagAndValidLease() { var r=new MinerRig();r.PrepareDeparture("valid-lease");r.Resources.ScanProblem=P.L.MinerInventoryScanLimit; int disconnects=0;r.Connector.Method("Disconnect",a=>{disconnects++;return null;}); Call(r.Miner,"DockedTick",.1);Assert.Equal(0,disconnects);Assert.Equal(P.L.MinerInventoryScanLimit,Read(r.Miner,"reason")); } static Stub DockPeer(MinerRig r) { var grid=new Stub().Set("EntityId",50L); var peer=new Stub().Set("CubeGrid",grid.Value).Set("EntityId",51L) .Set("WorldMatrix",MatrixD.CreateWorld(new Vector3D(10,20,30),Vector3D.Left,Vector3D.Up)) .Method("GetPosition",a=>new Vector3D(10,20,30)); r.Connector.Set("Status",MyShipConnectorStatus.Connected).Set("OtherConnector",peer.Value); return peer; } [Fact] public void FirstPhysicalDockAutomaticallyCalibratesAnOffsetRotatedReferenceAndSavesOnce() { var r=new MinerRig();var peer=DockPeer(r);r.Position=new Vector3D(13,24,35);r.State(P.FlightState.Docked); r.Miner.Tick(.1); Assert.True(Read(r.Miner,"calibrated")); var inverse=MatrixD.Transpose(peer.Value.WorldMatrix); Assert.Equal(Vector3D.TransformNormal(r.Position-peer.Value.GetPosition(),inverse),Read(r.Miner,"calibratedPosition")); Assert.Equal(Vector3D.TransformNormal(Vector3D.Forward,inverse),Read(r.Miner,"calibratedForward")); var stored=r.Program.Storage;Assert.False(string.IsNullOrEmpty(stored)); int events=r.Program.Events.Count;r.Miner.Tick(.1); Assert.Equal(events,r.Program.Events.Count);Assert.Equal(stored,r.Program.Storage); } [Fact] public void ReconnectingMeasuresTheNewPhysicalMountWithoutTakingManualControl() { var r=new MinerRig();DockPeer(r);r.State(P.FlightState.Manual);r.Miner.Tick(.1); var original=Read(r.Miner,"calibratedPosition"); r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);r.Miner.Tick(.1); r.Position=Vector3D.Right*4;r.Connector.Set("Status",MyShipConnectorStatus.Connected);r.Miner.Tick(.1); Assert.NotEqual(original,Read(r.Miner,"calibratedPosition"));Assert.Equal(P.FlightState.Manual,r.StateNow); Assert.All(r.Hardware.Gyros,g=>Assert.False(g.GyroOverride)); } [Fact] public void AutomaticCalibrationCanBeDisabledAndTheOriginalCommandStillWorks() { var r=new MinerRig();r.Program.Config=new P.Settings("[Hardware]\nAutoCalibrate=false","miner","test");DockPeer(r);r.State(P.FlightState.Docked); r.Miner.Tick(.1);Assert.False(Read(r.Miner,"calibrated")); r.Miner.Command("calibrate");Assert.True(Read(r.Miner,"calibrated")); } [Theory] [InlineData("missing")] [InlineData("unconnected")] [InlineData("same-grid")] [InlineData("wrong-berth")] [InlineData("invalid-pose")] public void AutomaticCalibrationRequiresValidForeignPhysicalDockEvidence(string fault) { var r=new MinerRig();var peer=DockPeer(r);r.State(P.FlightState.Docked); if(fault=="missing")r.Connector.Set("OtherConnector",null); if(fault=="unconnected")r.Connector.Set("Status",MyShipConnectorStatus.Unconnected); if(fault=="same-grid")peer.Set("CubeGrid",r.Program.Me.CubeGrid); if(fault=="wrong-berth")Write(r.Miner,"dock",new P.DockFrame {ConnectorId=99}); if(fault=="invalid-pose") {var pose=MatrixD.Identity;pose.M11=double.NaN;peer.Set("WorldMatrix",pose);} r.Miner.Tick(.1);Assert.False(Read(r.Miner,"calibrated")); } [Fact] public void FailedInitializationRetriesAreBoundedAndYieldToThePilot() { var r=new MinerRig();int scans=0; TestRig.SetBase(r.Program,"GridTerminalSystem",new Stub().Method("GetBlocksOfType",a=>{scans++;return null;}).Value); Write(r.Miner,"healthy",false);r.Controller.Set("IsUnderControl",true); r.Miner.Tick(.1);Assert.Equal(0,scans); r.Controller.Set("IsUnderControl",false);r.Miner.Tick(.1);Assert.Equal(1,scans); r.Program.Now=4;r.Miner.Tick(.1);Assert.Equal(1,scans); r.Program.Now=5;r.Miner.Tick(.1);Assert.Equal(2,scans); } [Fact] public void RemoteDiagnosisReturnsTheCompleteReportOnlyToTheConfiguredBase() { var r=new MinerRig();var body=new MyIni();body.Set("command","Text","diagnose"); r.Miner.Receive(new P.Packet {Kind="COMMAND",From="stranger",Body=body}); Assert.DoesNotContain(r.Sent,p=>p.Kind=="DIAGNOSTICS"); r.Miner.Receive(new P.Packet {Kind="COMMAND",From=r.Program.Config.BaseId,Body=body}); var reply=Assert.Single(r.Sent,p=>p.Kind=="DIAGNOSTICS"); Assert.Equal(r.Miner.Diagnostics.TrimEnd(),reply.Body.Get("diagnostics","Text").ToString().TrimEnd());Assert.Equal(r.Program.Config.BaseId,reply.Target); } } }