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

AutoMiningScript

【SpaceEngineer】全自动挖矿脚本

公开
关注 0 Fork 0 Star 0
UTF-8
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Sandbox.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame.Utilities;
using VRageMath;
using Xunit;
using P=AutoMiningScript.Program;

namespace AutoMiningScript.Tests
{
    public class FleetStateTests
    {
        internal sealed class Rig
        {
            public readonly P Program;
            public readonly P.FleetController Fleet;
            public readonly List<P.Packet> Sent=new List<P.Packet>();
            public readonly List<P.Telemetry> Miners=new List<P.Telemetry>();
            readonly List<Stub<IMyShipConnector>> portStubs=new List<Stub<IMyShipConnector>>(),remoteStubs=new List<Stub<IMyShipConnector>>();
            long sequence;
            public Rig(int count,bool connected,string config="")
            {
                Program=TestRig.Program();Program.Config=new P.Settings("[System]\nId=base\nFleetId=mining\nBaseId=base\n"+config,"fleet","base");
                var receiver=new Stub<IMyBroadcastListener>();var unicast=new Stub<IMyUnicastListener>();
                var igc=new Stub<IMyIntergridCommunicationSystem>().Set("UnicastListener",unicast.Value).Method("RegisterBroadcastListener",a=>receiver.Value)
                    .Method("SendBroadcastMessage",a=>{Capture(a[1] as string);return null;}).Method("SendUnicastMessage",a=>{Capture(a[2] as string);return true;});
                TestRig.SetBase(Program,"IGC",igc.Value);Program.Bus=new P.Wire(Program);
                var controls=new List<IMyTerminalBlock>();var ports=new List<IMyTerminalBlock>();
                controls.Add(new Stub<IMyShipController>().Set("CubeGrid",Program.Me.CubeGrid).Set("IsFunctional",true).Set("WorldMatrix",MatrixD.Identity).Set("CenterOfMass",Vector3D.Zero)
                    .Method("GetPosition",a=>Vector3D.Zero).Method("GetShipVelocities",a=>new MyShipVelocities(Vector3D.Zero,Vector3D.Zero)).Value);
                for(int n=0;n<count;n++)
                {
                    Vector3D position=new Vector3D(n*40,0,0);MatrixD matrix=MatrixD.CreateWorld(position,Vector3D.Forward,Vector3D.Up);
                    var grid=new Stub<IMyCubeGrid>().Set("EntityId",1000L+n);
                    var remote=new Stub<IMyShipConnector>().Set("CubeGrid",grid.Value).Set("EntityId",2000L+n).Set("Status",connected?MyShipConnectorStatus.Connected:MyShipConnectorStatus.Unconnected);
                    var port=new Stub<IMyShipConnector>().Set("CubeGrid",Program.Me.CubeGrid).Set("EntityId",100L+n).Set("IsFunctional",true).Set("WorldMatrix",matrix)
                        .Set("Status",connected?MyShipConnectorStatus.Connected:MyShipConnectorStatus.Unconnected).Set("OtherConnector",connected?remote.Value:null).Method("GetPosition",a=>position);
                    ports.Add(port.Value);portStubs.Add(port);remoteStubs.Add(remote);
                    Miners.Add(new P.Telemetry {Id="miner-"+n,State=P.FlightState.Servicing,Position=position,Connected=connected,ConnectorId=2000L+n,PeerConnectorId=100L+n,Battery=1,Hydrogen=1,Radius=3,StoredMWh=1,CapacityMWh=1});
                }
                var group=new Stub<IMyBlockGroup>().Method("GetBlocksOfType",a=>Fill(a,ports));
                var terminal=new Stub<IMyGridTerminalSystem>().Method("GetBlockGroupWithName",a=>(string)a[0]=="AMS Docks"?group.Value:null).Method("GetBlocksOfType",a=>Fill(a,controls));
                TestRig.SetBase(Program,"GridTerminalSystem",terminal.Value);Fleet=new P.FleetController(Program);
            }
            static object Fill(object[] args,List<IMyTerminalBlock> blocks)
            {
                var list=(IList)args[0];var itemType=list.GetType().GetGenericArguments()[0];Delegate filter=args.Length>1?args[1] as Delegate:null;
                foreach(var block in blocks)if(itemType.IsInstanceOfType(block)&&(filter==null||(bool)filter.DynamicInvoke(block)))list.Add(block);return null;
            }
            void Capture(string value) {P.Packet packet;if(P.Packet.TryDecode(value,"mining",out packet))Sent.Add(packet);}
            public void Receive(string kind,P.Telemetry telemetry,long? source=null)
            {
                int index=Miners.IndexOf(telemetry);Fleet.Receive(new P.Packet {Kind=kind,From=telemetry.Id,Source=source??index+5000,Id="packet-"+(++sequence),Session="miner-session",Seq=sequence,Body=telemetry.ToIni()});
            }
            public void RegisterAll() {foreach(var miner in Miners)Receive("HELLO",miner);}
            public void SetConnected(int index,bool connected)
            {
                portStubs[index].Set("Status",connected?MyShipConnectorStatus.Connected:MyShipConnectorStatus.Unconnected).Set("OtherConnector",connected?remoteStubs[index].Value:null);
                remoteStubs[index].Set("Status",connected?MyShipConnectorStatus.Connected:MyShipConnectorStatus.Unconnected);Miners[index].Connected=connected;
            }
            public void SetPort(int index,Vector3D position,Vector3D forward)
            {
                portStubs[index].Set("WorldMatrix",MatrixD.CreateWorld(position,forward,DataUp(forward))).Method("GetPosition",a=>position);
            }
            static Vector3D DataUp(Vector3D forward) {return P.Data.Perpendicular(forward,Vector3D.Up);}
            public void Tick(int steps) {for(int n=0;n<steps;n++){Program.Now+=0.1;Fleet.Tick(0.1);}}
            public string Checkpoint()
            {
                string value=null;int frames=0;while(!Fleet.CheckpointStorage(out value)&&frames++<2000){}Assert.NotNull(value);TestRig.SetBase(Program,"Storage",value);return value;
            }
            public void Restore(string value)
            {
                TestRig.SetBase(Program,"Storage",value);Assert.True(Fleet.BeginRestoreStorage(value));int frames=0;while(Fleet.Restoring&&frames++<2000)Fleet.RestoreStorageStep();Assert.False(Fleet.Restoring);
            }
            public MyIni Save() {var ini=new MyIni();Fleet.Save(ini);return ini;}
            public P.TaskPlanner Planner {get{return (P.TaskPlanner)typeof(P.FleetController).GetField("planner",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic).GetValue(Fleet);}}
        }

