using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting;
using Sandbox.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame;
using VRageMath;
using Xunit;
using P=AutoMiningScript.Program;
namespace AutoMiningScript.Tests
{
public class DockPowerCameraTests
{
const BindingFlags Private=BindingFlags.Instance|BindingFlags.NonPublic;
static T Read<T>(object target,string name) => (T)target.GetType().GetField(name,Private).GetValue(target);
static void Write(object target,string name,object value) => target.GetType().GetField(name,Private).SetValue(target,value);
static void Call(object target,string name,params object[] args) => target.GetType().GetMethod(name,Private).Invoke(target,args);
static readonly Vector3D[] Directions={Vector3D.Forward,Vector3D.Backward,Vector3D.Right,Vector3D.Left,Vector3D.Up,Vector3D.Down};
[Theory]
[InlineData(P.FlightState.Servicing)]
[InlineData(P.FlightState.Docked)]
[InlineData(P.FlightState.Ready)]
[InlineData(P.FlightState.Fault)]
[InlineData(P.FlightState.Manual)]
public void EveryConnectedStateSwitchesThrustersOff(P.FlightState state)
{
var r=new MinerStateTests.MinerRig();r.PrepareDeparture("valid-lease");r.State(state);
Write(r.Miner,"running",false);
foreach(var t in r.Hardware.Thrusters)t.ThrustOverridePercentage=.6f;
r.Miner.Tick(.1);
Assert.All(r.Hardware.Thrusters,t=>{Assert.False(t.Enabled);Assert.Equal(0,t.ThrustOverridePercentage);});
r.Hardware.Thrusters[0].Enabled=true;r.Miner.Tick(.1);
Assert.False(r.Hardware.Thrusters[0].Enabled);
}
[Fact]
public void AllOwnGridThrustersParkEvenOutsideControlGroupWithoutTouchingMotherShip()
{
var r=new MinerStateTests.MinerRig();
r.Program.Config=new P.Settings("[Hardware]\nGroup=managed","miner","test");
var extra=new Stub<IMyThrust>().Set("CubeGrid",r.Program.Me.CubeGrid).Set("Enabled",true).Set("ThrustOverridePercentage",.8f);
var mother=new Stub<IMyThrust>().Set("CubeGrid",new Stub<IMyCubeGrid>().Value).Set("Enabled",true).Set("ThrustOverridePercentage",.9f);
var managed=new List<IMyTerminalBlock>{r.Controller.Value,r.Connector.Value};
foreach(var t in r.Hardware.Thrusters)
{var definition=t.BlockDefinition;definition.SubtypeName="MockThruster";((Stub<IMyThrust>)RemotingServices.GetRealProxy(t)).Set("BlockDefinition",definition);}
managed.AddRange(r.Hardware.Thrusters);managed.AddRange(r.Hardware.Gyros);managed.AddRange(r.Hardware.Drills);
managed.AddRange(r.Hardware.Cameras);managed.AddRange(r.Hardware.Batteries);
var all=new List<IMyTerminalBlock>(managed){extra.Value,mother.Value};
Func<object[],IEnumerable<IMyTerminalBlock>,object> fill=(args,blocks)=>{
var target=(IList)args[0];var filter=args.Length>1?args[1] as Delegate:null;
foreach(var b in blocks)if(filter==null || (bool)filter.DynamicInvoke(b))target.Add(b);return null;
};
var group=new Stub<IMyBlockGroup>().Method("GetBlocks",a=>fill(a,managed));
TestRig.SetBase(r.Program,"GridTerminalSystem",new Stub<IMyGridTerminalSystem>()
.Method("GetBlocksOfType",a=>fill(a,all)).Method("GetBlockGroupWithName",a=>group.Value).Value);
r.Connector.Set("Status",MyShipConnectorStatus.Connected);
string error;Assert.True(r.Hardware.Scan(out error),error);
Assert.DoesNotContain(r.Hardware.Thrusters,t=>ReferenceEquals(t,extra.Value));Assert.False(extra.Value.Enabled);Assert.Equal(0,extra.Value.ThrustOverridePercentage);
Assert.True(mother.Value.Enabled);Assert.Equal(.9f,mother.Value.ThrustOverridePercentage);
r.Hardware.PrepareFlight();Assert.True(extra.Value.Enabled);Assert.All(r.Hardware.Thrusters,t=>Assert.True(t.Enabled));
Assert.Equal(.9f,mother.Value.ThrustOverridePercentage);
}
[Fact]
public void DeparturePowersThrustersBeforeCheckingCapabilityAndUnlocking()
{
var r=new MinerStateTests.MinerRig();r.PrepareDeparture("valid-lease");int disconnects=0;
r.Connector.Method("Disconnect",a=>{
Assert.All(r.Hardware.Thrusters,t=>{Assert.True(t.Enabled);Assert.Equal(0,t.ThrustOverridePercentage);});
disconnects++;r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);return null;
});
r.Hardware.ParkThrusters(true);Call(r.Miner,"DockedTick",.1);
Assert.Equal(1,disconnects);Assert.Equal(P.FlightState.Departing,r.StateNow);
}
[Fact]
public void FailedDepartureCapabilityLeavesThrustersOffAndConnectorLocked()
{
var r=new MinerStateTests.MinerRig();r.PrepareDeparture("valid-lease");
foreach(var t in r.Hardware.Thrusters)((Stub<IMyThrust>)RemotingServices.GetRealProxy(t)).Set("MaxEffectiveThrust",0f);
Call(r.Miner,"DockedTick",.1);
Assert.All(r.Hardware.Thrusters,t=>Assert.False(t.Enabled));Assert.Equal(MyShipConnectorStatus.Connected,r.Connector.Value.Status);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void AnUnexpectedDisconnectRestoresParkedThrustersIncludingManualControl(bool pilot)
{
var r=new MinerStateTests.MinerRig();r.PrepareDeparture("valid-lease");Write(r.Miner,"running",false);
r.Miner.Tick(.1);Assert.All(r.Hardware.Thrusters,t=>Assert.False(t.Enabled));
r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);r.Controller.Set("IsUnderControl",pilot);r.Miner.Tick(.1);
Assert.All(r.Hardware.Thrusters,t=>Assert.True(t.Enabled));
if(pilot)Assert.Equal(P.FlightState.Manual,r.StateNow);
else Assert.Contains(r.StateNow,new[]{P.FlightState.Returning,P.FlightState.Holding});
}
[Fact]
public void NormalManualReleasePreservesDisabledThrusters()
{
var r=new MinerStateTests.MinerRig();r.Hardware.Thrusters[0].Enabled=false;r.Miner.Command("release");r.Miner.Tick(.1);
Assert.False(r.Hardware.Thrusters[0].Enabled);Assert.All(r.Hardware.Thrusters,t=>Assert.Equal(0,t.ThrustOverridePercentage));
}
[Fact]
public void AirborneInitializationRestoresThrustersDisabledByAnEarlierDockedProgram()
{
var r=new MinerStateTests.MinerRig();r.State(P.FlightState.Paused);
foreach(var t in r.Hardware.Thrusters)t.Enabled=false;
r.Miner.Tick(.1);Assert.All(r.Hardware.Thrusters,t=>Assert.True(t.Enabled));
}
[Fact]
public void MagneticCaptureParksOnTheSameTick()
{
var r=new MinerStateTests.MinerRig();r.State(P.FlightState.Returning);r.Connector.Set("Status",MyShipConnectorStatus.Connectable);
r.Connector.Method("Connect",a=>{r.Connector.Set("Status",MyShipConnectorStatus.Connected);return null;});
Call(r.Miner,"TryConnect");Assert.All(r.Hardware.Thrusters,t=>Assert.False(t.Enabled));
}
sealed class CameraRig
{
public readonly MinerStateTests.MinerRig R=new MinerStateTests.MinerRig();
public readonly P.FlightController Flight;
public readonly P.Job Job;
public MatrixD Orientation=MatrixD.Identity;
public readonly List<int> Rays=new List<int>();
public CameraRig(Vector3D direction)
{
R.Task();Job=Read<P.Job>(R.Miner,"job");Job.Kind=P.JobKind.Survey;Job.Depth=100;Job.Direction=direction;Job.Up=P.Data.Perpendicular(direction,Vector3D.Up);
R.State(P.FlightState.Survey);Flight=Read<P.FlightController>(R.Miner,"flight");R.Hardware.Cameras.Clear();
R.Controller.Method("get_WorldMatrix",a=>Orientation);
foreach(var t in R.Hardware.Thrusters)
{
var proxy=(Stub<IMyThrust>)RemotingServices.GetRealProxy(t);var local=t.WorldMatrix;proxy.Values.Remove("WorldMatrix");proxy.Method("get_WorldMatrix",a=>local*Orientation);
}
var gyro=(Stub<IMyGyro>)RemotingServices.GetRealProxy(R.Hardware.Gyros[0]);gyro.Values.Remove("WorldMatrix");gyro.Method("get_WorldMatrix",a=>Orientation);
}
public Stub<IMyCameraBlock> Add(int id,Vector3D direction,Func<bool> charged=null,Vector3D offset=default(Vector3D))
{
var local=MatrixD.CreateWorld(Vector3D.Zero,direction,P.Data.Perpendicular(direction,Vector3D.Up));
var camera=new Stub<IMyCameraBlock>().Set("EntityId",(long)id).Set("CubeGrid",R.Program.Me.CubeGrid)
.Set("Enabled",true).Set("IsFunctional",true).Set("IsWorking",true).Set("EnableRaycast",true)
.Set("RaycastConeLimit",45f).Set("RaycastDistanceLimit",-1d).Set("AvailableScanRange",1000d)
.Method("get_WorldMatrix",a=>local*Orientation).Method("GetPosition",a=>R.Position+Vector3D.TransformNormal(offset,Orientation))
.Method("CanScan",a=>{
var ray=Vector3D.TransformNormal((Vector3D)a[0]-R.Position-Vector3D.TransformNormal(offset,Orientation),MatrixD.Transpose(local*Orientation));
return (charged==null || charged()) && -ray.Z>0 && Math.Abs(Math.Atan2(ray.X,-ray.Z))<=Math.PI/4 &&
Math.Abs(Math.Atan2(ray.Y,Math.Sqrt(ray.X*ray.X+ray.Z*ray.Z)))<=Math.PI/4;
}).Method("Raycast",a=>{Rays.Add(id);return default(MyDetectedEntityInfo);});
R.Hardware.Cameras.Add(camera.Value);return camera;
}
public void Tick()
{R.Program.Now+=.1;Call(R.Miner,"Survey",.1);}
}
[Theory]
[InlineData(0)] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] [InlineData(5)]
public void SixDirectionSurveyUsesTheInstalledCameraWithoutRotating(int axis)
{
var r=new CameraRig(Directions[axis]);for(int n=0;n<6;n++)r.Add(n,Directions[n]);r.Tick();
Assert.Equal(new[]{axis},r.Rays);Assert.Equal(MatrixD.Identity,Read<MatrixD>(r.Flight,"previousDesired"));
Assert.Equal("SurveyEmpty",P.JobReport.FromIni(Assert.Single(r.R.Sent,p=>p.Kind=="RESULT").Body).Outcome);
}
[Fact]
public void AChargingSideCameraWaitsWithoutTurningAndAnotherChargedCameraCanTakeItsRay()
{
bool charged=false;var r=new CameraRig(Vector3D.Right);r.Add(1,Vector3D.Right,()=>charged);r.Add(2,Vector3D.Forward);
for(int n=0;n<10;n++)r.Tick();
Assert.Empty(r.Rays);Assert.DoesNotContain(r.R.Sent,p=>p.Kind=="RESULT");Assert.Equal(MatrixD.Identity,Read<MatrixD>(r.Flight,"previousDesired"));
r.Add(3,Vector3D.Right);for(int n=0;n<3;n++)r.Tick();Assert.Equal(new[]{3},r.Rays);
}
[Fact]
public void AWorkingSideMountedCameraIsRotatedToFillAMissingDirection()
{
var r=new CameraRig(Vector3D.Forward);r.Add(1,Vector3D.Right);r.Tick();
Assert.Empty(r.Rays);var desired=Read<MatrixD>(r.Flight,"previousDesired");Assert.NotEqual(MatrixD.Identity,desired);
r.Orientation=desired.GetOrientation();r.Tick();
Assert.Equal(new[]{1},r.Rays);Assert.Equal("SurveyEmpty",P.JobReport.FromIni(Assert.Single(r.R.Sent,p=>p.Kind=="RESULT").Body).Outcome);
}
[Fact]
public void AForeignCameraDoesNotSuppressRotationOfAnActualMinerCamera()
{
var r=new CameraRig(Vector3D.Forward);r.Add(1,Vector3D.Forward).Set("CubeGrid",new Stub<IMyCubeGrid>().Value);r.Add(2,Vector3D.Left);
r.Tick();Assert.Empty(r.Rays);r.Orientation=Read<MatrixD>(r.Flight,"previousDesired").GetOrientation();r.Tick();Assert.Equal(new[]{2},r.Rays);
}
[Theory]
[InlineData(0)] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] [InlineData(5)]
public void SavedMinerSevenCameraLayoutCoversAllAxesWithoutRotating(int axis)
{
// Actual 28 lens positions and optical axes relative to the remote control,
// from the 2026-09-14 14:41:28 save (grid 87777267991379386).
double[,] layout={
{1,1,1,0,1,0},{0,1,1.5,0,0,1},{0,1,0,0,0,-1},{2,0,1,1,0,0},
{0,-.5,-.5,0,0,-1},{0,-1.5,.5,0,0,-1},{-2,0,1,-1,0,0},{1.5,0,2,0,0,1},
{-1.5,0,2,0,0,1},{-1,1,1,0,1,0},{1,1,1.5,0,1,0},{-1,1,1.5,0,1,0},
{.5,-1.5,1.5,0,-1,0},{-.5,-1.5,1.5,0,-1,0},{.5,-1.5,1,0,-1,0},{-.5,-1.5,1,0,-1,0},
{0,-1.5,1.5,0,0,1},{1.5,.5,2,0,0,1},{-1.5,.5,2,0,0,1},{0,0,-.5,0,0,-1},
{-.5,1,.5,0,0,-1},{.5,1,.5,0,0,-1},{-.5,1,1.5,0,0,1},{.5,1,1.5,0,0,1},
{-2,0,.5,-1,0,0},{2,0,.5,1,0,0},{-2,0,1.5,-1,0,0},{2,0,1.5,1,0,0}
};
var r=new CameraRig(Directions[axis]);
for(int n=0;n<layout.GetLength(0);n++)r.Add(n,new Vector3D(layout[n,3],layout[n,4],layout[n,5]),null,new Vector3D(layout[n,0],layout[n,1],layout[n,2]));
r.Tick();Assert.Single(r.Rays);Assert.Equal(MatrixD.Identity,Read<MatrixD>(r.Flight,"previousDesired"));
var hit=r.R.Hardware.Cameras[r.Rays[0]];Assert.True(Vector3D.Dot(hit.WorldMatrix.Forward,Directions[axis])>.999);
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting;
using Sandbox.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame;
using VRageMath;
using Xunit;
using P=AutoMiningScript.Program;
namespace AutoMiningScript.Tests
{
public class DockPowerCameraTests
{
const BindingFlags Private=BindingFlags.Instance|BindingFlags.NonPublic;
static T Read<T>(object target,string name) => (T)target.GetType().GetField(name,Private).GetValue(target);
static void Write(object target,string name,object value) => target.GetType().GetField(name,Private).SetValue(target,value);
static void Call(object target,string name,params object[] args) => target.GetType().GetMethod(name,Private).Invoke(target,args);
static readonly Vector3D[] Directions={Vector3D.Forward,Vector3D.Backward,Vector3D.Right,Vector3D.Left,Vector3D.Up,Vector3D.Down};
[Theory]
[InlineData(P.FlightState.Servicing)]
[InlineData(P.FlightState.Docked)]
[InlineData(P.FlightState.Ready)]
[InlineData(P.FlightState.Fault)]
[InlineData(P.FlightState.Manual)]
public void EveryConnectedStateSwitchesThrustersOff(P.FlightState state)
{
var r=new MinerStateTests.MinerRig();r.PrepareDeparture("valid-lease");r.State(state);
Write(r.Miner,"running",false);
foreach(var t in r.Hardware.Thrusters)t.ThrustOverridePercentage=.6f;
r.Miner.Tick(.1);
Assert.All(r.Hardware.Thrusters,t=>{Assert.False(t.Enabled);Assert.Equal(0,t.ThrustOverridePercentage);});
r.Hardware.Thrusters[0].Enabled=true;r.Miner.Tick(.1);
Assert.False(r.Hardware.Thrusters[0].Enabled);
}
[Fact]
public void AllOwnGridThrustersParkEvenOutsideControlGroupWithoutTouchingMotherShip()
{
var r=new MinerStateTests.MinerRig();
r.Program.Config=new P.Settings("[Hardware]\nGroup=managed","miner","test");
var extra=new Stub<IMyThrust>().Set("CubeGrid",r.Program.Me.CubeGrid).Set("Enabled",true).Set("ThrustOverridePercentage",.8f);
var mother=new Stub<IMyThrust>().Set("CubeGrid",new Stub<IMyCubeGrid>().Value).Set("Enabled",true).Set("ThrustOverridePercentage",.9f);
var managed=new List<IMyTerminalBlock>{r.Controller.Value,r.Connector.Value};
foreach(var t in r.Hardware.Thrusters)
{var definition=t.BlockDefinition;definition.SubtypeName="MockThruster";((Stub<IMyThrust>)RemotingServices.GetRealProxy(t)).Set("BlockDefinition",definition);}
managed.AddRange(r.Hardware.Thrusters);managed.AddRange(r.Hardware.Gyros);managed.AddRange(r.Hardware.Drills);
managed.AddRange(r.Hardware.Cameras);managed.AddRange(r.Hardware.Batteries);
var all=new List<IMyTerminalBlock>(managed){extra.Value,mother.Value};
Func<object[],IEnumerable<IMyTerminalBlock>,object> fill=(args,blocks)=>{
var target=(IList)args[0];var filter=args.Length>1?args[1] as Delegate:null;
foreach(var b in blocks)if(filter==null || (bool)filter.DynamicInvoke(b))target.Add(b);return null;
};
var group=new Stub<IMyBlockGroup>().Method("GetBlocks",a=>fill(a,managed));
TestRig.SetBase(r.Program,"GridTerminalSystem",new Stub<IMyGridTerminalSystem>()
.Method("GetBlocksOfType",a=>fill(a,all)).Method("GetBlockGroupWithName",a=>group.Value).Value);
r.Connector.Set("Status",MyShipConnectorStatus.Connected);
string error;Assert.True(r.Hardware.Scan(out error),error);
Assert.DoesNotContain(r.Hardware.Thrusters,t=>ReferenceEquals(t,extra.Value));Assert.False(extra.Value.Enabled);Assert.Equal(0,extra.Value.ThrustOverridePercentage);
Assert.True(mother.Value.Enabled);Assert.Equal(.9f,mother.Value.ThrustOverridePercentage);
r.Hardware.PrepareFlight();Assert.True(extra.Value.Enabled);Assert.All(r.Hardware.Thrusters,t=>Assert.True(t.Enabled));
Assert.Equal(.9f,mother.Value.ThrustOverridePercentage);
}
[Fact]
public void DeparturePowersThrustersBeforeCheckingCapabilityAndUnlocking()
{
var r=new MinerStateTests.MinerRig();r.PrepareDeparture("valid-lease");int disconnects=0;
r.Connector.Method("Disconnect",a=>{
Assert.All(r.Hardware.Thrusters,t=>{Assert.True(t.Enabled);Assert.Equal(0,t.ThrustOverridePercentage);});
disconnects++;r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);return null;
});
r.Hardware.ParkThrusters(true);Call(r.Miner,"DockedTick",.1);
Assert.Equal(1,disconnects);Assert.Equal(P.FlightState.Departing,r.StateNow);
}
[Fact]
public void FailedDepartureCapabilityLeavesThrustersOffAndConnectorLocked()
{
var r=new MinerStateTests.MinerRig();r.PrepareDeparture("valid-lease");
foreach(var t in r.Hardware.Thrusters)((Stub<IMyThrust>)RemotingServices.GetRealProxy(t)).Set("MaxEffectiveThrust",0f);
Call(r.Miner,"DockedTick",.1);
Assert.All(r.Hardware.Thrusters,t=>Assert.False(t.Enabled));Assert.Equal(MyShipConnectorStatus.Connected,r.Connector.Value.Status);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void AnUnexpectedDisconnectRestoresParkedThrustersIncludingManualControl(bool pilot)
{
var r=new MinerStateTests.MinerRig();r.PrepareDeparture("valid-lease");Write(r.Miner,"running",false);
r.Miner.Tick(.1);Assert.All(r.Hardware.Thrusters,t=>Assert.False(t.Enabled));
r.Connector.Set("Status",MyShipConnectorStatus.Unconnected);r.Controller.Set("IsUnderControl",pilot);r.Miner.Tick(.1);
Assert.All(r.Hardware.Thrusters,t=>Assert.True(t.Enabled));
if(pilot)Assert.Equal(P.FlightState.Manual,r.StateNow);
else Assert.Contains(r.StateNow,new[]{P.FlightState.Returning,P.FlightState.Holding});
}
[Fact]
public void NormalManualReleasePreservesDisabledThrusters()
{
var r=new MinerStateTests.MinerRig();r.Hardware.Thrusters[0].Enabled=false;r.Miner.Command("release");r.Miner.Tick(.1);
Assert.False(r.Hardware.Thrusters[0].Enabled);Assert.All(r.Hardware.Thrusters,t=>Assert.Equal(0,t.ThrustOverridePercentage));
}
[Fact]
public void AirborneInitializationRestoresThrustersDisabledByAnEarlierDockedProgram()
{
var r=new MinerStateTests.MinerRig();r.State(P.FlightState.Paused);
foreach(var t in r.Hardware.Thrusters)t.Enabled=false;
r.Miner.Tick(.1);Assert.All(r.Hardware.Thrusters,t=>Assert.True(t.Enabled));
}
[Fact]
public void MagneticCaptureParksOnTheSameTick()
{
var r=new MinerStateTests.MinerRig();r.State(P.FlightState.Returning);r.Connector.Set("Status",MyShipConnectorStatus.Connectable);
r.Connector.Method("Connect",a=>{r.Connector.Set("Status",MyShipConnectorStatus.Connected);return null;});
Call(r.Miner,"TryConnect");Assert.All(r.Hardware.Thrusters,t=>Assert.False(t.Enabled));
}
sealed class CameraRig
{
public readonly MinerStateTests.MinerRig R=new MinerStateTests.MinerRig();
public readonly P.FlightController Flight;
public readonly P.Job Job;
public MatrixD Orientation=MatrixD.Identity;
public readonly List<int> Rays=new List<int>();
public CameraRig(Vector3D direction)
{
R.Task();Job=Read<P.Job>(R.Miner,"job");Job.Kind=P.JobKind.Survey;Job.Depth=100;Job.Direction=direction;Job.Up=P.Data.Perpendicular(direction,Vector3D.Up);
R.State(P.FlightState.Survey);Flight=Read<P.FlightController>(R.Miner,"flight");R.Hardware.Cameras.Clear();
R.Controller.Method("get_WorldMatrix",a=>Orientation);
foreach(var t in R.Hardware.Thrusters)
{
var proxy=(Stub<IMyThrust>)RemotingServices.GetRealProxy(t);var local=t.WorldMatrix;proxy.Values.Remove("WorldMatrix");proxy.Method("get_WorldMatrix",a=>local*Orientation);
}
var gyro=(Stub<IMyGyro>)RemotingServices.GetRealProxy(R.Hardware.Gyros[0]);gyro.Values.Remove("WorldMatrix");gyro.Method("get_WorldMatrix",a=>Orientation);
}
public Stub<IMyCameraBlock> Add(int id,Vector3D direction,Func<bool> charged=null,Vector3D offset=default(Vector3D))
{
var local=MatrixD.CreateWorld(Vector3D.Zero,direction,P.Data.Perpendicular(direction,Vector3D.Up));
var camera=new Stub<IMyCameraBlock>().Set("EntityId",(long)id).Set("CubeGrid",R.Program.Me.CubeGrid)
.Set("Enabled",true).Set("IsFunctional",true).Set("IsWorking",true).Set("EnableRaycast",true)
.Set("RaycastConeLimit",45f).Set("RaycastDistanceLimit",-1d).Set("AvailableScanRange",1000d)
.Method("get_WorldMatrix",a=>local*Orientation).Method("GetPosition",a=>R.Position+Vector3D.TransformNormal(offset,Orientation))
.Method("CanScan",a=>{
var ray=Vector3D.TransformNormal((Vector3D)a[0]-R.Position-Vector3D.TransformNormal(offset,Orientation),MatrixD.Transpose(local*Orientation));
return (charged==null || charged()) && -ray.Z>0 && Math.Abs(Math.Atan2(ray.X,-ray.Z))<=Math.PI/4 &&
Math.Abs(Math.Atan2(ray.Y,Math.Sqrt(ray.X*ray.X+ray.Z*ray.Z)))<=Math.PI/4;
}).Method("Raycast",a=>{Rays.Add(id);return default(MyDetectedEntityInfo);});
R.Hardware.Cameras.Add(camera.Value);return camera;
}
public void Tick()
{R.Program.Now+=.1;Call(R.Miner,"Survey",.1);}
}
[Theory]
[InlineData(0)] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] [InlineData(5)]
public void SixDirectionSurveyUsesTheInstalledCameraWithoutRotating(int axis)
{
var r=new CameraRig(Directions[axis]);for(int n=0;n<6;n++)r.Add(n,Directions[n]);r.Tick();
Assert.Equal(new[]{axis},r.Rays);Assert.Equal(MatrixD.Identity,Read<MatrixD>(r.Flight,"previousDesired"));
Assert.Equal("SurveyEmpty",P.JobReport.FromIni(Assert.Single(r.R.Sent,p=>p.Kind=="RESULT").Body).Outcome);
}
[Fact]
public void AChargingSideCameraWaitsWithoutTurningAndAnotherChargedCameraCanTakeItsRay()
{
bool charged=false;var r=new CameraRig(Vector3D.Right);r.Add(1,Vector3D.Right,()=>charged);r.Add(2,Vector3D.Forward);
for(int n=0;n<10;n++)r.Tick();
Assert.Empty(r.Rays);Assert.DoesNotContain(r.R.Sent,p=>p.Kind=="RESULT");Assert.Equal(MatrixD.Identity,Read<MatrixD>(r.Flight,"previousDesired"));
r.Add(3,Vector3D.Right);for(int n=0;n<3;n++)r.Tick();Assert.Equal(new[]{3},r.Rays);
}
[Fact]
public void AWorkingSideMountedCameraIsRotatedToFillAMissingDirection()
{
var r=new CameraRig(Vector3D.Forward);r.Add(1,Vector3D.Right);r.Tick();
Assert.Empty(r.Rays);var desired=Read<MatrixD>(r.Flight,"previousDesired");Assert.NotEqual(MatrixD.Identity,desired);
r.Orientation=desired.GetOrientation();r.Tick();
Assert.Equal(new[]{1},r.Rays);Assert.Equal("SurveyEmpty",P.JobReport.FromIni(Assert.Single(r.R.Sent,p=>p.Kind=="RESULT").Body).Outcome);
}
[Fact]
public void AForeignCameraDoesNotSuppressRotationOfAnActualMinerCamera()
{
var r=new CameraRig(Vector3D.Forward);r.Add(1,Vector3D.Forward).Set("CubeGrid",new Stub<IMyCubeGrid>().Value);r.Add(2,Vector3D.Left);
r.Tick();Assert.Empty(r.Rays);r.Orientation=Read<MatrixD>(r.Flight,"previousDesired").GetOrientation();r.Tick();Assert.Equal(new[]{2},r.Rays);
}
[Theory]
[InlineData(0)] [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] [InlineData(5)]
public void SavedMinerSevenCameraLayoutCoversAllAxesWithoutRotating(int axis)
{
// Actual 28 lens positions and optical axes relative to the remote control,
// from the 2026-09-14 14:41:28 save (grid 87777267991379386).
double[,] layout={
{1,1,1,0,1,0},{0,1,1.5,0,0,1},{0,1,0,0,0,-1},{2,0,1,1,0,0},
{0,-.5,-.5,0,0,-1},{0,-1.5,.5,0,0,-1},{-2,0,1,-1,0,0},{1.5,0,2,0,0,1},
{-1.5,0,2,0,0,1},{-1,1,1,0,1,0},{1,1,1.5,0,1,0},{-1,1,1.5,0,1,0},
{.5,-1.5,1.5,0,-1,0},{-.5,-1.5,1.5,0,-1,0},{.5,-1.5,1,0,-1,0},{-.5,-1.5,1,0,-1,0},
{0,-1.5,1.5,0,0,1},{1.5,.5,2,0,0,1},{-1.5,.5,2,0,0,1},{0,0,-.5,0,0,-1},
{-.5,1,.5,0,0,-1},{.5,1,.5,0,0,-1},{-.5,1,1.5,0,0,1},{.5,1,1.5,0,0,1},
{-2,0,.5,-1,0,0},{2,0,.5,1,0,0},{-2,0,1.5,-1,0,0},{2,0,1.5,1,0,0}
};
var r=new CameraRig(Directions[axis]);
for(int n=0;n<layout.GetLength(0);n++)r.Add(n,new Vector3D(layout[n,3],layout[n,4],layout[n,5]),null,new Vector3D(layout[n,0],layout[n,1],layout[n,2]));
r.Tick();Assert.Single(r.Rays);Assert.Equal(MatrixD.Identity,Read<MatrixD>(r.Flight,"previousDesired"));
var hit=r.R.Hardware.Cameras[r.Rays[0]];Assert.True(Vector3D.Dot(hit.WorldMatrix.Forward,Directions[axis])>.999);
}
}
}