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(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().Set("CubeGrid",r.Program.Me.CubeGrid).Set("Enabled",true).Set("ThrustOverridePercentage",.8f); var mother=new Stub().Set("CubeGrid",new Stub().Value).Set("Enabled",true).Set("ThrustOverridePercentage",.9f); var managed=new List{r.Controller.Value,r.Connector.Value}; foreach(var t in r.Hardware.Thrusters) {var definition=t.BlockDefinition;definition.SubtypeName="MockThruster";((Stub)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(managed){extra.Value,mother.Value}; Func,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().Method("GetBlocks",a=>fill(a,managed)); TestRig.SetBase(r.Program,"GridTerminalSystem",new Stub() .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)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 Rays=new List(); public CameraRig(Vector3D direction) { R.Task();Job=Read(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(R.Miner,"flight");R.Hardware.Cameras.Clear(); R.Controller.Method("get_WorldMatrix",a=>Orientation); foreach(var t in R.Hardware.Thrusters) { var proxy=(Stub)RemotingServices.GetRealProxy(t);var local=t.WorldMatrix;proxy.Values.Remove("WorldMatrix");proxy.Method("get_WorldMatrix",a=>local*Orientation); } var gyro=(Stub)RemotingServices.GetRealProxy(R.Hardware.Gyros[0]);gyro.Values.Remove("WorldMatrix");gyro.Method("get_WorldMatrix",a=>Orientation); } public Stub Add(int id,Vector3D direction,Func charged=null,Vector3D offset=default(Vector3D)) { var local=MatrixD.CreateWorld(Vector3D.Zero,direction,P.Data.Perpendicular(direction,Vector3D.Up)); var camera=new Stub().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(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(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(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().Value);r.Add(2,Vector3D.Left); r.Tick();Assert.Empty(r.Rays);r.Orientation=Read(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(r.Flight,"previousDesired")); var hit=r.R.Hardware.Cameras[r.Rays[0]];Assert.True(Vector3D.Dot(hit.WorldMatrix.Forward,Directions[axis])>.999); } } }