XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

AutoMiningScript

【SpaceEngineer】全自动挖矿脚本

公开
关注 0 Fork 0 Star 0
UTF-8
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);
        }
    }
}