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<T>(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<IMyShipDrill> Drill;
public readonly Stub<IMyShipController> Controller;
public readonly Stub<IMyCameraBlock> Camera;
public readonly Stub<IMyShipConnector> Connector;
public Stub<IMyInventory> CargoInventory;
public Vector3D Position;
public MyDetectedEntityInfo CameraHit;
public readonly List<P.Packet> Sent = new List<P.Packet>();
public MinerRig()
{
Program = TestRig.Program("[Flight]\nDepartureMass=1000\n[Mining]\nFootprintWidth=10\nFootprintHeight=10\n");
TestRig.SetBase(Program,"GridTerminalSystem",new Stub<IMyGridTerminalSystem>().Value);
var listener = new Stub<IMyBroadcastListener>();
var unicast = new Stub<IMyUnicastListener>();
var igc = new Stub<IMyIntergridCommunicationSystem>().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<IMyIntergridCommunicationSystem>)(()=>igc.Value));
Program.Bus = new P.Wire(Program);
Miner = new P.MinerController(Program);
Write(Program,"role",Miner);
Hardware = Read<P.ShipHardware>(Miner,"ship"); Resources = Read<P.Resources>(Miner,"resources");
Controller = new Stub<IMyShipController>().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<List<IMyShipConnector>>(Hardware,"connectors").Add(Connector.Value);
Drill = new Stub<IMyShipDrill>().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("WorldMatrix",MatrixD.Identity);
Hardware.Drills.Add(Drill.Value);
Hardware.Gyros.Add(new Stub<IMyGyro>().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("WorldMatrix",MatrixD.Identity).Value);
Camera = new Stub<IMyCameraBlock>().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<IMyThrust>().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("Enabled",true)
.Set("MaxEffectiveThrust",100000f).Set("WorldMatrix",orientation).Value);
}
Read<List<IMyThrust>>(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<IMyGridTerminalSystem>().Method("GetBlocksOfType",a=>{
var cameras=a[0] as List<IMyCameraBlock>;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<P.FlightState>(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<IMyInventory>().Set("CurrentVolume",(VRage.MyFixedPoint)0).Set("MaxVolume",(VRage.MyFixedPoint)10);
CargoInventory=inventory;
Hardware.InventoryBlocks.Add(new Stub<IMyCargoContainer>().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<string>(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<IMyCameraBlock>().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<P.Job>(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<P.Job>(r.Miner,"job"));Assert.DoesNotContain(r.Sent,packet=>packet.Kind=="RESULT");
Assert.Equal(survey?P.L.MinerWaitSurveyScan:P.L.MinerWaitEntryScan,Read<string>(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<Vector3D>(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<P.Job>(r.Miner,"job").Progress);
Assert.True(Read<bool>(r.Miner,"manualStop"));Assert.True(Read<bool>(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<string>(r.Miner,"reason"));
Assert.Equal(P.FlightState.Departing,r.StateNow);Assert.Equal("valid-lease",Read<string>(r.Miner,"lane"));
Assert.NotNull(Read<P.Job>(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<string>(r.Miner,"reason"));
var flight=Read<P.FlightController>(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<P.DockFrame>(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<P.Job>(r.Miner,"job").Id);
Assert.Equal("",Read<string>(r.Miner,"lane"));Assert.Single(r.Sent,p=>p.Kind=="LANE_RELEASE");
Assert.Equal(P.L.MinerTravelTaskEntry,Read<string>(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<P.DockFrame>(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<bool>(r.Miner,"leaseConfirmed"));Assert.True(Read<bool>(r.Miner,"canceling"));
Call(r.Miner,"Waiting",.1);Assert.Equal(P.FlightState.DockAlign,r.StateNow);
Assert.Contains(P.L.MinerDockOuterEntry,Read<string>(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<string>(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<string>(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<string>(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<P.FlightController>(r.Miner,"flight").ScanClearance);
Assert.All(r.Hardware.Thrusters,t=>Assert.Equal(0,t.ThrustOverridePercentage));
Assert.Contains(P.L.MinerCameraUnavailable,Read<string>(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<string>(r.Miner,"reason"));Assert.Contains(expected,r.Miner.Diagnostics);
Assert.Equal(P.FlightState.Holding,r.StateNow);Assert.Equal(-1d,Read<double>(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<string>(r.Miner,"reason"));Assert.Equal(-1d,Read<double>(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<string>(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<bool>(r.Miner,"canceling"));
r.Miner.Command("resume");r.Miner.Tick(.1);
Assert.Equal(P.FlightState.Servicing,r.StateNow);Assert.False(Read<bool>(r.Miner,"canceling"));
Assert.Null(Read<P.Job>(r.Miner,"job"));Assert.False(Read<bool>(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<MyInventoryItem>)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<P.FlightController>(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<P.Job>(r.Miner,"job").Progress);Assert.Equal("",Read<string>(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<P.Job>(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<P.Job>(r.Miner,"job"));Assert.False(Read<bool>(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<int> 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<P.Job>(reload.Miner,"job"));
receive(11);Assert.Equal("base-job-11",Read<P.Job>(reload.Miner,"job").Id);
reload.Miner.Command("cancel-through 10");Assert.Equal("base-job-11",Read<P.Job>(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<P.Job>(reload.Miner,"job").Progress);Assert.False(Read<bool>(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<bool>(r.Miner,"canceling"));Assert.False(Read<bool>(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<bool>(r.Miner,"canceling"));Assert.False(Read<bool>(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<string>(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<string>(r.Miner,"reason"));
}
static Stub<IMyShipConnector> DockPeer(MinerRig r)
{
var grid=new Stub<IMyCubeGrid>().Set("EntityId",50L);
var peer=new Stub<IMyShipConnector>().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<bool>(r.Miner,"calibrated"));
var inverse=MatrixD.Transpose(peer.Value.WorldMatrix);
Assert.Equal(Vector3D.TransformNormal(r.Position-peer.Value.GetPosition(),inverse),Read<Vector3D>(r.Miner,"calibratedPosition"));
Assert.Equal(Vector3D.TransformNormal(Vector3D.Forward,inverse),Read<Vector3D>(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<Vector3D>(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<Vector3D>(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<bool>(r.Miner,"calibrated"));
r.Miner.Command("calibrate");Assert.True(Read<bool>(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<bool>(r.Miner,"calibrated"));
}
[Fact]
public void FailedInitializationRetriesAreBoundedAndYieldToThePilot()
{
var r=new MinerRig();int scans=0;
TestRig.SetBase(r.Program,"GridTerminalSystem",new Stub<IMyGridTerminalSystem>().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);
}
}
}
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<T>(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<IMyShipDrill> Drill;
public readonly Stub<IMyShipController> Controller;
public readonly Stub<IMyCameraBlock> Camera;
public readonly Stub<IMyShipConnector> Connector;
public Stub<IMyInventory> CargoInventory;
public Vector3D Position;
public MyDetectedEntityInfo CameraHit;
public readonly List<P.Packet> Sent = new List<P.Packet>();
public MinerRig()
{
Program = TestRig.Program("[Flight]\nDepartureMass=1000\n[Mining]\nFootprintWidth=10\nFootprintHeight=10\n");
TestRig.SetBase(Program,"GridTerminalSystem",new Stub<IMyGridTerminalSystem>().Value);
var listener = new Stub<IMyBroadcastListener>();
var unicast = new Stub<IMyUnicastListener>();
var igc = new Stub<IMyIntergridCommunicationSystem>().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<IMyIntergridCommunicationSystem>)(()=>igc.Value));
Program.Bus = new P.Wire(Program);
Miner = new P.MinerController(Program);
Write(Program,"role",Miner);
Hardware = Read<P.ShipHardware>(Miner,"ship"); Resources = Read<P.Resources>(Miner,"resources");
Controller = new Stub<IMyShipController>().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<List<IMyShipConnector>>(Hardware,"connectors").Add(Connector.Value);
Drill = new Stub<IMyShipDrill>().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("WorldMatrix",MatrixD.Identity);
Hardware.Drills.Add(Drill.Value);
Hardware.Gyros.Add(new Stub<IMyGyro>().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("WorldMatrix",MatrixD.Identity).Value);
Camera = new Stub<IMyCameraBlock>().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<IMyThrust>().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("Enabled",true)
.Set("MaxEffectiveThrust",100000f).Set("WorldMatrix",orientation).Value);
}
Read<List<IMyThrust>>(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<IMyGridTerminalSystem>().Method("GetBlocksOfType",a=>{
var cameras=a[0] as List<IMyCameraBlock>;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<P.FlightState>(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<IMyInventory>().Set("CurrentVolume",(VRage.MyFixedPoint)0).Set("MaxVolume",(VRage.MyFixedPoint)10);
CargoInventory=inventory;
Hardware.InventoryBlocks.Add(new Stub<IMyCargoContainer>().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<string>(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<IMyCameraBlock>().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<P.Job>(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<P.Job>(r.Miner,"job"));Assert.DoesNotContain(r.Sent,packet=>packet.Kind=="RESULT");
Assert.Equal(survey?P.L.MinerWaitSurveyScan:P.L.MinerWaitEntryScan,Read<string>(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<Vector3D>(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<P.Job>(r.Miner,"job").Progress);
Assert.True(Read<bool>(r.Miner,"manualStop"));Assert.True(Read<bool>(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<string>(r.Miner,"reason"));
Assert.Equal(P.FlightState.Departing,r.StateNow);Assert.Equal("valid-lease",Read<string>(r.Miner,"lane"));
Assert.NotNull(Read<P.Job>(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<string>(r.Miner,"reason"));
var flight=Read<P.FlightController>(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<P.DockFrame>(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<P.Job>(r.Miner,"job").Id);
Assert.Equal("",Read<string>(r.Miner,"lane"));Assert.Single(r.Sent,p=>p.Kind=="LANE_RELEASE");
Assert.Equal(P.L.MinerTravelTaskEntry,Read<string>(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<P.DockFrame>(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<bool>(r.Miner,"leaseConfirmed"));Assert.True(Read<bool>(r.Miner,"canceling"));
Call(r.Miner,"Waiting",.1);Assert.Equal(P.FlightState.DockAlign,r.StateNow);
Assert.Contains(P.L.MinerDockOuterEntry,Read<string>(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<string>(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<string>(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<string>(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<P.FlightController>(r.Miner,"flight").ScanClearance);
Assert.All(r.Hardware.Thrusters,t=>Assert.Equal(0,t.ThrustOverridePercentage));
Assert.Contains(P.L.MinerCameraUnavailable,Read<string>(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<string>(r.Miner,"reason"));Assert.Contains(expected,r.Miner.Diagnostics);
Assert.Equal(P.FlightState.Holding,r.StateNow);Assert.Equal(-1d,Read<double>(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<string>(r.Miner,"reason"));Assert.Equal(-1d,Read<double>(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<string>(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<bool>(r.Miner,"canceling"));
r.Miner.Command("resume");r.Miner.Tick(.1);
Assert.Equal(P.FlightState.Servicing,r.StateNow);Assert.False(Read<bool>(r.Miner,"canceling"));
Assert.Null(Read<P.Job>(r.Miner,"job"));Assert.False(Read<bool>(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<MyInventoryItem>)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<P.FlightController>(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<P.Job>(r.Miner,"job").Progress);Assert.Equal("",Read<string>(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<P.Job>(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<P.Job>(r.Miner,"job"));Assert.False(Read<bool>(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<int> 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<P.Job>(reload.Miner,"job"));
receive(11);Assert.Equal("base-job-11",Read<P.Job>(reload.Miner,"job").Id);
reload.Miner.Command("cancel-through 10");Assert.Equal("base-job-11",Read<P.Job>(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<P.Job>(reload.Miner,"job").Progress);Assert.False(Read<bool>(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<bool>(r.Miner,"canceling"));Assert.False(Read<bool>(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<bool>(r.Miner,"canceling"));Assert.False(Read<bool>(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<string>(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<string>(r.Miner,"reason"));
}
static Stub<IMyShipConnector> DockPeer(MinerRig r)
{
var grid=new Stub<IMyCubeGrid>().Set("EntityId",50L);
var peer=new Stub<IMyShipConnector>().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<bool>(r.Miner,"calibrated"));
var inverse=MatrixD.Transpose(peer.Value.WorldMatrix);
Assert.Equal(Vector3D.TransformNormal(r.Position-peer.Value.GetPosition(),inverse),Read<Vector3D>(r.Miner,"calibratedPosition"));
Assert.Equal(Vector3D.TransformNormal(Vector3D.Forward,inverse),Read<Vector3D>(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<Vector3D>(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<Vector3D>(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<bool>(r.Miner,"calibrated"));
r.Miner.Command("calibrate");Assert.True(Read<bool>(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<bool>(r.Miner,"calibrated"));
}
[Fact]
public void FailedInitializationRetriesAreBoundedAndYieldToThePilot()
{
var r=new MinerRig();int scans=0;
TestRig.SetBase(r.Program,"GridTerminalSystem",new Stub<IMyGridTerminalSystem>().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);
}
}
}