        [Fact]
        public void SixteenMinersReceiveDistinctNonIntersectingReservedHoles()
        {
            var rig=new Rig(16,true);rig.RegisterAll();rig.Fleet.Command("mark 300 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(45);
            var owned=rig.Fleet.GetJobs().Where(j=>j.Owner.Length>0).ToList();Assert.Equal(16,owned.Count);Assert.Equal(16,owned.Select(j=>j.Owner).Distinct().Count());
            for(int i=0;i<owned.Count;i++)for(int k=i+1;k<owned.Count;k++)
                Assert.True(P.Data.SegmentDistance(owned[i].Entry-owned[i].Direction*15,owned[i].Entry+owned[i].Direction*20,owned[k].Entry-owned[k].Direction*15,owned[k].Entry+owned[k].Direction*20)>=8);
            var unknown=new P.Telemetry {Id="miner-17",Battery=1,Hydrogen=1};rig.Receive("HELLO",unknown);Assert.Equal(16,rig.Fleet.GetTelemetry().Count);
        }
        [Fact]
        public void IndependentPortPrioritizesUrgentReturnAndRetainsTheOtherUnknownOccupancy()
        {
            var rig=new Rig(2,false);rig.RegisterAll();rig.Tick(30);rig.Miners[0].Battery=.9;rig.Miners[1].Battery=.1;
            rig.Receive("DOCK_REQUEST",rig.Miners[0]);rig.Receive("DOCK_REQUEST",rig.Miners[1]);rig.Tick(12);
            P.Packet granted=Assert.Single(rig.Sent,p=>p.Kind=="DOCK_GRANT");Assert.Equal("miner-1",granted.Target);
            rig.Sent.Clear();rig.Tick(600);rig.Receive("DOCK_REQUEST",rig.Miners[0]);rig.Tick(12);
            var second=rig.Sent.First(p=>p.Kind=="DOCK_GRANT");Assert.Equal("miner-0",second.Target);
            Assert.NotEqual(P.DockFrame.FromIni(granted.Body).ConnectorId,P.DockFrame.FromIni(second.Body).ConnectorId);
            Assert.Equal(2,P.Data.ReadLong(rig.Save(),"fleet","LaneCount"));Assert.Contains("miner-1",rig.Fleet.Diagnostics);
        }
        [Fact]
        public void RestartRetainsOccupancyButNeedsFreshRequestAndNewSessionToken()
        {
            var before=new Rig(1,false);before.RegisterAll();before.Tick(30);before.Receive("DOCK_REQUEST",before.Miners[0]);before.Tick(12);
            string oldToken=P.DockFrame.FromIni(before.Sent.First(p=>p.Kind=="DOCK_GRANT").Body).Token;var saved=new MyIni();before.Fleet.Save(saved);
            var after=new Rig(1,false);after.Fleet.Load(saved);after.RegisterAll();after.Tick(30);
            Assert.DoesNotContain(after.Sent,p=>p.Kind=="DOCK_GRANT");Assert.All(after.Sent.Where(p=>p.Kind=="DOCKPOSE"),p=>Assert.Equal("",P.DockFrame.FromIni(p.Body).Token));
            after.Receive("DOCK_REQUEST",after.Miners[0]);after.Tick(12);
            string renewed=P.DockFrame.FromIni(after.Sent.First(p=>p.Kind=="DOCK_GRANT").Body).Token;
            Assert.NotEqual(oldToken,renewed);Assert.Contains(after.Program.Bus.Session,renewed);
        }
        [Fact]
        public void MarkReceiptsRemainIdempotentAcrossFleetReload()
        {
            var before=new Rig(0,false);var body=new MyIni();body.Set("mark","Entry","0,0,0");body.Set("mark","Direction","0,0,-1");body.Set("mark","Up","0,1,0");body.Set("mark","Width",6);body.Set("mark","Height",6);body.Set("mark","Depth",10);
            var packet=new P.Packet {Kind="MARK",From="marker",Id="marker-session-1",Session="marker-session",Seq=1,Body=body};before.Fleet.Receive(packet);before.Fleet.Receive(packet);Assert.Single(before.Fleet.GetJobs());
            var saved=new MyIni();before.Fleet.Save(saved);var after=new Rig(0,false);after.Fleet.Load(saved);after.Fleet.Receive(packet);Assert.Single(after.Fleet.GetJobs());
        }
        [Fact]
        public void RecordStorageRestoresLargePlannerIncrementallyWithUnknownOwnership()
        {
            var before=new Rig(1,true);before.RegisterAll();before.Fleet.Command("survey 2000");before.Fleet.Command("start");before.Tick(40);
            string storage=null;int frames=0;while(!before.Fleet.CheckpointStorage(out storage)&&frames++<500){}Assert.InRange(frames,1,499);Assert.StartsWith("XFEAMS-FLEET-3\n",storage);
            var after=new Rig(1,true);Assert.True(after.Fleet.BeginRestoreStorage(storage));after.Fleet.RestoreStorageStep();Assert.True(after.Fleet.Restoring);
            frames=0;while(after.Fleet.Restoring&&frames++<500)after.Fleet.RestoreStorageStep();Assert.False(after.Fleet.Restoring);
            Assert.Equal(before.Fleet.GetJobs().Count,after.Fleet.GetJobs().Count);Assert.Equal(before.Fleet.GetJobs().Select(j=>j.Owner),after.Fleet.GetJobs().Select(j=>j.Owner));Assert.All(after.Fleet.GetTelemetry(),t=>Assert.True(t.ReceivedAt<0));
        }
        [Fact]
        public void RecordStorageResumesIncompleteGenerationExactlyOnce()
        {
            var before=new Rig(0,false);before.Fleet.Command("survey 2000");Assert.Equal(8,before.Fleet.GetJobs().Count);
            string storage=null;int frames=0;while(!before.Fleet.CheckpointStorage(out storage)&&frames++<500){}
            var after=new Rig(0,false);after.Fleet.BeginRestoreStorage(storage);while(after.Fleet.Restoring)after.Fleet.RestoreStorageStep();before.Tick(40);after.Tick(40);
            Assert.Equal(294,after.Fleet.GetJobs().Count);Assert.Equal(before.Fleet.GetJobs().Select(j=>j.Id),after.Fleet.GetJobs().Select(j=>j.Id));Assert.Equal(before.Fleet.GetJobs().Select(j=>j.Entry),after.Fleet.GetJobs().Select(j=>j.Entry));
        }
        [Fact]
        public void RecordStorageRejectsTruncationAndIdentityMismatch()
        {
            var before=new Rig(0,false);before.Fleet.Command("survey 2000");string storage=null;int frames=0;while(!before.Fleet.CheckpointStorage(out storage)&&frames++<500){}
            var after=new Rig(0,false);Assert.True(after.Fleet.BeginRestoreStorage(storage.Substring(0,storage.Length-1)));Assert.Throws<ArgumentException>(()=>{while(after.Fleet.Restoring)after.Fleet.RestoreStorageStep();});
            after=new Rig(0,false);after.Program.Config=new P.Settings("[System]\nId=other\nFleetId=mining\n","fleet","other");after.Fleet.BeginRestoreStorage(storage);Assert.Throws<ArgumentException>(()=>after.Fleet.RestoreStorageStep());
        }
        [Fact]
        public void FleetCheckpointIncludesFreshOwnershipAndCanRestoreFleet()
        {
            var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 300 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");
            var checkpoint=new MyIni();Assert.False(rig.Fleet.Checkpoint(checkpoint));rig.Tick(2);int frames=0;while(!rig.Fleet.Checkpoint(checkpoint)&&frames++<100){}
            Assert.True(frames<100);var restored=new Rig(1,true);restored.Fleet.Load(checkpoint);Assert.Equal(rig.Fleet.GetJobs().Count,restored.Fleet.GetJobs().Count);Assert.Single(restored.Fleet.GetJobs(),j=>j.Owner=="miner-0");
        }
        [Fact]
        public void ClearTasksImmediatelyCommitsEmptyPlannerAndIndependentAirborneGeometry()
        {
            var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(2);
            P.Job job=Assert.Single(rig.Fleet.GetJobs());Assert.Equal("miner-0",job.Owner);
            rig.SetConnected(0,false);rig.Miners[0].JobId=job.Id;rig.Miners[0].State=P.FlightState.Drilling;rig.Miners[0].Position=job.Entry+job.Direction*5;rig.Receive("TELEMETRY",rig.Miners[0]);
            rig.Sent.Clear();rig.Fleet.Command("clear-tasks");Assert.Empty(rig.Fleet.GetJobs());Assert.Equal(1,rig.Fleet.CancellationCount);Assert.Single(rig.Fleet.GetTelemetry());
            Assert.Contains(rig.Sent,p=>p.Kind=="COMMAND"&&p.Body.Get("command","Text").ToString().StartsWith("cancel-through "));
            Assert.StartsWith("XFEAMS-FLEET-3\n",rig.Program.Storage);
            var after=new Rig(1,false);after.Fleet.BeginRestoreStorage(rig.Program.Storage);int frames=0;while(after.Fleet.Restoring&&frames++<200)after.Fleet.RestoreStorageStep();
            Assert.False(after.Fleet.Restoring);Assert.Empty(after.Fleet.GetJobs());Assert.Equal(1,after.Fleet.CancellationCount);Assert.Single(after.Fleet.GetTelemetry());
            var saved=new MyIni();after.Fleet.Save(saved);Assert.Equal(0,P.Data.ReadLong(saved,"planner","Jobs"));Assert.Equal(1,P.Data.ReadLong(saved,"fleet","ClearanceCount"));
            var geometry=new MyIni();Assert.True(geometry.TryParse(saved.Get("fleet","ClearanceData0").ToString()));P.Job held=P.Job.FromIni(geometry);Assert.Equal(job.Entry,held.Entry);Assert.Equal(job.Id,held.Id);Assert.Equal(job.Direction,held.Direction);
            after.RegisterAll();Assert.Contains(after.Sent,p=>p.Kind=="COMMAND"&&p.Body.Get("command","Text").ToString().StartsWith("cancel-through "));
        }
        [Fact]
        public void ClearRetainsPortLaneAndRequiresAppliedWatermarkPlusPhysicalDock()
        {
            var rig=new Rig(1,false);rig.RegisterAll();rig.Tick(30);rig.Receive("DOCK_REQUEST",rig.Miners[0]);rig.Tick(12);
            rig.Fleet.Command("clear-all");var state=new MyIni();rig.Fleet.Save(state);Assert.Equal("miner-0",state.Get("fleet","LaneOwner").ToString());long watermark=P.Data.ReadLong(state,"fleet","CancelledThrough");
            rig.Miners[0].JobId="";rig.Receive("TELEMETRY",rig.Miners[0]);Assert.Equal(1,rig.Fleet.CancellationCount);
            rig.Miners[0].CancelledThrough=watermark;rig.Receive("TELEMETRY",rig.Miners[0]);Assert.Equal(1,rig.Fleet.CancellationCount);
            rig.SetConnected(0,true);rig.Receive("TELEMETRY",rig.Miners[0]);Assert.Equal(0,rig.Fleet.CancellationCount);state.Clear();rig.Fleet.Save(state);Assert.Equal("",state.Get("fleet","LaneOwner").ToString());
        }
        [Fact]
        public void CancellationRetriesWithNewSequenceAndBlocksNewDispatchUntilConfirmed()
        {
            var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(2);string oldId=rig.Fleet.GetJobs()[0].Id;
            rig.Sent.Clear();rig.Fleet.Command("clear-tasks");var cleared=new MyIni();rig.Fleet.Save(cleared);long watermark=P.Data.ReadLong(cleared,"fleet","CancelledThrough");
            rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-300: GPS:inward:0:0:-301:");rig.Fleet.Command("start");rig.Tick(25);rig.Program.Bus.Tick();
            Assert.DoesNotContain(rig.Sent,p=>p.Kind=="JOB");var cancels=rig.Sent.Where(p=>p.Kind=="COMMAND"&&p.Body.Get("command","Text").ToString().StartsWith("cancel-through ")).ToList();Assert.True(cancels.Select(p=>p.Seq).Distinct().Count()>=2);
            rig.Miners[0].CancelledThrough=watermark;rig.Miners[0].JobId="";rig.Receive("TELEMETRY",rig.Miners[0]);Assert.Equal(0,rig.Fleet.CancellationCount);rig.Tick(4);
            var assigned=Assert.Single(rig.Fleet.GetJobs());Assert.NotEqual(oldId,assigned.Id);Assert.Equal("miner-0",assigned.Owner);
        }
        [Fact]
        public void ClearKeepsExistingCompletedHoldAndNewOwnedHoleForTheSameMiner()
        {
            var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(2);P.Job first=rig.Fleet.GetJobs()[0];
            rig.Fleet.Receive(new P.Packet {Kind="RESULT",From="miner-0",Source=5000,Body=new P.JobReport {JobId=first.Id,Outcome="Complete",Progress=20}.ToIni()});
            rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-400: GPS:inward:0:0:-401:");P.Job second=rig.Fleet.GetJobs().Last();
            var planner=(P.TaskPlanner)typeof(P.FleetController).GetField("planner",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic).GetValue(rig.Fleet);planner.SetOwner(second,"miner-0");
            rig.SetConnected(0,false);rig.Miners[0].JobId=second.Id;rig.Miners[0].Position=second.Entry;rig.Receive("TELEMETRY",rig.Miners[0]);rig.Fleet.Command("clear-tasks");
            var saved=new MyIni();rig.Fleet.Save(saved);Assert.Equal(2,P.Data.ReadLong(saved,"fleet","ClearanceCount"));Assert.Empty(rig.Fleet.GetJobs());
            var restored=new Rig(1,false);restored.Fleet.BeginRestoreStorage(rig.Program.Storage);while(restored.Fleet.Restoring)restored.Fleet.RestoreStorageStep();saved.Clear();restored.Fleet.Save(saved);Assert.Equal(2,P.Data.ReadLong(saved,"fleet","ClearanceCount"));
        }
        [Fact]
        public void CancelledDepartureCanTurnItsReservedLaneIntoReturnWithoutReleasingIt()
        {
            var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(30);
            rig.Miners[0].JobId=rig.Fleet.GetJobs()[0].Id;rig.Receive("DEPART_REQUEST",rig.Miners[0]);rig.Tick(12);
            string outbound=P.DockFrame.FromIni(rig.Sent.First(p=>p.Kind=="DEPART_GRANT").Body).Token;
            rig.SetConnected(0,false);rig.Miners[0].Position=new Vector3D(0,0,-20);rig.Miners[0].State=P.FlightState.Departing;rig.Receive("TELEMETRY",rig.Miners[0]);rig.Fleet.Command("clear-tasks");rig.Receive("DOCK_REQUEST",rig.Miners[0]);
            P.DockFrame inbound=P.DockFrame.FromIni(rig.Sent.Last(p=>p.Kind=="DOCK_GRANT").Body);Assert.Equal("miner-0",inbound.Owner);Assert.NotEqual(outbound,inbound.Token);Assert.Equal(1,rig.Fleet.CancellationCount);
        }
        static string BeginDeparture(Rig rig)
        {
            rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(30);
            rig.Miners[0].JobId=rig.Fleet.GetJobs()[0].Id;rig.Receive("DEPART_REQUEST",rig.Miners[0]);rig.Tick(12);
            string token=P.DockFrame.FromIni(rig.Sent.First(p=>p.Kind=="DEPART_GRANT").Body).Token;
            rig.SetConnected(0,false);rig.Miners[0].State=P.FlightState.Departing;return token;
        }
        [Fact]
        public void OutboundGrantSurvivesClearanceBoundaryUntilMinerCompletesDeparture()
        {
            var rig=new Rig(1,true);string token=BeginDeparture(rig);
            // Fleet clearance is 63 m, while the actual outbound target is 65 m.
            foreach(double distance in new[]{63.2,65.0})
            {
                rig.Miners[0].Position=new Vector3D(0,0,-distance);rig.Receive("TELEMETRY",rig.Miners[0]);rig.Sent.Clear();rig.Tick(12);
                Assert.Equal("miner-0",rig.Save().Get("fleet","LaneOwner").ToString());
                Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString());
                Assert.Contains(rig.Sent,p=>p.Kind=="DOCKPOSE");
                Assert.All(rig.Sent.Where(p=>p.Kind=="DOCKPOSE"),p=>Assert.Equal(token,P.DockFrame.FromIni(p.Body).Token));
            }
            rig.Miners[0].State=P.FlightState.Transit;rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12);
            Assert.Equal("",rig.Save().Get("fleet","LaneOwner").ToString());
        }
        [Theory]
        [InlineData(P.FlightState.Survey)]
        [InlineData(P.FlightState.Align)]
        [InlineData(P.FlightState.Drilling)]
        [InlineData(P.FlightState.Retreat)]
        public void PostDeparturePhaseCanReconcileAReleaseWithoutObservingShortTransit(P.FlightState phase)
        {
            var rig=new Rig(1,true);BeginDeparture(rig);rig.Miners[0].Position=new Vector3D(0,0,-100);rig.Miners[0].State=phase;
            rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12);Assert.Equal("",rig.Save().Get("fleet","LaneOwner").ToString());
        }
        [Theory]
        [InlineData(P.FlightState.Departing)]
        [InlineData(P.FlightState.DockRetreat)]
        [InlineData(P.FlightState.Returning)]
        [InlineData(P.FlightState.Paused)]
        [InlineData(P.FlightState.Manual)]
        public void InterruptedDepartureRetainsItsCorridorEvenBeyondClearance(P.FlightState phase)
        {
            var rig=new Rig(1,true);string token=BeginDeparture(rig);rig.Miners[0].Position=new Vector3D(0,0,-100);rig.Miners[0].State=phase;
            rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12);Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString());
            rig.Tick(600);Assert.Equal("miner-0",rig.Save().Get("fleet","LaneOwner").ToString());
        }
        [Fact]
        public void PostDeparturePhaseAlsoRequiresFreshTelemetryAndPhysicalClearance()
        {
            var rig=new Rig(1,true);string token=BeginDeparture(rig);rig.Miners[0].State=P.FlightState.Transit;
            rig.Miners[0].Position=new Vector3D(0,0,-62);rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12);
            Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString());
            rig.Miners[0].Position=new Vector3D(0,0,-100);rig.Miners[0].Connected=true;rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12);
            Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString());
            rig.Miners[0].Connected=false;rig.Receive("TELEMETRY",rig.Miners[0]);rig.Program.Now+=4;rig.Tick(12);
            Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString());
            rig.Receive("TELEMETRY",rig.Miners[0]);rig.Tick(12);Assert.Equal("",rig.Save().Get("fleet","LaneOwner").ToString());
        }
        [Fact]
        public void ExplicitOutboundReleaseStillRequiresItsTokenAndFreshSafeDistance()
        {
            var rig=new Rig(1,true);string token=BeginDeparture(rig);var body=new MyIni();body.Set("lease","Token",token);
            var release=new P.Packet {Kind="LANE_RELEASE",From="miner-0",Source=5000,Body=body};
            rig.Miners[0].Position=new Vector3D(0,0,-20);rig.Receive("TELEMETRY",rig.Miners[0]);rig.Fleet.Receive(release);
            Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString());
            rig.Miners[0].Position=new Vector3D(0,0,-65);rig.Receive("TELEMETRY",rig.Miners[0]);body.Set("lease","Token","obsolete");rig.Fleet.Receive(release);
            Assert.Equal(token,rig.Save().Get("fleet","LaneToken").ToString());
            body.Set("lease","Token",token);rig.Fleet.Receive(release);Assert.Equal("",rig.Save().Get("fleet","LaneOwner").ToString());
        }
        [Fact]
        public void ConfirmedMinerCanTakeNewWorkAndFreeItsPortWhileAnotherAwaitsCancellation()
        {
            var rig=new Rig(2,true);rig.SetConnected(1,false);rig.RegisterAll();rig.Fleet.Command("clear-tasks");
            var saved=new MyIni();rig.Fleet.Save(saved);long watermark=P.Data.ReadLong(saved,"fleet","CancelledThrough");
            rig.Miners[0].CancelledThrough=watermark;rig.Miners[0].JobId="";rig.Receive("TELEMETRY",rig.Miners[0]);Assert.Equal(1,rig.Fleet.CancellationCount);
            rig.Sent.Clear();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(30);
            Assert.Equal("miner-0",Assert.Single(rig.Fleet.GetJobs()).Owner);
            Assert.DoesNotContain(rig.Sent,p=>p.Kind=="COMMAND"&&p.Target=="miner-1"&&p.Body.Get("command","Text").ToString()=="resume");
            rig.Miners[0].JobId=rig.Fleet.GetJobs()[0].Id;rig.Receive("DEPART_REQUEST",rig.Miners[0]);rig.Tick(12);
            Assert.Contains(rig.Sent,p=>p.Kind=="DEPART_GRANT"&&p.Target=="miner-0");Assert.Equal(1,rig.Fleet.CancellationCount);
        }
        [Fact]
        public void ClearPreservesUnregisteredLegacyOwnerGeometryWithoutExpandingRegisteredLedger()
        {
            var rig=new Rig(16,false);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");
            var planner=(P.TaskPlanner)typeof(P.FleetController).GetField("planner",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic).GetValue(rig.Fleet);planner.SetOwner(rig.Fleet.GetJobs()[0],"legacy-offline");
            rig.Fleet.Command("clear-tasks");Assert.Equal(16,rig.Fleet.CancellationCount);
            var restored=new Rig(16,false);restored.Fleet.BeginRestoreStorage(rig.Program.Storage);while(restored.Fleet.Restoring)restored.Fleet.RestoreStorageStep();var saved=new MyIni();restored.Fleet.Save(saved);
            Assert.Equal(16,restored.Fleet.CancellationCount);Assert.Equal(1,P.Data.ReadLong(saved,"fleet","ClearanceCount"));Assert.Equal("legacy-offline",saved.Get("fleet","ClearanceOwner0").ToString());
        }
        [Theory]
        [InlineData(5)]
        [InlineData(12)]
        public void ClearOfflineUsesStrictConfiguredAgeAndLeavesOnlineTelemetryUntouched(int seconds)
        {
            var rig=new Rig(2,false,"[Display]\nOfflineSeconds="+seconds+"\n");rig.RegisterAll();rig.Program.Now=seconds;
            rig.Fleet.Command("clear-offline");Assert.Equal(2,rig.Fleet.GetTelemetry().Count);
            rig.Receive("TELEMETRY",rig.Miners[1]);rig.Program.Now=seconds+.01;rig.Fleet.Command("remove-offline");
            var online=Assert.Single(rig.Fleet.GetTelemetry());Assert.Equal("miner-1",online.Id);Assert.Equal(seconds,online.ReceivedAt);
            rig.Receive("HELLO",rig.Miners[0]);Assert.Equal(2,rig.Fleet.GetTelemetry().Count);
        }
        [Fact]
        public void ClearOfflineJournalSurvivesImmediateRestartAndDoesNotRestoreDeletedRegistration()
        {
            var before=new Rig(2,false);before.RegisterAll();string baseline=before.Checkpoint();before.Program.Now=6;before.Receive("TELEMETRY",before.Miners[1]);
            before.Fleet.Command("clear-offline");Assert.StartsWith(baseline,before.Program.Storage);Assert.Contains("RemoveMiners",before.Program.Storage);
            var after=new Rig(2,false);after.Restore(before.Program.Storage);Assert.Equal("miner-1",Assert.Single(after.Fleet.GetTelemetry()).Id);
            Assert.Equal(2,P.Data.ReadLong(after.Save(),"fleet","NextWaitingSlot"));
            after.Receive("HELLO",after.Miners[0]);Assert.Equal(2,after.Fleet.GetTelemetry().Count);Assert.Equal(2,after.Fleet.GetTelemetry().Single(t=>t.Id=="miner-0").WaitingSlot);
        }
        [Fact]
        public void RemovalInvalidatesPartialCheckpointWithoutDroppingOnlineJobOrMovingItsWaitPoint()
        {
            var rig=new Rig(2,true);rig.RegisterAll();rig.Fleet.Command("survey 2000");rig.Fleet.Command("start");rig.Tick(30);rig.Checkpoint();
            P.Job live=rig.Planner.OwnedJobs["miner-1"];int slot=rig.Fleet.GetTelemetry().Single(t=>t.Id=="miner-1").WaitingSlot;
            string partial;Assert.False(rig.Fleet.CheckpointStorage(out partial));rig.Program.Now=10;rig.Receive("TELEMETRY",rig.Miners[1]);rig.Fleet.Command("clear-offline");
            Assert.Same(live,rig.Planner.OwnedJobs["miner-1"]);Assert.Equal(slot,Assert.Single(rig.Fleet.GetTelemetry()).WaitingSlot);
            var after=new Rig(2,false);after.Restore(rig.Checkpoint());Assert.Equal("miner-1",Assert.Single(after.Fleet.GetTelemetry()).Id);Assert.Equal(live.Id,after.Planner.OwnedJobs["miner-1"].Id);
        }
        [Fact]
        public void OfflineRemovalSnapshotsNewUncheckpointedGeometryLaneAndSerial()
        {
            var before=new Rig(1,true);before.RegisterAll();before.Checkpoint();before.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");before.Fleet.Command("start");before.Tick(30);
            P.Job owned=Assert.Single(before.Fleet.GetJobs());before.Miners[0].JobId=owned.Id;before.Receive("DEPART_REQUEST",before.Miners[0]);before.Tick(12);
            Assert.Contains(before.Sent,p=>p.Kind=="DEPART_GRANT");before.SetConnected(0,false);before.Miners[0].Position=new Vector3D(0,0,-20);before.Receive("TELEMETRY",before.Miners[0]);
            long serial=before.Planner.TaskSerial;before.Program.Now+=6;before.Fleet.Command("clear-offline");Assert.Empty(before.Fleet.GetTelemetry());Assert.Equal("miner-0",owned.Owner);
            var after=new Rig(1,false);after.Restore(before.Program.Storage);var saved=after.Save();Assert.Empty(after.Fleet.GetTelemetry());Assert.Empty(after.Fleet.GetJobs());
            Assert.Equal("miner-0",saved.Get("fleet","LaneOwner").ToString());Assert.Equal("",saved.Get("fleet","LaneToken").ToString());Assert.Equal(1,P.Data.ReadLong(saved,"fleet","ClearanceCount"));
            var body=new MyIni();Assert.True(body.TryParse(saved.Get("fleet","ClearanceData0").ToString()));Assert.Equal(owned.Entry,P.Job.FromIni(body).Entry);Assert.Equal(serial,after.Planner.TaskSerial);
            Assert.Equal(1,P.Data.ReadLong(saved,"fleet","ProtectedOwnerCount"));after.Receive("HELLO",after.Miners[0],7000);Assert.Empty(after.Fleet.GetTelemetry());
            after.Receive("HELLO",after.Miners[0],5000);Assert.Single(after.Fleet.GetTelemetry());Assert.DoesNotContain(after.Sent,p=>p.Kind=="DEPART_GRANT"||p.Kind=="DOCK_GRANT");
        }
        [Fact]
        public void SixteenDeletedOwnersDoNotAllowAnotherSixteenOwnershipsOrInvalidStorage()
        {
            var rig=new Rig(16,true);rig.RegisterAll();rig.Fleet.Command("mark 300 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(45);Assert.Equal(16,rig.Planner.OwnedJobs.Count);
            rig.Checkpoint();rig.Program.Now+=6;rig.Fleet.Command("clear-offline");Assert.Empty(rig.Fleet.GetTelemetry());
            for(int n=0;n<16;n++) {rig.Miners[n].Id="renamed-"+n;rig.Receive("HELLO",rig.Miners[n],7000+n);}rig.Tick(20);
            Assert.Equal(16,rig.Fleet.GetTelemetry().Count);Assert.Equal(16,rig.Planner.OwnedJobs.Count);Assert.DoesNotContain(rig.Planner.OwnedJobs.Keys,id=>id.StartsWith("renamed-"));
            var after=new Rig(16,false);after.Restore(rig.Checkpoint());Assert.Equal(16,after.Planner.OwnedJobs.Count);Assert.Equal(16,after.Fleet.GetTelemetry().Count);
        }
        [Fact]
        public void WaitingSlotsIgnoreClientValuesRemainStableAndNeverReuseDeletedSlotsAfterRestart()
        {
            var rig=new Rig(3,false);foreach(var t in rig.Miners)t.WaitingSlot=500;rig.RegisterAll();Assert.Equal(new[]{0,1,2},rig.Fleet.GetTelemetry().Select(t=>t.WaitingSlot));
            rig.Program.Now=6;rig.Receive("TELEMETRY",rig.Miners[1]);rig.Receive("TELEMETRY",rig.Miners[2]);rig.Fleet.Command("clear-offline");
            Assert.Equal(new[]{1,2},rig.Fleet.GetTelemetry().Select(t=>t.WaitingSlot));rig.Miners[0].Id="renamed";rig.Receive("HELLO",rig.Miners[0]);Assert.Equal(3,rig.Fleet.GetTelemetry().Single(t=>t.Id=="renamed").WaitingSlot);
            var after=new Rig(3,false);after.Restore(rig.Checkpoint());after.Miners[0].Id="next";after.Receive("HELLO",after.Miners[0]);Assert.Equal(4,after.Fleet.GetTelemetry().Single(t=>t.Id=="next").WaitingSlot);
            after.Fleet.Command("clear-tasks");Assert.Equal(5,P.Data.ReadLong(after.Save(),"fleet","NextWaitingSlot"));
        }
        [Fact]
        public void ReusingIdleDeletedNameFromAnotherProgrammableBlockIsAllowed()
        {
            var rig=new Rig(1,false);rig.RegisterAll();rig.Program.Now=6;rig.Fleet.Command("clear-offline");rig.Receive("HELLO",rig.Miners[0],7000);
            Assert.Equal(7000,Assert.Single(rig.Fleet.GetTelemetry()).Address);Assert.Equal(0,P.Data.ReadLong(rig.Save(),"fleet","ProtectedOwnerCount"));
        }
        [Fact]
        public void RemovalClearsCancellationRequestsAndWireWorkWhileRetainingUnknownLane()
        {
            var rig=new Rig(1,false);rig.RegisterAll();rig.Tick(30);rig.Receive("DOCK_REQUEST",rig.Miners[0]);rig.Tick(12);rig.Fleet.Command("clear-tasks");Assert.Equal(1,rig.Fleet.CancellationCount);
            rig.Program.Now+=6;rig.Fleet.Command("clear-offline");Assert.Equal(0,rig.Fleet.CancellationCount);Assert.Equal("miner-0",rig.Save().Get("fleet","LaneOwner").ToString());
            rig.Sent.Clear();rig.Program.Bus.Tick();Assert.DoesNotContain(rig.Sent,p=>p.Target=="miner-0");
            var after=new Rig(1,false);after.Restore(rig.Program.Storage);Assert.Empty(after.Fleet.GetTelemetry());Assert.Equal(0,after.Fleet.CancellationCount);Assert.Equal("miner-0",after.Save().Get("fleet","LaneOwner").ToString());
        }
        [Fact]
        public void RemovalRejectsOversizeJournalBeforeChangingRegistrationOrStorage()
        {
            var rig=new Rig(1,false);rig.RegisterAll();rig.Checkpoint();TestRig.SetBase(rig.Program,"Storage",rig.Program.Storage.PadRight(1500000,' '));string original=rig.Program.Storage;rig.Program.Now=6;
            Assert.Throws<ArgumentException>(()=>rig.Fleet.Command("clear-offline"));Assert.Single(rig.Fleet.GetTelemetry());Assert.Equal(original,rig.Program.Storage);
        }
        [Fact]
        public void LegacyWaitingSlotsAreAllocatedInRegistryOrderAndSavedWatermarkNeverRegresses()
        {
            var before=new Rig(2,false);before.RegisterAll();var legacy=before.Save();legacy.Set("fleet","NextWaitingSlot",7);
            for(int n=0;n<2;n++) {var miner=new MyIni();Assert.True(miner.TryParse(legacy.Get("fleet","Miner"+n).ToString()));miner.Delete("telemetry","WaitingSlot");legacy.Set("fleet","Miner"+n,miner.ToString());}
            var after=new Rig(2,false);after.Fleet.Load(legacy);Assert.Equal(new[]{7,8},after.Fleet.GetTelemetry().Select(t=>t.WaitingSlot));Assert.Equal(9,P.Data.ReadLong(after.Save(),"fleet","NextWaitingSlot"));
        }
        [Fact]
        public void OccupancyOverflowRejectsClearBeforeCancellingOrDeletingAnything()
        {
            var rig=new Rig(1,true);rig.RegisterAll();rig.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");rig.Fleet.Command("start");rig.Tick(2);P.Job owned=Assert.Single(rig.Fleet.GetJobs());
            var holds=(Dictionary<string,P.Job>)typeof(P.FleetController).GetField("clearanceHolds",System.Reflection.BindingFlags.Instance|System.Reflection.BindingFlags.NonPublic).GetValue(rig.Fleet);
            for(int n=0;n<64;n++) {var hold=P.Job.FromIni(owned.ToIni());hold.Id="unknown-"+n;hold.Owner="unknown";holds.Add(hold.Id,hold);}
            Assert.Throws<ArgumentException>(()=>rig.Fleet.Command("clear-tasks"));Assert.Single(rig.Fleet.GetJobs());Assert.Equal(0,rig.Fleet.CancellationCount);
            rig.Program.Now+=6;Assert.Throws<ArgumentException>(()=>rig.Fleet.Command("clear-offline"));Assert.Single(rig.Fleet.GetTelemetry());Assert.Equal("miner-0",owned.Owner);
        }
        [Fact]
        public void DockedRenamedMinerCanReclaimCapacityAfterOldRegistrationWasAlreadyRemoved()
        {
            var before=new Rig(1,true);before.RegisterAll();before.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");before.Fleet.Command("start");before.Tick(30);
            P.Job job=Assert.Single(before.Fleet.GetJobs());job.Progress=7;before.Miners[0].JobId=job.Id;before.Receive("DEPART_REQUEST",before.Miners[0]);before.Tick(12);before.Checkpoint();
            before.Program.Now+=6;before.Fleet.Command("clear-offline");Assert.Empty(before.Fleet.GetTelemetry());Assert.Equal("miner-0",job.Owner);
            var after=new Rig(1,true);after.Restore(before.Program.Storage);after.Miners[0].Id="renamed";after.Receive("HELLO",after.Miners[0]);
            Assert.Equal("",Assert.Single(after.Fleet.GetJobs()).Owner);Assert.Equal(7,after.Fleet.GetJobs()[0].Progress);
            Assert.Equal("",after.Save().Get("fleet","LaneOwner").ToString());Assert.Equal(0,P.Data.ReadLong(after.Save(),"fleet","ProtectedOwnerCount"));
            after.Tick(2);Assert.Equal("renamed",Assert.Single(after.Fleet.GetJobs()).Owner);
        }

        [Theory]
        [InlineData(false,5000)]
        [InlineData(true,7000)]
        public void RenamedIdWithoutPhysicalDockOrOriginalSourceCannotReleaseProtectedWork(bool connected,long source)
        {
            var r=new Rig(1,true);r.RegisterAll();r.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");r.Fleet.Command("start");r.Tick(2);
            r.Program.Now+=6;r.Fleet.Command("clear-offline");r.SetConnected(0,connected);r.Miners[0].Id="renamed";r.Miners[0].Connected=true;r.Receive("HELLO",r.Miners[0],source);
            Assert.Equal("miner-0",Assert.Single(r.Fleet.GetJobs()).Owner);Assert.Equal(1,P.Data.ReadLong(r.Save(),"fleet","ProtectedOwnerCount"));
        }

        [Fact]
        public void ConfirmedDockedRenameReleasesOldIdentityOccupancyAndPreservesTaskProgressOnReload()
        {
            var before=new Rig(1,true);before.RegisterAll();before.Fleet.Command("mark 6 6 20 GPS:entry:0:0:-200: GPS:inward:0:0:-201:");before.Fleet.Command("start");before.Tick(30);
            P.Job job=Assert.Single(before.Fleet.GetJobs());job.Progress=7;before.Miners[0].JobId=job.Id;before.Receive("DEPART_REQUEST",before.Miners[0]);before.Tick(12);
            before.Checkpoint();before.Program.Now+=6;before.Miners[0].Id="renamed";before.Miners[0].JobId="";before.Receive("HELLO",before.Miners[0]);before.Fleet.Command("clear-offline");
            Assert.Equal("renamed",Assert.Single(before.Fleet.GetTelemetry()).Id);Assert.Equal("",job.Owner);Assert.Equal(7,job.Progress);
            var saved=before.Save();Assert.Equal("",saved.Get("fleet","LaneOwner").ToString());Assert.Equal(0,P.Data.ReadLong(saved,"fleet","ProtectedOwnerCount"));
            var after=new Rig(1,false);after.Restore(before.Program.Storage);P.Job restored=Assert.Single(after.Fleet.GetJobs());Assert.Equal("",restored.Owner);Assert.Equal(7,restored.Progress);Assert.Equal("",after.Save().Get("fleet","LaneOwner").ToString());
        }
    }
}