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.Text;
using Sandbox.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame.Utilities;
using VRageMath;

namespace AutoMiningScript
{
    public partial class Program
    {
        public class FleetController : RoleLogic
        {
            class Request { public string Owner, Kind, Problem=""; public double At; }
            class PortYield { public string Peer; public Vector3D Point, Velocity, PeerVelocity; }
            readonly Random trafficRandom=new Random();
            readonly Dictionary<string,int> trafficOrder=Data.CreateDictionary<string,int>();
            readonly Dictionary<string,PortYield> portYields=Data.CreateDictionary<string,PortYield>();
            // Keep the old save/wire names; these records reserve only a connector.
            class Lane
            {
                public string Owner="", Kind="", Token="";
                public long Dock;
                public bool Renew, Consent;
                public Vector3D Position;
                public double ApproachDistance, Clearance=60;
            }
            readonly Dictionary<string,Lane> lanes = Data.CreateDictionary<string,Lane>();
            readonly Dictionary<string, Telemetry> miners = Data.CreateDictionary<string, Telemetry>();
            readonly Dictionary<string, Request> requests = Data.CreateDictionary<string, Request>();
            readonly List<IMyShipConnector> docks = Data.CreateList<IMyShipConnector>();
            readonly Dictionary<long,double> approachOffsets=Data.CreateDictionary<long,double>();
            readonly List<IMyTerminalBlock> cargo = Data.CreateList<IMyTerminalBlock>();
            readonly Dictionary<long, DockFrame> frames = Data.CreateDictionary<long, DockFrame>();

            readonly Dictionary<long, Vector3D> previousVelocity = Data.CreateDictionary<long, Vector3D>();
            readonly Dictionary<long, List<IMyTerminalBlock>> connectedBlocks = Data.CreateDictionary<long, List<IMyTerminalBlock>>();
            readonly Dictionary<string, double> unloadProgress = Data.CreateDictionary<string, double>();
            readonly Dictionary<string, int> unloadCursor = Data.CreateDictionary<string, int>();
            readonly Dictionary<string, double> dispatchedAt = Data.CreateDictionary<string, double>();
            readonly Dictionary<string, string> minerSessions = Data.CreateDictionary<string, string>();
            readonly Dictionary<string, Job> clearanceHolds = Data.CreateDictionary<string, Job>();
            readonly Dictionary<string, string> pendingCancellation = Data.CreateDictionary<string, string>();
            readonly Dictionary<string, double> cancellationSent = Data.CreateDictionary<string, double>();
            readonly HashSet<string> operatorStops = new HashSet<string>();
            readonly Dictionary<string, long> protectedOwners = Data.CreateDictionary<string, long>();
            readonly HashSet<string> persistedOccupancies = new HashSet<string>();
            readonly Dictionary<string, long> restoringRemoved = Data.CreateDictionary<string, long>();
            int nextWaitingSlot;
            long cancelledThrough = -1;
            public int CancellationCount => pendingCancellation.Count;
            readonly Queue<string> markerReceipts = new Queue<string>();
            readonly HashSet<string> markerReceiptSet = new HashSet<string>();
            readonly List<Job> occupied = Data.CreateList<Job>();
            readonly TaskPlanner planner;
            IMyShipController controller;
            string problem = "";
            int tokenSerial, unloadMinerCursor, assignMinerCursor, assignmentJobCursor, poseCursor;
            readonly Dictionary<string, double> poseSent = Data.CreateDictionary<string, double>();
            readonly Dictionary<string,double> trafficSent=Data.CreateDictionary<string,double>();
            string assignmentMiner = "";
            Job assignmentBest;
            string assignmentRegion = "";
            bool assignmentPending;
            double assignmentScore = double.MinValue, assignmentClock;
            bool running;
            double poseClock, scheduleClock, unloadClock, scanClock;
            DockFrame baseFrame;
            public FleetController(Program p) : base(p) { planner = new TaskPlanner(p); Scan(); P.Bus.AllowRebind=CanRebind; }
            public override List<Telemetry> GetTelemetry() => new List<Telemetry>(miners.Values);
            public override List<Job> GetJobs() => planner.Jobs;
            public override MiningRegion MapRegion(string id) => planner.Region(id);
            public override string Diagnostics
            {
                get
                {
                    if (Restoring) return RestoreStatus;
                    int online = 0, pending = planner.PendingCount, unknown = 0;
                    foreach (var t in miners.Values) if (P.Now - t.ReceivedAt <= OfflineSeconds) online++; else unknown++;
                    return L.F(L.FleetDiagnostics, running ? L.FleetRunning : L.FleetPaused, online, miners.Count, unknown, docks.Count, requests.Count,
                        LaneSummary(), pending, planner.Jobs.Count) +
                        (planner.Generating ? L.F(L.FleetPlanningProgress, planner.PendingGeneration) + "\n" : "") + (pendingCancellation.Count > 0 ? L.F(L.FleetAwaitCancellation, pendingCancellation.Count) + "\n" : "") + (controller == null ? L.FleetControllerMissing + "\n" : "") + (docks.Count==0 ? MissingGroup("DockGroup","AMS Docks",L.FleetBerthsMissing)+"\n" : "") + (cargo.Count == 0 ? MissingGroup("CargoGroup","AMS Cargo",L.FleetCargoMissing) + "\n" : "") + problem;
                }
            }
            void Scan()
            {
                docks.Clear(); cargo.Clear(); connectedBlocks.Clear();
                var group = FindGroup("DockGroup", "AMS Docks");
                if (group != null) group.GetBlocksOfType(docks, b => b.CubeGrid == P.Me.CubeGrid);
                docks.Sort((a,b)=>a.EntityId.CompareTo(b.EntityId));
                var order=new List<IMyShipConnector>(docks);
                order.Sort((a,b)=>{int rank=PortOrder(a.EntityId).CompareTo(PortOrder(b.EntityId));return rank!=0?rank:a.EntityId.CompareTo(b.EntityId);});
                approachOffsets.Clear();for(int n=0;n<order.Count;n++)approachOffsets[order[n].EntityId]=n*10;
                var cg = FindGroup("CargoGroup", "AMS Cargo");
                if (cg != null) cg.GetBlocks(cargo, b => b.CubeGrid == P.Me.CubeGrid && b.HasInventory);
                var controls = Data.CreateList<IMyShipController>();
                P.GridTerminalSystem.GetBlocksOfType(controls, b => b.CubeGrid == P.Me.CubeGrid && b.IsFunctional);
                var name = P.Config.Text("Fleet", "Controller", ""); controller = null;
                foreach (var c in controls) if (name.Length == 0 ? controller == null || c.IsMainCockpit : c.CustomName == name) controller = c;
            }
            // Stable shuffle: connector roll, enumeration order and reloads do not move entry points.
            static ulong PortOrder(long id)
            {
                unchecked {var n=(ulong)id;n=(n^(n>>30))*0xbf58476d1ce4e5b9UL;n=(n^(n>>27))*0x94d049bb133111ebUL;return n^(n>>31);}
            }
            double EntryDistance(long connector)
            {double offset;approachOffsets.TryGetValue(connector,out offset);return P.Config.Number("Dock","ApproachDistance",30,10,300)+offset;}
            IMyBlockGroup FindGroup(string key,string standard)
            {
                var name=P.Config.Text("Fleet",key,standard);
                var group=P.GridTerminalSystem.GetBlockGroupWithName(name);
                if(group!=null)return group;
                var local=Settings.DefaultName(standard);
                // Only built-in default names are aliases; custom groups stay exact.
                if(name==local)return P.GridTerminalSystem.GetBlockGroupWithName(standard);
                if(name==standard)return P.GridTerminalSystem.GetBlockGroupWithName(local);
                return null;
            }
            string MissingGroup(string key,string standard,string message)
            => message+": "+P.Config.Text("Fleet",key,standard);
            public override void Tick(double dt)
            {
                P.Bus.AllowRebind=CanRebind;
                if (Restoring) return;
                poseClock += dt; scheduleClock += dt; unloadClock += dt; scanClock += dt; assignmentClock += dt;
                if (scanClock >= 10 && P.HasBudget(0.3)) { scanClock = 0; Scan(); }
                if (poseClock >= 0.1 && P.HasBudget(0.35)) { UpdateFrames(poseClock); poseClock = 0; }
                if (P.HasBudget(0.45)) SendPoses();
                if (unloadClock >= 0.5 && P.HasBudget(0.35)) { unloadClock = 0; UnloadOne(); }
                if (scheduleClock >= 1 && P.HasBudget(0.4))
                {
                    scheduleClock = 0;
                    if (baseFrame != null) P.Bus.Send("BASE", "", baseFrame.ToIni());
                    ReconcilePhysicalDock(); ReconcileMineClearance(); PruneProtectedOwners(); RetryCancellation(); ScheduleLane();
                }
                if (P.HasBudget(0.4)) planner.Advance();
                if (running && assignmentClock >= 0.1 && P.HasBudget(0.45)) { assignmentClock = 0; Assign(); }
            }
            void UpdateFrames(double dt)
            {
                Vector3D center = controller == null ? P.Me.GetPosition() : controller.CenterOfMass, velocity = Data.Zero, angular = Data.Zero;
                if (controller != null) { var motion = controller.GetShipVelocities(); velocity = motion.LinearVelocity; angular = motion.AngularVelocity; }
                var basis = controller == null ? P.Me.WorldMatrix : controller.WorldMatrix;
                baseFrame = new DockFrame { BaseId = P.Config.Id, Center = center, Position = center, Forward = basis.Forward, Up = basis.Up, Velocity = velocity, AngularVelocity = angular, ReceivedAt = P.Now };
                baseFrame.Problem=docks.Count==0?MissingGroup("DockGroup","AMS Docks",L.FleetBerthsMissing):controller==null?L.FleetControllerMissing:"";
                frames.Clear();
                foreach (var dock in docks)
                {
                    if (dock.Closed || !dock.IsFunctional) continue;
                    Vector3D pointVelocity = velocity + Data.Cross(angular, dock.GetPosition() - center), old;
                    var known = previousVelocity.TryGetValue(dock.EntityId, out old);
                    var frame = new DockFrame
                    {
                        BaseId = P.Config.Id,
                        ConnectorId = dock.EntityId,
                        GridId = dock.CubeGrid.EntityId,
                        Position = dock.GetPosition(),
                        Forward = dock.WorldMatrix.Forward,
                        Up = dock.WorldMatrix.Up,
                        Center = center,
                        Velocity = velocity,
                        AngularVelocity = angular,
                        Acceleration = known ? (pointVelocity - old) / Data.Max(dt, 0.001) : new Vector3D(100, 0, 0),
                        ReceivedAt = P.Now,
                        Connected = dock.Status == MyShipConnectorStatus.Connected
                    };
                    frames[dock.EntityId] = frame; previousVelocity[dock.EntityId] = pointVelocity;
                    foreach(var lane in lanes.Values)if(lane.Dock==dock.EntityId)lane.Position=frame.Position;
                }
            }
            DockFrame Pose(long connector, string owner, string token)
            {
                DockFrame frame; if (!frames.TryGetValue(connector, out frame)) return null;
                var copy = new DockFrame { BaseId = frame.BaseId, ConnectorId = frame.ConnectorId, GridId = frame.GridId, Position = frame.Position, Forward = frame.Forward, Up = frame.Up, Center = frame.Center, Velocity = frame.Velocity, AngularVelocity = frame.AngularVelocity, Acceleration = frame.Acceleration, ReceivedAt = P.Now, Connected = frame.Connected, Owner = owner, Token = token };
                Telemetry waiting; int slot = miners.TryGetValue(owner, out waiting) && waiting.WaitingSlot >= 0 ? waiting.WaitingSlot : 0;
                double spacing = P.Config.Number("Dock", "WaitingSpacing", 20, 10, 100), distance = P.Config.Number("Dock", "WaitingDistance", 80, 40, 500);
                Lane lane;bool allocated=lanes.TryGetValue(owner,out lane) && lane.Dock==connector;
                copy.ApproachDistance=allocated?lane.ApproachDistance:EntryDistance(connector);
                copy.WaitingLocal = new Vector3D(0,0,-Data.Max(distance,copy.ApproachDistance+spacing)-(slot/Data.Max(1,docks.Count))*spacing);
                Request request;if(token.Length==0 && requests.TryGetValue(owner,out request))copy.Problem=request.Problem;
                copy.LaneClearance=allocated?lane.Clearance:P.Config.Number("Dock","LaneClearance",60,20,300);return copy;
            }
            void SendPoses()
            {
                if (miners.Count == 0) return;
                var list = new List<Telemetry>(miners.Values); int seen = 0, sent = 0;
                while (seen++ < list.Count && sent < 4 && P.HasBudget(0.55))
                {
                    poseCursor %= list.Count; var t = list[poseCursor++]; double previous;
                    if(P.Now-t.ReceivedAt<3 && (!trafficSent.TryGetValue(t.Id,out previous) || P.Now-previous>=(t.State==FlightState.DockAlign || t.State==FlightState.DockApproach?.1:.5))) {SendTraffic(t);trafficSent[t.Id]=P.Now;sent++;}
                    if (sent>=4 || P.Now - t.ReceivedAt > 10 || (poseSent.TryGetValue(t.Id, out previous) && P.Now - previous < 0.099)) continue;
                    long connector = 0; var token = "";
                    Lane lane;
                    if (lanes.TryGetValue(t.Id,out lane)) { connector = lane.Dock; token = lane.Token; }
                    else
                    {
                        var actual = ConnectedDock(t); if (actual != null) connector = actual.EntityId;
                        else if (requests.ContainsKey(t.Id) && docks.Count > 0) connector = docks[Data.Max(0,t.WaitingSlot)%docks.Count].EntityId;
                    }
                    if (connector == 0) continue;
                    var pose = Pose(connector, t.Id, token); if (pose != null) { P.Bus.Send("DOCKPOSE", t.Id, pose.ToIni(), false, t.Address); poseSent[t.Id] = P.Now; sent++; }
                }
            }
            IMyShipConnector ConnectedDock(Telemetry t)
            {
                foreach (var d in docks) if (!d.Closed && d.Status == MyShipConnectorStatus.Connected && d.OtherConnector != null && d.OtherConnector.EntityId == t.ConnectorId) return d;
                return null;
            }
            bool CanReroute(Telemetry t)
            => t.State==FlightState.Transit || t.State==FlightState.Returning || t.State==FlightState.Holding || t.State==FlightState.DockAlign || t.State==FlightState.DockApproach;
            int TrafficOrder(string id)
            {int order;if(!trafficOrder.TryGetValue(id,out order))trafficOrder[id]=order=trafficRandom.Next();return order;}
            void TrafficMessage(Telemetry t,bool wait,bool avoid,Vector3D point,string peer)
            {
                var body=new MyIni();body.Set("traffic","Wait",wait);body.Set("traffic","Avoid",avoid);Data.PutVector(body,"traffic","Point",point);body.Set("traffic","Peer",peer);
                P.Bus.Send("TRAFFIC",t.Id,body,false,t.Address);
            }
            bool NearTraffic(Telemetry t,Telemetry other)
            => NearTraffic(t,other,t.Velocity,other.Velocity);
            bool NearTraffic(Telemetry t,Telemetry other,Vector3D velocity,Vector3D otherVelocity)
            {
                if(other.Id==t.Id || ConnectedDock(other)!=null)return false;
                var offset=other.Position+other.Velocity*Data.Clamp(P.Now-other.ReceivedAt,0,1)-t.Position-t.Velocity*Data.Clamp(P.Now-t.ReceivedAt,0,1);
                var relative=otherVelocity-velocity;double speed=Data.Length(relative);
                if(speed<.1 || Data.Dot(offset,relative)>=0)return false;
                double when=-Data.Dot(offset,relative)/(speed*speed),gap=Data.Max(1,t.Radius)+Data.Max(1,other.Radius)+1;
                double miss=Data.LengthSquared(offset+relative*when);if(miss>gap*gap)return false;
                // Time to first envelope contact, not a clamped closest-point guess.
                double contact=when-Math.Sqrt(Data.Max(0,gap*gap-miss))/speed;
                double margin=t.ThrustMargin>0 && other.ThrustMargin>0?Data.Min(t.ThrustMargin,other.ThrustMargin):Data.Max(t.ThrustMargin,other.ThrustMargin);
                // Proportional braking has distance v/gain even with unlimited
                // thrust. Add actuator saturation and measured message latency;
                // only relative velocity contributes, including on a moving base.
                return contact<=.5+Data.Clamp(P.Now-Data.Min(t.ReceivedAt,other.ReceivedAt),0,1)+
                    Data.Max(t.BrakeResponse,other.BrakeResponse)+(margin>0?speed/(2*Data.Max(.05,margin*.65)):0);
            }
            Vector3D YieldSide(Telemetry loser,Telemetry winner)
            {
                var travel=Data.Unit(winner.Velocity,Data.Unit(winner.RouteTarget-winner.Position,winner.Forward));
                var side=Data.Unit(Data.Cross(travel,winner.Up),Vector3D.Right);
                if(Data.Dot(loser.Position-winner.Position,side)<0)side=-side;
                DockFrame nearest=null;double distance=double.MaxValue;Lane assigned;
                if(lanes.TryGetValue(loser.Id,out assigned) && frames.TryGetValue(assigned.Dock,out nearest))distance=Data.Distance(loser.Position,nearest.Position);
                else foreach(var frame in frames.Values)
                {
                    var d=Data.Distance(loser.Position,frame.Position);
                    if(d<distance){distance=d;nearest=frame;}
                }
                // A local port plane is a geometric backstop, not proof of free hull space.
                // The miner still scans its whole body before making this short side-step.
                if(nearest!=null && distance<Data.Max(30,loser.Radius*6))
                {
                    var pose=nearest.Predicted(P.Now);var normal=pose.Forward;
                    side+=normal*Data.Max(0,Data.Min(0,(loser.Radius-Data.Dot(loser.Position-pose.Translation,normal))/2)-Data.Dot(side,normal));
                    if(Data.LengthSquared(side)<.01 || Data.Dot(side,loser.Position-winner.Position)<0)
                        side=Data.Unit(loser.Position-winner.Position-normal*Data.Dot(loser.Position-winner.Position,normal),Data.Perpendicular(normal,loser.Up));
                }
                return side;
            }
            bool YieldRisk(Telemetry loser,Telemetry peer,PortYield yielding)
            => ConnectedDock(peer)==null && NearTraffic(loser,peer,yielding.Velocity,
                peer.HasRoute && (Data.Length(peer.Velocity)<.1 || Data.Dot(Data.Unit(peer.Velocity,Data.Zero),Data.Unit(yielding.PeerVelocity,Data.Zero))>.8)?yielding.PeerVelocity:peer.Velocity);
            void SendTraffic(Telemetry t)
            {
                if(t.Connected || !CanReroute(t))return;
                PortYield yielding;Telemetry peer;
                if(portYields.TryGetValue(t.Id,out yielding))
                {
                    if(miners.TryGetValue(yielding.Peer,out peer) && YieldRisk(t,peer,yielding))
                    {
                        foreach(var third in miners.Values)
                        {
                            if(!P.HasBudget(.6))return;
                            if(third.Id!=peer.Id && NearTraffic(t,third)){TrafficMessage(t,true,false,yielding.Point,third.Id);return;}
                        }
                        if(Data.Distance(t.Position,yielding.Point)<.5 || Data.Dot(yielding.Point-t.Position,t.Position-peer.Position)<0)
                            yielding.Point=t.Position+YieldSide(t,peer)*2;
                        bool fresh=P.Now-peer.ReceivedAt<=3;TrafficMessage(t,!fresh,fresh,yielding.Point,peer.Id);return;
                    }
                    portYields.Remove(t.Id);
                }
                foreach(var entry in portYields)
                {
                    if(!P.HasBudget(.6))return;
                    if(entry.Value.Peer==t.Id && miners.TryGetValue(entry.Key,out peer) && YieldRisk(peer,t,entry.Value))
                    {TrafficMessage(t,true,false,entry.Value.Point,peer.Id);return;}
                }
                foreach(var other in miners.Values)
                {
                    if(!P.HasBudget(.6))return;
                    if(!NearTraffic(t,other))continue;
                    bool movable=P.Now-other.ReceivedAt<=3 && CanReroute(other);
                    bool giveWay=!movable || TrafficOrder(t.Id)>TrafficOrder(other.Id) || (TrafficOrder(t.Id)==TrafficOrder(other.Id) && string.CompareOrdinal(t.Id,other.Id)>0);
                    var loser=giveWay?t:other;var winner=giveWay?other:t;
                    if(!portYields.TryGetValue(loser.Id,out yielding))
                    {
                        yielding=new PortYield {Peer=winner.Id,Point=loser.Position+YieldSide(loser,winner)*2,Velocity=loser.Velocity,PeerVelocity=winner.Velocity};portYields[loser.Id]=yielding;
                    }
                    bool fresh=P.Now-other.ReceivedAt<=3;
                    TrafficMessage(t,!giveWay || !fresh,giveWay && fresh,yielding.Point,other.Id);return;
                }
                TrafficMessage(t,false,false,Data.Zero,"");
            }
            bool CanRebind(Packet packet)
            {
                if(!packet.IsRegistration || packet.Kind=="DEPART_REQUEST")return false;
                Telemetry old;if(!miners.TryGetValue(packet.From,out old) || protectedOwners.ContainsKey(packet.From))return false;
                var current=Telemetry.FromIni(packet.Body);
                if(current.Id!=packet.From || current.ConnectorId==0)return false;
                if(current.ConnectorId==old.ConnectorId)return true;
                if(P.Now-old.ReceivedAt<=OfflineSeconds || current.JobId.Length>0 || HasOwnedJob(old.Id) ||
                    (current.State!=FlightState.Returning && current.State!=FlightState.Holding))return false;
                foreach(var hold in clearanceHolds.Values)if(hold.Owner==old.Id)return false;
                return true;
            }
            bool Register(Packet packet)
            {
                var t = Telemetry.FromIni(packet.Body);
                if (t.Id != packet.From || t.Id.Length == 0) return false;
                long protectedAddress; if (protectedOwners.TryGetValue(t.Id, out protectedAddress) && protectedAddress != packet.Source) { problem = L.F(L.FleetProtectedIdentity, t.Id); return false; }
                Telemetry old;
                if (miners.TryGetValue(t.Id, out old) && old.Address != 0 && old.Address != packet.Source)
                {
                    if(!CanRebind(packet)){problem=L.F(L.FleetDuplicateMiner,t.Id);return false;}
                    problem="";
                    P.Bus.CancelPending(null,t.Id);minerSessions.Remove(t.Id);poseSent.Remove(t.Id);trafficSent.Remove(t.Id);portYields.Remove(t.Id);
                    Lane prior;if(lanes.TryGetValue(t.Id,out prior)){prior.Token="";prior.Renew=true;prior.Consent=false;}
                }
                if (!miners.ContainsKey(t.Id) && miners.Count >= P.Config.Integer("Fleet", "MaxMiners", 16, 1, 16)) { problem = L.FleetRegistrationLimit; return false; }
                // The base owns slot allocation. Client-provided values must never move
                // another ship's waiting point or recycle an abandoned flight corridor.
                if (old != null && old.WaitingSlot >= 0) t.WaitingSlot = old.WaitingSlot;
                else { if (nextWaitingSlot >= 1000000) { problem = L.FleetWaitingSlotsExhausted; return false; } t.WaitingSlot = nextWaitingSlot++; }
                t.ReceivedAt = P.Now; t.Address = packet.Source; t.Epoch=packet.Epoch; miners[t.Id] = t;
                if ((old == null || old.NextAfter != t.NextAfter) && CanContinue(t) && planner.Find(t.NextAfter).Outcome == "SurveyHit") Prioritize(t);
                if(ConnectedDock(t)!=null){trafficOrder.Remove(t.Id);portYields.Remove(t.Id);}
                string session; var fresh = !minerSessions.TryGetValue(t.Id, out session) || session != packet.Session; minerSessions[t.Id] = packet.Session;
                // The actual peer connector, not the telemetry Connected flag, is authoritative.
                if (ConnectedDock(t) != null && !unloadProgress.ContainsKey(t.Id)) unloadProgress[t.Id] = P.Now;
                if (t.Address != 0 && ConnectedDock(t) != null)
                {
                    // A renamed PB may only fit in the registry after clear-offline.
                    // Its later physical return is evidence for those retained identities.
                    var priorIds = new List<string>(protectedOwners.Keys);
                    foreach (var id in priorIds) if (id != t.Id && protectedOwners[id] == t.Address) ReleaseOwnerOccupancy(id);
                }
                if (cancelledThrough >= 0 && t.CancelledThrough < cancelledThrough && !pendingCancellation.ContainsKey(t.Id)) pendingCancellation[t.Id] = t.JobId;
                if (pendingCancellation.ContainsKey(t.Id)) { ConfirmCancellation(t); if (pendingCancellation.ContainsKey(t.Id)) SendCancellation(t, fresh); }
                Job owned; if (fresh && running && !operatorStops.Contains(t.Id) && !pendingCancellation.ContainsKey(t.Id) && planner.OwnedJobs.TryGetValue(t.Id, out owned)) SendJob(owned, t);
                return true;
            }
            public override void Receive(Packet packet)
            {
                if (Restoring) return;
                if(packet.Epoch!=P.Bus.Epoch) {RecallOldEpoch(packet);return;}
                var requesting=packet.IsPortRequest;
                if(packet.IsRegistration)
                {if(!Register(packet) || !requesting)return;}
                if (packet.Kind == "MARK" || packet.Kind == "SURVEY_REQUEST")
                {
                    ReceiveMarker(packet); return;
                }
                Telemetry t; if (!miners.TryGetValue(packet.From, out t) || t.Address != packet.Source) return;
                if (packet.Kind == "DIAGNOSTICS")
                {
                    var text = Data.Text(packet.Body,"diagnostics", "Text");
                    problem = t.Id + "\n" + (text.Length > 2400 ? text.Substring(0, 2400) : text); P.Log(problem); return;
                }
                if(requesting)
                {
                    UpdateFrames(baseFrame==null?.1:Data.Max(.001,P.Now-baseFrame.ReceivedAt));poseClock=0;
                    if (packet.Kind == "DEPART_REQUEST" && (!running || operatorStops.Contains(t.Id) || pendingCancellation.ContainsKey(t.Id))) return;
                    Request request; if (!requests.TryGetValue(packet.From, out request)) requests[packet.From] = new Request { Owner = packet.From, Kind = packet.Kind, At = P.Now };
                    else request.Kind = packet.Kind;
                    Lane lane;
                    if (lanes.TryGetValue(packet.From,out lane))
                    {
                        // A cancelled departure may need the very same reserved corridor
                        // to return. Re-issue it to its current owner as an inbound lease.
                        if (packet.Kind != lane.Kind) { P.Bus.CancelPending(lane.Kind=="DEPART_REQUEST"?"DEPART_GRANT":"DOCK_GRANT",t.Id);lane.Renew = true; lane.Token = ""; }
                        if (lane.Renew) { lane.Consent = true; lane.Kind = packet.Kind; }
                        else SendGrant(t);
                    }
                    ReconcilePhysicalDock();ScheduleLane();
                }
                else if (packet.Kind == "LANE_RELEASE")
                {
                    Lane lane;
                    if (!lanes.TryGetValue(packet.From,out lane) || Data.Text(packet.Body,"lease", "Token") != lane.Token || lane.Token.Length==0) return;
                    var clear = ConnectedDock(t)!=null || (lane.Kind=="DEPART_REQUEST" ? FarFromPort(t) : SafeStopped(t,lane));
                    if (clear) ClearLane(t.Id); else problem = L.F(L.FleetLaneReleaseDeferred, t.Id);
                }
                else if (packet.Kind == "RESULT")
                {
                    var report = JobReport.FromIni(packet.Body); Job j = planner.Find(report.JobId);
                    if (j == null || j.Owner != packet.From || TaskPlanner.Finished(j)) return;
                    if (report.Progress < 0 || report.Progress > j.Depth + 1) return;
                    planner.Report(j, report);
                    if (report.Outcome == "SurveyHit" && t.NextAfter == j.Id) Prioritize(t);
                    if (TaskPlanner.Finished(j))
                    {
                        dispatchedAt.Remove(j.Id); Job held = Job.FromIni(j.ToIni()); held.Owner = t.Id;
                        if(j.Kind == JobKind.Survey) held.Entry = t.Position;
                        // Several rays at one observation position occupy one physical
                        // volume. Keep the newest receipt as the continuation identity.
                        var keys = new List<string>(clearanceHolds.Keys);
                        foreach(var key in keys) {var old = clearanceHolds[key]; if(old.Owner == t.Id && old.Kind == JobKind.Survey && held.Kind == JobKind.Survey && Data.Distance(old.Entry,held.Entry) < .1) clearanceHolds.Remove(key);}
                        clearanceHolds[held.Id] = held;
                    }
                    if (report.Outcome == "Blocked" || report.Outcome == "InvalidSample") P.Log(packet.From + ": " + L.Outcome(report.Outcome) + " " + report.Message);
                }
            }
            void ReceiveMarker(Packet packet)
            {
                var receipt = packet.From + "/" + packet.Id; if (markerReceiptSet.Contains(receipt)) return;
                if (packet.Kind == "MARK")
                {
                    Vector3D entry = Data.ReadVector(packet.Body, "mark", "Entry"), direction = Data.ReadVector(packet.Body, "mark", "Direction"), up = Data.ReadVector(packet.Body, "mark", "Up", Data.Up);
                    P.Log(planner.Manual(entry, direction, up, Data.ReadNumber(packet.Body, "mark", "Width"), Data.ReadNumber(packet.Body, "mark", "Height"), Data.ReadNumber(packet.Body, "mark", "Depth")));
                }
                else
                {
                    if (controller == null) { problem = L.FleetSurveyControllerRequired; return; }
                    P.Log(planner.Survey(controller.GetPosition(), controller.GetNaturalGravity(), controller.WorldMatrix.Forward, controller.WorldMatrix.Up, Data.ReadNumber(packet.Body, "survey", "Radius")));
                }
                markerReceiptSet.Add(receipt); markerReceipts.Enqueue(receipt);
                while (markerReceipts.Count > 128) markerReceiptSet.Remove(markerReceipts.Dequeue());
            }
            bool FarFromPort(Telemetry t)
            {
                if (P.Now - t.ReceivedAt > 3) return false;
                var distance = P.Config.Number("Dock", "LaneClearance", 60, 20, 300) + Data.Max(t.Radius, 3);
                foreach (var d in docks) if (Data.DistanceSquared(d.GetPosition(), t.Position) < distance * distance) return false;
                return true;
            }
            void ClearLane(string owner) {requests.Remove(owner);lanes.Remove(owner);}
            string LaneSummary()
            {
                var names=Data.CreateList<string>();foreach(var lane in lanes.Values)names.Add(lane.Owner+" "+(lane.Kind=="DEPART_REQUEST"?L.FleetDeparting:L.FleetDocking));
                return names.Count==0?L.FleetLaneFree:string.Join(" / ",names);
            }
            bool SafeStopped(Telemetry t,Lane lane)
            => P.Now-t.ReceivedAt<=3 && Data.Length(t.Velocity)<.5 &&
                    (t.State==FlightState.Fault || ((t.State==FlightState.Holding || t.State==FlightState.DockRetreat) &&
                    Data.Distance(t.Position,lane.Position)>t.Radius*2+3));
            void ReconcileMineClearance()
            {
                var keys = new List<string>(clearanceHolds.Keys);
                foreach (var id in keys)
                {
                    Job j = clearanceHolds[id]; Telemetry t; if (!miners.TryGetValue(j.Owner, out t) || P.Now - t.ReceivedAt > 3) continue;
                    var stand = P.Config.Number("Mining", "StandOff", 15, 5, 100);
                    Vector3D a = j.Kind == JobKind.Survey ? j.Entry : j.Entry - j.Direction * stand, b = j.Kind == JobKind.Survey ? j.Entry : j.Entry + j.Direction * j.Depth;
                    if (ConnectedDock(t) != null || Data.SegmentDistance(a, b, t.Position, t.Position) > t.Radius + j.Radius + 2) clearanceHolds.Remove(id);
                }
            }
            void ReconcilePhysicalDock()
            {
                foreach(var t in miners.Values)if(t.State==FlightState.Fault && P.Now-t.ReceivedAt<=3 && Data.Length(t.Velocity)<.5 && ConnectedDock(t)==null)
                {
                    var id="fault-"+t.Id;
                    if(clearanceHolds.ContainsKey(id) || clearanceHolds.Count+planner.OwnedJobs.Count<64)
                        clearanceHolds[id]=new Job {Id=id,Owner=t.Id,Kind=JobKind.Survey,Entry=t.Position,Radius=Data.Max(3,t.Radius),Depth=1,Outcome="Blocked"};
                }
                var owners=new List<string>(lanes.Keys);
                foreach(var owner in owners)
                {
                    var lane=lanes[owner];Telemetry t;if(!miners.TryGetValue(owner,out t))continue;
                    if((lane.Kind=="DOCK_REQUEST" && ConnectedDock(t)!=null) ||
                        (lane.Kind=="DEPART_REQUEST" && !t.Connected && DepartureComplete(t.State) && FarFromPort(t)) ||
                        (t.State==FlightState.Fault && clearanceHolds.ContainsKey("fault-"+t.Id) && SafeStopped(t,lane))) ClearLane(owner);
                }
                // A stopped, known ship protects its actual location. An abandoned
                // task far away must not remain reserved on behalf of that ship.
                var jobs=new List<Job>(planner.OwnedJobs.Values);
                foreach(var j in jobs)
                {
                    Telemetry t;if(!miners.TryGetValue(j.Owner,out t) || P.Now-t.ReceivedAt>3 || t.State!=FlightState.Fault || Data.Length(t.Velocity)>=.5)continue;
                    if(Data.SegmentDistance(j.Entry-j.Direction*30,j.Entry+j.Direction*(j.Kind==JobKind.Survey?0:j.Depth),t.Position,t.Position)>t.Radius+j.Radius+2)planner.SetOwner(j,"");
                }
            }
            static bool DepartureComplete(FlightState state)
            => state == FlightState.Transit || state == FlightState.Survey || state == FlightState.Align ||
                    state == FlightState.Drilling || state == FlightState.Retreat;
            bool CanReassign(Telemetry t,Lane lane)
            => lane.Kind=="DOCK_REQUEST" && lane.Renew && lane.Consent && !t.Connected && P.Now-t.ReceivedAt<=3 &&
                (t.State==FlightState.Returning || t.State==FlightState.Holding) &&
                Data.Distance(t.Position,lane.Position)>Data.Max(lane.ApproachDistance,t.Radius*2)+t.Radius+2;
            void ScheduleLane()
            {
                // Fresh requests outside the entry may change berths after an expired grant.
                foreach(var lane in new List<Lane>(lanes.Values))
                {
                    if(!P.HasBudget(.55))return;Telemetry held;
                    if(!lane.Renew || !lane.Consent || !miners.TryGetValue(lane.Owner,out held))continue;
                    if(CanReassign(held,lane)) {lanes.Remove(lane.Owner);P.Bus.CancelPending("DOCK_GRANT",lane.Owner);}
                    else RenewLane(held,lane);
                }
                var queue=Data.CreateList<Request>();
                foreach(var q in requests.Values)
                {
                    Telemetry t; if (lanes.ContainsKey(q.Owner) || !miners.TryGetValue(q.Owner, out t) || P.Now - t.ReceivedAt > 5 || t.State==FlightState.Fault || t.State==FlightState.Manual || t.State==FlightState.Paused) continue;
                    queue.Add(q);
                }
                queue.Sort((a,b)=>RequestScore(b).CompareTo(RequestScore(a)));
                foreach(var q in queue)
                {
                    q.Problem=L.FleetNoFreeDock;
                    if(lanes.Count>=16)return;
                    if(!P.HasBudget(.6)){q.Problem=L.CommonBudgetDeferred;return;}
                    var t=miners[q.Owner];Lane granted=null;
                    if(q.Kind=="DEPART_REQUEST")
                    {
                        if(!running || operatorStops.Contains(t.Id) || pendingCancellation.ContainsKey(t.Id) || !HasOwnedJob(t.Id))continue;
                        var berth=ConnectedDock(t);if(berth==null)continue;

                        q.Problem=PortProblem(berth.EntityId,t.Id);if(q.Problem.Length>0)continue;
                        granted=NewLane(q,t,berth.EntityId);
                    }
                    else
                    {
                        double nearest=double.MaxValue;
                        foreach(var d in docks)
                        {
                            if(!P.HasBudget(.6)){q.Problem=L.CommonBudgetDeferred;return;}
                            if(d.Closed || !d.IsFunctional || d.Status==MyShipConnectorStatus.Connected)continue;
                            double distance=Data.DistanceSquared(t.Position,d.GetPosition());
                            if(distance>=nearest)continue;

                            var why=PortProblem(d.EntityId,t.Id);
                            if(why.Length==0){granted=NewLane(q,t,d.EntityId);nearest=distance;}else q.Problem=why;
                        }
                    }
                    if(granted==null)continue;
                    q.Problem="";granted.Token=NewLaneToken();lanes.Add(t.Id,granted);SendGrant(t);
                }
            }
            double RequestScore(Request q) => P.Now-q.At+(q.Kind=="DOCK_REQUEST"?1000+(1-Data.Min(miners[q.Owner].Battery,miners[q.Owner].Hydrogen))*1000:0);
            string NewLaneToken() => P.Config.Id+"-"+P.Bus.Session+"-lane-"+(++tokenSerial);
            void WriteLanes(MyIni ini,string section,IEnumerable<Lane> values)
            {
                ini.Set(section,"LaneOwner","");ini.Set(section,"LaneToken","");
                int n=0;
                foreach(var lane in values)
                {
                    var s=n==0?section:section+"Lane"+n;n++;
                    ini.Set(s,"LaneOwner",lane.Owner);ini.Set(s,"LaneKind",lane.Kind);ini.Set(s,"LaneDock",lane.Dock);ini.Set(s,"LaneToken",lane.Token);
                    Data.PutVector(ini,s,"LanePosition",lane.Position);ini.Set(s,"LaneClearance",lane.Clearance);ini.Set(s,"LaneApproachDistance",lane.ApproachDistance);
                }
                ini.Set(section,"LaneCount",n);
            }
            void ReadLanes(MyIni ini,string section,bool clear)
            {
                if(clear)lanes.Clear();
                int count=StoredCount(ini,section,"LaneCount",16);
                if(count==0 && Data.Text(ini,section,"LaneOwner").Length>0)count=1;
                var seen=new HashSet<string>();
                for(int n=0;n<count;n++)
                {
                    var s=n==0?section:section+"Lane"+n;
                    var lane=new Lane {Owner=Data.Text(ini,s,"LaneOwner"),Kind=Data.Text(ini,s,"LaneKind"),Dock=Data.ReadLong(ini,s,"LaneDock"),Renew=true};
                    if(lane.Owner.Length==0 || !seen.Add(lane.Owner) || (section=="remove" && !restoringRemoved.ContainsKey(lane.Owner)) || (lane.Kind!="DOCK_REQUEST" && lane.Kind!="DEPART_REQUEST"))throw Data.Invalid(L.FleetStorageRecordInvalid);
                    lane.Position=Data.ReadVector(ini,s,"LanePosition");
                    lane.ApproachDistance=Data.Clamp(Data.ReadNumber(ini,s,"LaneApproachDistance",EntryDistance(lane.Dock)),10,10000);
                    lane.Clearance=Data.Clamp(Data.ReadNumber(ini,s,"LaneClearance",P.Config.Number("Dock","LaneClearance",60,20,300)),20,10000);
                    lanes[lane.Owner]=lane;
                }
            }
            Lane NewLane(Request request,Telemetry t,long connector)
            {
                var approach=EntryDistance(connector);
                double offset;approachOffsets.TryGetValue(connector,out offset);
                return new Lane {Owner=t.Id,Kind=request.Kind,Dock=connector,Position=frames[connector].Position,ApproachDistance=approach,
                    Clearance=Data.Max(P.Config.Number("Dock","LaneClearance",60,20,300)+offset,Data.Max(approach,t.Radius*2)-t.Radius-2)};
            }
            string PortProblem(long connector,string owner)
            {
                foreach(var other in lanes.Values)if(other.Owner!=owner && other.Dock==connector)return L.F(L.FleetLaneBlocked,other.Owner);
                return "";
            }
            void RenewLane(Telemetry t,Lane lane)
            {
                if (!lane.Renew || !lane.Consent || P.Now - t.ReceivedAt > 3) return;
                Request request;if(!requests.TryGetValue(t.Id,out request))return;

                request.Problem=L.FleetNoFreeDock;
                IMyShipConnector berth = null; foreach (var d in docks) if (d.EntityId == lane.Dock) berth = d;
                if (berth == null || berth.Closed || !berth.IsFunctional) return;
                if (lane.Kind == "DEPART_REQUEST") { if (ConnectedDock(t) != berth) return; }
                else if (berth.Status == MyShipConnectorStatus.Connected) return;
                request.Problem=PortProblem(lane.Dock,t.Id);if(request.Problem.Length>0)return;
                lane.Token = NewLaneToken(); lane.Renew = false; SendGrant(t);
            }
            bool HasOwnedJob(string owner) => planner.OwnedJobs.ContainsKey(owner);
            void SendGrant(Telemetry t)
            {
                Lane lane;if(!lanes.TryGetValue(t.Id,out lane) || lane.Renew || lane.Token.Length==0)return;
                var pose = Pose(lane.Dock, t.Id, lane.Token); if (pose == null) return;
                P.Bus.Send(lane.Kind == "DEPART_REQUEST" ? "DEPART_GRANT" : "DOCK_GRANT", t.Id, pose.ToIni(), true, t.Address);
            }
            string SearchRoot(Job job)
            {
                var region = planner.Region(job.RegionId);
                for(int n=0; region!=null && region.Parent.Length>0 && n<64; n++) region=planner.Region(region.Parent);
                return region==null?job.RegionId:region.Id;
            }
            bool CanContinue(Telemetry t)
            {
                Job held = planner.Find(t.NextAfter);
                return !t.Connected && t.State==FlightState.Survey && t.JobId.Length==0 && t.NextAfter.Length>0 &&
                    held!=null && held.LastOwner==t.Id && held.Kind!=JobKind.Manual && TaskPlanner.Finished(held);
            }
            bool Conflict(Job candidate, double radius, string owner = "")
            {
                var stand = P.Config.Number("Mining", "StandOff", 15, 5, 100);
                var start = candidate.Kind == JobKind.Survey ? candidate.Entry : candidate.Entry - candidate.Direction * stand;
                var end = candidate.Kind == JobKind.Survey ? candidate.Entry : candidate.Entry + candidate.Direction * candidate.Depth;
                foreach (var other in occupied)
                {
                    if (other == candidate || other.Owner == owner) continue;
                    Telemetry t; var otherRadius = other.Radius;
                    if (miners.TryGetValue(other.Owner, out t)) otherRadius = Data.Max(otherRadius, t.Radius);
                    var otherStart = other.Kind == JobKind.Survey ? other.Entry : other.Entry - other.Direction * stand;
                    var otherEnd = other.Kind == JobKind.Survey ? other.Entry : other.Entry + other.Direction * other.Depth;
                    if (Data.SegmentDistance(start, end, otherStart, otherEnd) < radius + otherRadius + 2) return true;
                }
                foreach(var t in miners.Values)if(t.Id!=owner && ConnectedDock(t)==null && Data.SegmentDistance(start,end,t.Position,t.Position)<radius+Data.Max(3,t.Radius)+2)return true;
                return false;
            }
            void Assign()
            {
                if (miners.Count == 0) return;
                occupied.Clear(); foreach (var job in planner.OwnedJobs.Values) occupied.Add(job);
                foreach (var held in clearanceHolds.Values) occupied.Add(held);
                Telemetry t;
                if (assignmentMiner.Length == 0)
                {
                    var list = new List<Telemetry>(miners.Values); assignMinerCursor %= list.Count; t = list[assignMinerCursor++];
                    if (P.Now - t.ReceivedAt > 5 || t.Epoch!=P.Bus.Epoch || operatorStops.Contains(t.Id) || pendingCancellation.ContainsKey(t.Id)) return;
                    Job owned; planner.OwnedJobs.TryGetValue(t.Id, out owned);
                    if (owned != null)
                    {
                        double sent; var first = !dispatchedAt.TryGetValue(owned.Id, out sent);
                        if ((t.JobId != owned.Id || first) && (t.State == FlightState.Docked || t.State == FlightState.Servicing || t.State == FlightState.Ready || t.State == FlightState.Paused || CanContinue(t)) && (first || P.Now - sent >= 2)) SendJob(owned, t);
                        return;
                    }
                    if (planner.OwnedJobs.Count >= 16 || clearanceHolds.Count + planner.OwnedJobs.Count >= 64) { problem = L.FleetUnknownOccupancyLimit; return; }
                    if (t.JobId.Length > 0 || (!CanContinue(t) && (!t.Connected || ConnectedDock(t) == null)) || t.Battery < 0.25 || t.Hydrogen < 0.2 || t.State == FlightState.Manual || t.State == FlightState.Fault || t.State == FlightState.Paused) return;
                    assignmentMiner = t.Id; assignmentJobCursor = 0; assignmentBest = null; assignmentScore = double.MinValue;
                    assignmentRegion = CanContinue(t)?SearchRoot(planner.Find(t.NextAfter)):""; assignmentPending=false;
                }
                if (!miners.TryGetValue(assignmentMiner, out t) || t.Epoch!=P.Bus.Epoch || operatorStops.Contains(t.Id) || pendingCancellation.ContainsKey(t.Id) || P.Now - t.ReceivedAt > 5 || (!t.Connected && !CanContinue(t)) || t.JobId.Length > 0) { assignmentMiner = ""; return; }
                int examined = 0;
                while (assignmentJobCursor < planner.Jobs.Count && examined++ < 32 && P.HasBudget(0.55))
                {
                    Job j = planner.Jobs[assignmentJobCursor++]; if (TaskPlanner.Finished(j) || (assignmentRegion.Length>0 && (j.Kind==JobKind.Manual || SearchRoot(j)!=assignmentRegion))) continue;
                    var priority=planner.JobPriority(j);if(priority<0)continue;
                    assignmentPending=true;if(j.Owner.Length>0)continue;
                    var score = priority * 100000 - Data.Distance(t.Position, j.Entry) - j.HoleIndex * 0.001;
                    if (score <= assignmentScore || Conflict(j, Data.Max(j.Radius, t.Radius),t.Id)) continue;
                    assignmentScore = score; assignmentBest = j;
                }
                if (assignmentJobCursor < planner.Jobs.Count) return;
                if (planner.OwnedJobs.Count < 16 && clearanceHolds.Count + planner.OwnedJobs.Count < 64 && assignmentBest != null && assignmentBest.Owner.Length == 0 && !TaskPlanner.Finished(assignmentBest) && !Conflict(assignmentBest, Data.Max(assignmentBest.Radius, t.Radius),t.Id)) { planner.SetOwner(assignmentBest, t.Id); SendJob(assignmentBest, t); }
                else if(CanContinue(t)) {var body=new MyIni();body.Set("next","After",t.NextAfter);body.Set("next","Done",!assignmentPending && !planner.Generating);P.Bus.Send("NEXT",t.Id,body,false,t.Address);}
                assignmentMiner = "";
            }
            // A newly confirmed surface gets the next available assignment pass;
            // existing scan and bore owners keep their current work.
            void Prioritize(Telemetry t) { assignmentMiner = ""; assignMinerCursor = new List<string>(miners.Keys).IndexOf(t.Id); }
            void SendJob(Job job, Telemetry t) { if (operatorStops.Contains(t.Id) || pendingCancellation.ContainsKey(t.Id) || planner.Find(job.Id) != job) return; var body=job.ToIni();body.Set("next","After",t.NextAfter);P.Bus.Send("JOB", t.Id, body, true, t.Address); dispatchedAt[job.Id] = P.Now; }
            void UnloadOne()
            {
                if (cargo.Count == 0 || miners.Count == 0) return;
                var list = new List<Telemetry>(miners.Values); unloadMinerCursor %= list.Count; var t = list[unloadMinerCursor++];
                var dock = ConnectedDock(t); if (dock == null || dock.OtherConnector == null) { unloadCursor.Remove(t.Id); unloadProgress.Remove(t.Id); return; }
                var grid = dock.OtherConnector.CubeGrid.EntityId; List<IMyTerminalBlock> blocks;
                if (!connectedBlocks.TryGetValue(grid, out blocks))
                {
                    blocks = Data.CreateList<IMyTerminalBlock>(); P.GridTerminalSystem.GetBlocksOfType(blocks, b => b.CubeGrid.EntityId == grid && b.HasInventory && Data.CargoInventory(b));
                    connectedBlocks[grid] = blocks;
                }
                int cursor; if (!unloadCursor.TryGetValue(t.Id, out cursor)) cursor = 0;
                int processed = 0, transfers = 0, attempts = 0; var anyOre = false;
                while (blocks.Count > 0 && processed < Data.Min(12, blocks.Count) && transfers < 24 && attempts < 48 && P.HasBudget(0.55))
                {
                    cursor %= blocks.Count; var block = blocks[cursor++]; processed++;
                    if (block.Closed || block.CubeGrid.EntityId != grid || !Data.CargoInventory(block)) continue;
                    for (int inv = 0; inv < block.InventoryCount; inv++)
                    {
                        var source = block.GetInventory(inv); var items = Data.CreateList<MyInventoryItem>(); source.GetItems(items);
                        for (int k = items.Count - 1; k >= 0 && transfers < 24 && attempts < 48 && P.HasBudget(0.55); k--)
                        {
                            if (items[k].Type.TypeId != "MyObjectBuilder_Ore") continue; anyOre = true;
                            foreach (var target in cargo)
                            {
                                if (attempts++ >= 48 || !P.HasBudget(0.55)) break;
                                if (target.Closed || target.CubeGrid != P.Me.CubeGrid || dock.Status != MyShipConnectorStatus.Connected || dock.OtherConnector == null || dock.OtherConnector.CubeGrid.EntityId != grid) break;
                                var destination = target.GetInventory(0);
                                if (!source.CanTransferItemTo(destination, items[k].Type)) continue;
                                if (source.TransferItemTo(destination, k, null, true)) { unloadProgress[t.Id] = P.Now; transfers++; break; }
                            }
                        }
                    }
                }
                unloadCursor[t.Id] = cursor;
                double last; if (anyOre && unloadProgress.TryGetValue(t.Id, out last) && P.Now - last > 30) problem = L.F(L.FleetUnloadBlocked, t.Id);
            }
            public override void Command(string command)
            {
                command=Settings.NormalizeCommand(command);
                if(command=="init") {P.Main(command,UpdateType.Trigger);return;}
                if (Restoring) { P.Log(RestoreStatus); return; }
                command = command.Trim(); var parts = command.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) return; var verb = parts[0].ToLowerInvariant();
                var target = command.Substring(parts[0].Length).Trim();
                if (target.Length >= 2 && target[0] == '"' && target[target.Length - 1] == '"') target = target.Substring(1, target.Length - 2);
                if (verb == "clear-tasks" || verb == "clear-all") { ClearAllTasks(); return; }
                if (verb == "clear-offline" || verb == "remove-offline") { ClearOffline(); return; }
                if (verb == "init" || verb == "rescan" || verb == "diagnose" || verb == "calibrate")
                {
                    if (target.Length == 0 && verb != "calibrate") { Scan(); P.Log(Diagnostics); if (verb == "diagnose") return; }
                    OperateMiners(verb == "init" ? "rescan" : verb, target); return;
                }
                if (verb == "mark") { var mark = Marking.Parse(P, command); P.Log(planner.Manual(Data.ReadVector(mark, "mark", "Entry"), Data.ReadVector(mark, "mark", "Direction"), Data.ReadVector(mark, "mark", "Up", Data.Up), Data.ReadNumber(mark, "mark", "Width"), Data.ReadNumber(mark, "mark", "Height"), Data.ReadNumber(mark, "mark", "Depth"))); return; }
                if (verb == "survey")
                {
                    if (controller == null) throw Data.Invalid(L.FleetSurveyControllerRequired);
                    var radius = parts.Length > 1 ? Marking.Number(parts[1]) : 0;
                    P.Log(planner.Survey(controller.GetPosition(), controller.GetNaturalGravity(), controller.WorldMatrix.Forward, controller.WorldMatrix.Up, radius)); return;
                }
                if (verb == "start" || verb == "resume" || verb == "pause" || verb == "return" || verb == "release" || verb == "return-all")
                {
                    OperateMiners(verb == "return-all" ? "return" : verb == "start" ? "resume" : verb, verb == "return-all" ? "all" : target); return;
                }
                if (verb == "release-lease" && target.Length > 0)
                {
                    Telemetry t; Lane lane;if (lanes.TryGetValue(target,out lane) && miners.TryGetValue(target, out t) && (ConnectedDock(t) != null || FarFromPort(t) || SafeStopped(t,lane))) ClearLane(target);
                    else throw Data.Invalid(L.FleetLeaseClearEvidenceRequired); return;
                }
                P.Log(L.FleetOperatorHelp);
            }
            void OperateMiners(string action, string target)
            {
                var all = target.Length == 0 || target == "*" || target.Equals("all", Data.IgnoreCase);
                Telemetry selected; if (!all && !miners.TryGetValue(target, out selected)) throw Data.Invalid(L.FleetUnknownMiner);
                if (action == "resume")
                {
                    // Starting one miner while the fleet is stopped must not dispatch the others.
                    if (!all && !running) foreach (var t in miners.Values) if (t.Id != target) operatorStops.Add(t.Id);
                    running = true; if (all) operatorStops.Clear(); else operatorStops.Remove(target);
                }
                else if (action == "pause" || action == "return" || action == "release")
                {
                    if (all) running = false;
                    foreach (var t in miners.Values) if (all || t.Id == target) operatorStops.Add(t.Id);
                }
                int count = 0; var report = "";
                foreach (var t in miners.Values)
                {
                    if (!all && t.Id != target) continue;
                    // Offline identities retain their safety reservation; a later reconnect
                    // must not execute a stale one-off service command unexpectedly.
                    if (P.Now - t.ReceivedAt > OfflineSeconds) continue;
                    var text = action == "resume" && pendingCancellation.ContainsKey(t.Id) && ConnectedDock(t) == null ? "return" : action;
                    var body = new MyIni(); body.Set("command", "Text", text); P.Bus.Send("COMMAND", t.Id, body, true, t.Address); count++;
                    if (action == "diagnose") report += t.Id + ": " + L.State(t) + " / " + t.Reason + "\n";
                }
                if (action == "diagnose" && report.Length > 0) problem = report;
                P.Log(L.F(L.FleetOperatorSent, count));
                if (action == "resume" && pendingCancellation.Count > 0) P.Log(L.F(L.FleetAwaitCancellation, pendingCancellation.Count));
            }
            double OfflineSeconds => P.Config.Number("Display", "OfflineSeconds", 5, 1, 60);
            bool HasOccupancy(string id)
            {
                if (lanes.ContainsKey(id) || planner.OwnedJobs.ContainsKey(id)) return true;
                foreach (var hold in clearanceHolds.Values) if (hold.Owner == id) return true;
                return false;
            }
            void ProtectOwner(string id, long address)
            {
                if (address == 0 || !HasOccupancy(id)) return;
                if (!protectedOwners.ContainsKey(id) && protectedOwners.Count >= 64) throw Data.Invalid(L.FleetUnknownOccupancyLimit);
                protectedOwners[id] = address; P.Bus.ProtectPeer(id, address);
            }
            void PruneProtectedOwners()
            {
                var ids = new List<string>(protectedOwners.Keys);
                foreach (var id in ids) if (!HasOccupancy(id)) { protectedOwners.Remove(id); P.Bus.ProtectPeer(id, 0); }
            }
            void EnsureOccupancyCapacity()
            {
                var ids = new HashSet<string>(clearanceHolds.Keys); foreach (var job in planner.OwnedJobs.Values) ids.Add(job.Id);
                if (ids.Count > 64) throw Data.Invalid(L.FleetUnknownOccupancyLimit);
            }
            void CapturePersistedOccupancies()
            {
                persistedOccupancies.Clear(); foreach (var hold in clearanceHolds.Values) persistedOccupancies.Add(hold.Id);
                foreach (var job in planner.OwnedJobs.Values) persistedOccupancies.Add(job.Id);
            }
            void ForgetMiner(string id, long address)
            {
                ProtectOwner(id, address);
                miners.Remove(id); requests.Remove(id); poseSent.Remove(id); minerSessions.Remove(id); unloadProgress.Remove(id); unloadCursor.Remove(id);
                portYields.Remove(id);trafficOrder.Remove(id);
                pendingCancellation.Remove(id); cancellationSent.Remove(id); operatorStops.Remove(id);
                Job owned; if (planner.OwnedJobs.TryGetValue(id, out owned)) dispatchedAt.Remove(owned.Id);
                P.Bus.ForgetPeer(id);
                // Cached block lists and the in-progress registry snapshot can contain the
                // deleted ship even when the planner snapshot is still being assembled.
                connectedBlocks.Clear(); storageMiners = null; storageMinerSnapshot = null; storageMinerCursor = 0;
                assignmentMiner = ""; assignmentBest = null; assignmentJobCursor = 0; assignmentScore = double.MinValue;
            }
            bool RenamedShipIsDocked(Telemetry removed)
            {
                if (removed.Address == 0) return false;
                foreach (var current in miners.Values)
                    if (current.Id != removed.Id && current.Address == removed.Address && P.Now - current.ReceivedAt <= OfflineSeconds && ConnectedDock(current) != null) return true;
                return false;
            }
            void ReleaseOwnerOccupancy(string id)
            {
                Job owned; if (planner.OwnedJobs.TryGetValue(id, out owned)) { planner.SetOwner(owned, ""); dispatchedAt.Remove(owned.Id); }
                var keys = new List<string>(clearanceHolds.Keys); foreach (var key in keys) if (clearanceHolds[key].Owner == id) clearanceHolds.Remove(key);
                ClearLane(id);
                protectedOwners.Remove(id); P.Bus.ProtectPeer(id, 0);
            }
            void ClearOffline()
            {
                var removed = Data.CreateList<Telemetry>(); foreach (var t in miners.Values) if (P.Now - t.ReceivedAt > OfflineSeconds) removed.Add(t);
                if (removed.Count == 0) { problem = L.F(L.FleetOfflineCleared, 0); P.Log(problem); return; }
                var verifiedClear = new HashSet<string>(); foreach (var t in removed) if (RenamedShipIsDocked(t)) verifiedClear.Add(t.Id);
                EnsureOccupancyCapacity();
                var protectedIds = new HashSet<string>(protectedOwners.Keys); foreach (var t in removed) if (t.Address != 0 && HasOccupancy(t.Id)) protectedIds.Add(t.Id);
                if (protectedIds.Count > 64) throw Data.Invalid(L.FleetUnknownOccupancyLimit);
                if (!string.IsNullOrEmpty(P.Storage))
                {
                    if (!P.Storage.StartsWith(StoragePrefix, Data.Ordinal)) throw Data.Invalid(L.FleetStorageRecordInvalid);
                    var record = new MyIni(); record.Set("remove", "Count", removed.Count); record.Set("remove", "NextWaitingSlot", nextWaitingSlot); record.Set("remove", "TaskSerial", planner.TaskSerial);
                    var ids = new HashSet<string>(); int n = 0;
                    foreach (var t in removed) { ids.Add(t.Id); record.Set("remove", "Id" + n, t.Id); record.Set("remove", "Address" + n, t.Address); record.Set("remove", "Cleared" + n, verifiedClear.Contains(t.Id)); n++; }
                    var retainedLanes=Data.CreateList<Lane>();foreach(var lane in lanes.Values)if(ids.Contains(lane.Owner) && !verifiedClear.Contains(lane.Owner))retainedLanes.Add(lane);
                    WriteLanes(record,"remove",retainedLanes);record.Set("remove","HasLane",retainedLanes.Count>0);record.Set("remove","TokenSerial",tokenSerial);
                    var chunks = Data.CreateList<string>(); chunks.Add(P.Storage); chunks.Add(TaskPlanner.StorageRecord(record, "RemoveMiners"));
                    var captured = new HashSet<string>();
                    foreach (var t in removed)
                    {
                        if (verifiedClear.Contains(t.Id)) continue;
                        Job owned; if (planner.OwnedJobs.TryGetValue(t.Id, out owned) && captured.Add(owned.Id)) AppendRemovedHold(chunks, owned, t.Address);
                        foreach (var hold in clearanceHolds.Values) if (hold.Owner == t.Id && captured.Add(hold.Id)) AppendRemovedHold(chunks, hold, t.Address);
                    }
                    var persisted = new HashSet<string>(persistedOccupancies); foreach (var id in captured) persisted.Add(id);
                    if (persisted.Count > 64) throw Data.Invalid(L.FleetUnknownOccupancyLimit);
                    var committed = string.Concat(chunks); if (committed.Length > 1500000) throw Data.Invalid(L.FleetStorageRecordInvalid);
                    // Persist the small removal journal before changing the registry. The
                    // last completed planner checkpoint is never parsed in this command.
                    P.Storage = committed;
                    foreach (var id in captured) persistedOccupancies.Add(id);
                }
                foreach (var t in removed) { if (verifiedClear.Contains(t.Id)) ReleaseOwnerOccupancy(t.Id); ForgetMiner(t.Id, t.Address); }
                problem = L.F(L.FleetOfflineCleared, removed.Count); P.Log(problem);
            }
            static void AppendRemovedHold(List<string> chunks, Job job, long address)
            {
                var record = job.ToIni(); record.Set("remove", "Address", address); chunks.Add(TaskPlanner.StorageRecord(record, "RemovedClearance"));
            }
            void ClearAllTasks()
            {
                EnsureOccupancyCapacity();
                running = false; cancelledThrough = Data.Max(cancelledThrough, planner.TaskSerial);
                foreach (var job in planner.OwnedJobs.Values)
                {
                    Job held = Job.FromIni(job.ToIni()); clearanceHolds[held.Id] = held;
                    if (miners.ContainsKey(held.Owner)) pendingCancellation[held.Owner] = held.Id;
                }
                foreach (var t in miners.Values) if (!pendingCancellation.ContainsKey(t.Id)) pendingCancellation[t.Id] = t.JobId;
                planner.ClearTasks(); assignmentMiner = ""; assignmentBest = null; assignmentJobCursor = assignMinerCursor = 0; assignmentScore = double.MinValue;
                occupied.Clear(); dispatchedAt.Clear(); cancellationSent.Clear(); storageMiners = null; storageMinerSnapshot = null; storageMinerCursor = 0;
                var requestIds = new List<string>(requests.Keys); foreach (var id in requestIds) if (requests[id].Kind == "DEPART_REQUEST" && !lanes.ContainsKey(id)) requests.Remove(id);
                P.Bus.CancelPending("JOB"); P.Bus.CancelPending("COMMAND"); P.Bus.CancelPending("DEPART_GRANT");
                // Commit a bounded, complete empty-plan snapshot before sending cancellation.
                // World-save/recompile in this very frame can never restore the previous plan.
                var fleetHeader = new MyIni(); SaveFleet(fleetHeader, false, false); fleetHeader.Set("storage", "Role", P.Config.Role); fleetHeader.Set("storage", "Id", P.Config.Id);
                var chunks = Data.CreateList<string>(); chunks.Add(StoragePrefix); chunks.Add(TaskPlanner.StorageRecord(fleetHeader, "Fleet"));
                foreach (var t in miners.Values) { var record = t.ToIni(); record.Set("record", "Address", t.Address); chunks.Add(TaskPlanner.StorageRecord(record, "Miner")); }
                AppendAuxiliaryRecords(chunks);
                var emptyPlanner = new MyIni(); planner.Save(emptyPlanner); chunks.Add(TaskPlanner.StorageRecord(emptyPlanner, "Planner"));
                P.Storage = string.Concat(chunks); CapturePersistedOccupancies();
                P.Log(L.F(L.FleetTasksCleared, pendingCancellation.Count));
                RetryCancellation();
            }
            void SendCancellation(Telemetry t, bool force = false)
            {
                if (cancelledThrough < 0 || !pendingCancellation.ContainsKey(t.Id) || !P.HasBudget(0.65)) return;
                double last; if (!force && cancellationSent.TryGetValue(t.Id, out last) && P.Now - last < 2) return;
                var body = new MyIni(); body.Set("command", "Text", "cancel-through " + cancelledThrough.ToString(Data.Culture));
                P.Bus.Send("COMMAND", t.Id, body, true, t.Address); cancellationSent[t.Id] = P.Now;
            }
            void RetryCancellation()
            {
                foreach (var t in miners.Values) if (pendingCancellation.ContainsKey(t.Id)) SendCancellation(t);
            }
            void ConfirmCancellation(Telemetry t)
            {
                string previous;
                if (!pendingCancellation.TryGetValue(t.Id, out previous) || t.CancelledThrough < cancelledThrough || P.Now - t.ReceivedAt > 3 || ConnectedDock(t) == null) return;
                if (previous.Length == 0 ? t.JobId.Length > 0 : t.JobId == previous) return;
                pendingCancellation.Remove(t.Id); cancellationSent.Remove(t.Id);
                ClearLane(t.Id);
                if (running && !operatorStops.Contains(t.Id) && t.State != FlightState.Manual) { var body = new MyIni(); body.Set("command", "Text", "resume"); P.Bus.Send("COMMAND", t.Id, body, true, t.Address); }
                if (pendingCancellation.Count == 0) P.Log(L.FleetCancellationComplete);
            }
            const string StoragePrefix = "XFEAMS-FLEET-3\n";
            const string ResetPrefix = "XFEAMS-RESET-1\n";
            public static string ResetStorage(long epoch) => ResetPrefix+epoch.ToString(Data.Culture);
            void RecallOldEpoch(Packet packet)
            {
                var body=new MyIni();body.Set("command","Text","return");body.Set("command","Reset",true);P.Bus.Send("COMMAND",packet.From,body,false,packet.Source);
            }
            string restoreValue;
            int restoreCursor, restorePhase, restoreAuxiliaryHolds, restoreAuxiliaryMarkers;
            MyIni restoreFleet;
            List<string> storageMiners;
            List<Telemetry> storageMinerSnapshot;
            int storageMinerCursor;
            public bool Restoring => restoreValue != null;
            public double RestoreProgress => !Restoring ? 1 : restoreValue.Length == 0 ? 0 : (double)restoreCursor / restoreValue.Length;
            public string RestoreStatus => L.F(L.FleetRestoreProgress,(RestoreProgress*100).ToString("0",Data.Culture));
            public bool BeginRestoreStorage(string value)
            {
                if (string.IsNullOrWhiteSpace(value)) return false;
                if(value.StartsWith(ResetPrefix,Data.Ordinal))
                {
                    long epoch;if(!long.TryParse(value.Substring(ResetPrefix.Length),out epoch) || epoch<1 || epoch>DateTime.MaxValue.Ticks)throw Data.Invalid(L.FleetStorageRecordInvalid);
                    P.Bus.Epoch=epoch;running=false;return true;
                }
                if(!value.StartsWith(StoragePrefix,Data.Ordinal))throw Data.Invalid(L.FleetLegacyResetRequired);
                if(value.Length>1500000)throw Data.Invalid(L.FleetStorageRecordInvalid);
                restoreValue=value;restorePhase=restoreAuxiliaryHolds=restoreAuxiliaryMarkers=0;restoreFleet=null;running=false;restoringRemoved.Clear();persistedOccupancies.Clear();
                restoreCursor=StoragePrefix.Length;
                return true;
            }
            public void RestoreStorageStep()
            {
                if (!Restoring) return;

                int work = 0;
                while (restoreCursor < restoreValue.Length && work++ < 4 && P.HasBudget(0.55))
                {
                    int colon = restoreValue.IndexOf(':', restoreCursor), length;
                    if (colon < restoreCursor || colon - restoreCursor > 8 || !int.TryParse(restoreValue.Substring(restoreCursor, colon - restoreCursor), out length) || length < 1 || length > 32768 || length > restoreValue.Length - colon - 1) throw Data.Invalid(L.FleetStorageRecordInvalid);
                    var ini = new MyIni(); if (!ini.TryParse(restoreValue.Substring(colon + 1, length))) throw Data.Invalid(L.FleetStorageRecordInvalid);
                    restoreCursor = colon + 1 + length; var kind = Data.Text(ini,"record", "Kind");
                    if (kind == "Fleet" && restorePhase == 0)
                    {
                        if (Data.Text(ini,"storage", "Role") != P.Config.Role || Data.Text(ini,"storage", "Id") != P.Config.Id) throw Data.Invalid(L.FleetStorageIdentityInvalid);
                        restoreFleet = ini; miners.Clear(); restorePhase = 1;
                    }
                    else if (kind == "Miner" && restorePhase == 1)
                    {
                        var t = Telemetry.FromIni(ini); if (miners.Count >= 16 || miners.ContainsKey(t.Id)) throw Data.Invalid(L.FleetStoredSizeInvalid); t.Address = Data.ReadLong(ini, "record", "Address"); t.ReceivedAt = -1e9; miners.Add(t.Id, t);
                    }
                    else if (kind == "Clearance" && restorePhase == 1 && Data.Flag(restoreFleet,"fleet", "AuxRecords"))
                    {
                        if (restoreAuxiliaryHolds >= 64 || restoreAuxiliaryHolds >= Data.ReadLong(restoreFleet, "fleet", "ClearanceCount")) throw Data.Invalid(L.FleetClearanceStorageInvalid);
                        Job hold = Job.FromIni(ini); int n = restoreAuxiliaryHolds++;
                        restoreFleet.Set("fleet", "ClearanceOwner" + n, hold.Owner); restoreFleet.Set("fleet", "ClearanceJob" + n, hold.Id); restoreFleet.Set("fleet", "ClearanceData" + n, ini.ToString());
                    }
                    else if (kind == "MarkerReceipt" && restorePhase == 1 && Data.Flag(restoreFleet,"fleet", "AuxRecords"))
                    {
                        var receipt = Data.Text(ini,"receipt", "Value");
                        if (receipt.Length > 256 || restoreAuxiliaryMarkers >= 128 || restoreAuxiliaryMarkers >= Data.ReadLong(restoreFleet, "fleet", "MarkerCount")) throw Data.Invalid(L.FleetMarkerReceiptsInvalid);
                        restoreFleet.Set("fleet", "MarkerReceipt" + (restoreAuxiliaryMarkers++), receipt);
                    }
                    else if (kind == "Planner" && restorePhase == 1) { planner.BeginRecordLoad(ini); restorePhase = 2; }
                    else if ((kind == "Job" || kind == "Region") && restorePhase == 2) planner.LoadRecord(ini, kind);
                    else if (kind == "RemoveMiners" && (restorePhase == 2 || restorePhase == 3))
                    {
                        if (restorePhase == 2) FinishFleetRecordLoad();
                        RestoreRemovedMiners(ini); restorePhase = 3;
                    }
                    else if (kind == "RemovedClearance" && restorePhase == 3)
                    {
                        Job hold = Job.FromIni(ini); long address;
                        if (!restoringRemoved.TryGetValue(hold.Owner, out address) || address != Data.ReadLong(ini, "remove", "Address")) throw Data.Invalid(L.FleetClearanceStorageInvalid);
                        if (!clearanceHolds.ContainsKey(hold.Id) && clearanceHolds.Count >= 64) throw Data.Invalid(L.FleetClearanceStorageInvalid);
                        clearanceHolds[hold.Id] = hold; persistedOccupancies.Add(hold.Id); ProtectOwner(hold.Owner, address);
                    }
                    else throw Data.Invalid(L.FleetStorageRecordInvalid);
                }
                if (restoreCursor != restoreValue.Length || !P.HasBudget(0.4)) return;
                if ((restorePhase != 2 && restorePhase != 3) || restoreFleet == null) throw Data.Invalid(L.FleetStorageRecordInvalid);
                if (restorePhase == 2) FinishFleetRecordLoad();
                restoreValue = null; restoreFleet = null; restoringRemoved.Clear(); P.Log(L.FleetStorageRestored);
            }
            void FinishFleetRecordLoad()
            {
                if (Data.Flag(restoreFleet,"fleet", "AuxRecords") && (restoreAuxiliaryHolds != Data.ReadLong(restoreFleet, "fleet", "ClearanceCount") || restoreAuxiliaryMarkers != Data.ReadLong(restoreFleet, "fleet", "MarkerCount"))) throw Data.Invalid(L.FleetStorageRecordInvalid);
                planner.FinishRecordLoad(); LoadFleet(restoreFleet, false); CapturePersistedOccupancies();
            }
            void RestoreRemovedMiners(MyIni ini)
            {
                int count = StoredCount(ini, "remove", "Count", 16), slot = StoredCount(ini, "remove", "NextWaitingSlot", 1000000);
                planner.PreserveSerial(Data.ReadLong(ini, "remove", "TaskSerial")); nextWaitingSlot = Data.Max(nextWaitingSlot, slot); restoringRemoved.Clear();
                for (int n = 0; n < count; n++)
                {
                    var id = Data.Text(ini,"remove", "Id" + n); var address = Data.ReadLong(ini, "remove", "Address" + n);
                    if (id.Length == 0 || restoringRemoved.ContainsKey(id)) throw Data.Invalid(L.FleetStoredSizeInvalid);
                    restoringRemoved[id] = address;
                }
                if (Data.Flag(ini,"remove", "HasLane"))
                {
                    ReadLanes(ini,"remove",false);tokenSerial=Data.Max(tokenSerial,StoredCount(ini,"remove","TokenSerial",1000000000));
                }
                for (int n = 0; n < count; n++)
                {
                    var id = Data.Text(ini,"remove", "Id" + n); if (Data.Flag(ini,"remove", "Cleared" + n)) ReleaseOwnerOccupancy(id);
                    ForgetMiner(id, restoringRemoved[id]);
                }
            }
            static int StoredCount(MyIni ini, string section, string key, int max)
            {
                var count = Data.ReadLong(ini, section, key); if (count < 0 || count > max) throw Data.Invalid(L.FleetStorageRecordInvalid); return (int)count;
            }
            public bool CheckpointStorage(out string value)
            {
                value = null; if (Restoring) return false;
                if (storageMiners == null) { storageMiners = Data.CreateList<string>(); storageMinerSnapshot = new List<Telemetry>(miners.Values); storageMinerCursor = 0; }
                int work = 0;
                while (storageMinerCursor < storageMinerSnapshot.Count && work++ < 2 && P.HasBudget(0.5))
                {
                    var t = storageMinerSnapshot[storageMinerCursor++]; var ini = t.ToIni(); ini.Set("record", "Address", t.Address); storageMiners.Add(TaskPlanner.StorageRecord(ini, "Miner"));
                }
                if (storageMinerCursor < storageMinerSnapshot.Count) return false;
                List<string> records; if (!planner.CheckpointRecords(out records)) return false;
                // Fleet metadata and ownership are captured on this same final frame. Telemetry
                // is restored as stale; its age and connector claims never grant a flight lane.
                var header = new MyIni(); SaveFleet(header, false, false); header.Set("fleet", "MinerCount", storageMiners.Count); header.Set("storage", "Role", P.Config.Role); header.Set("storage", "Id", P.Config.Id);
                var chunks = Data.CreateList<string>(); chunks.Add(StoragePrefix); chunks.Add(TaskPlanner.StorageRecord(header, "Fleet")); chunks.AddRange(storageMiners); AppendAuxiliaryRecords(chunks); chunks.AddRange(records); value = string.Concat(chunks); storageMiners = null; storageMinerSnapshot = null; CapturePersistedOccupancies(); return true;
            }
            public override void Save(MyIni ini)
            {
                planner.Save(ini); SaveFleet(ini);
            }
            public override bool Checkpoint(MyIni ini)
            {
                if (!planner.Checkpoint(ini)) return false;
                SaveFleet(ini); return true;
            }
            void AppendAuxiliaryRecords(List<string> chunks)
            {
                foreach (var hold in clearanceHolds.Values) chunks.Add(TaskPlanner.StorageRecord(hold.ToIni(), "Clearance"));
                foreach (var receipt in markerReceipts) { var ini = new MyIni(); ini.Set("receipt", "Value", receipt); chunks.Add(TaskPlanner.StorageRecord(ini, "MarkerReceipt")); }
            }
            void SaveFleet(MyIni ini, bool includeMiners = true, bool includeAuxiliary = true)
            {
                ini.Set("fleet","Epoch",P.Bus.Epoch);
                ini.Set("fleet", "Running", running); WriteLanes(ini,"fleet",lanes.Values); ini.Set("fleet", "TokenSerial", tokenSerial);
                ini.Set("fleet", "OperatorStopCount", operatorStops.Count); int stopIndex = 0; foreach (var id in operatorStops) ini.Set("fleet", "OperatorStop" + stopIndex++, id);
                ini.Set("fleet", "NextWaitingSlot", nextWaitingSlot); ini.Set("fleet", "ProtectedOwnerCount", protectedOwners.Count); int protectedIndex = 0;
                foreach (var owner in protectedOwners) { ini.Set("fleet", "ProtectedOwnerId" + protectedIndex, owner.Key); ini.Set("fleet", "ProtectedOwnerAddress" + protectedIndex, owner.Value); protectedIndex++; }
                ini.Set("fleet", "CancelledThrough", cancelledThrough); ini.Set("fleet", "CancellationCount", pendingCancellation.Count); int cancelIndex = 0;
                foreach (var cancellation in pendingCancellation) { ini.Set("fleet", "CancellationOwner" + cancelIndex, cancellation.Key); ini.Set("fleet", "CancellationJob" + cancelIndex, cancellation.Value); cancelIndex++; }
                ini.Set("fleet", "MinerCount", miners.Count); int n = 0;
                if (includeMiners) foreach (var t in miners.Values) { ini.Set("fleet", "Miner" + n, t.ToIni().ToString()); ini.Set("fleet", "Address" + n, t.Address); n++; }
                ini.Set("fleet", "AuxRecords", !includeAuxiliary);
                ini.Set("fleet", "ClearanceCount", clearanceHolds.Count); n = 0;
                if (includeAuxiliary) foreach (var hold in clearanceHolds.Values) { ini.Set("fleet", "ClearanceOwner" + n, hold.Owner); ini.Set("fleet", "ClearanceJob" + n, hold.Id); ini.Set("fleet", "ClearanceData" + n, hold.ToIni().ToString()); n++; }
                ini.Set("fleet", "MarkerCount", markerReceipts.Count); n = 0; if (includeAuxiliary) foreach (var receipt in markerReceipts) ini.Set("fleet", "MarkerReceipt" + (n++), receipt);
            }
            public override void Load(MyIni ini)
            {
                planner.Load(ini); LoadFleet(ini);
            }
            void LoadFleet(MyIni ini, bool includeMiners = true)
            {
                var epoch=Data.ReadLong(ini,"fleet","Epoch");if(epoch<0 || epoch>DateTime.MaxValue.Ticks)throw Data.Invalid(L.FleetStorageIdentityInvalid);P.Bus.Epoch=epoch;
                running = Data.Flag(ini,"fleet", "Running");ReadLanes(ini,"fleet",true);tokenSerial = (int)Data.ReadLong(ini, "fleet", "TokenSerial");
                cancelledThrough = Data.ReadLong(ini, "fleet", "CancelledThrough", -1); int cancellationCount = (int)Data.ReadLong(ini, "fleet", "CancellationCount");
                if (cancelledThrough < -1 || cancelledThrough > planner.TaskSerial || cancellationCount < 0 || cancellationCount > 16) throw Data.Invalid(L.FleetCancellationStorageInvalid);
                pendingCancellation.Clear(); cancellationSent.Clear();
                for (int n = 0; n < cancellationCount; n++) { var owner = Data.Text(ini,"fleet", "CancellationOwner" + n); if (owner.Length == 0 || pendingCancellation.ContainsKey(owner)) throw Data.Invalid(L.FleetCancellationStorageInvalid); pendingCancellation[owner] = Data.Text(ini,"fleet", "CancellationJob" + n); }
                int count = (int)Data.ReadLong(ini, "fleet", "MinerCount"); if (count < 0 || count > 16) throw Data.Invalid(L.FleetStoredSizeInvalid);
                if (includeMiners) miners.Clear();
                if (includeMiners) for (int n = 0; n < count; n++) { var body = new MyIni(); if (!body.TryParse(Data.Text(ini,"fleet", "Miner" + n))) continue; var t = Telemetry.FromIni(body); t.Address = Data.ReadLong(ini, "fleet", "Address" + n); t.ReceivedAt = -1e9; miners[t.Id] = t; }
                if (!includeMiners && miners.Count != count) throw Data.Invalid(L.FleetStoredSizeInvalid);
                operatorStops.Clear(); int stops = StoredCount(ini, "fleet", "OperatorStopCount", 16);
                for (int n = 0; n < stops; n++) { var id = Data.Text(ini,"fleet", "OperatorStop" + n); if (id.Length == 0 || !operatorStops.Add(id)) throw Data.Invalid(L.FleetStoredSizeInvalid); }
                nextWaitingSlot = StoredCount(ini, "fleet", "NextWaitingSlot", 1000000); var slots = new HashSet<int>();
                foreach (var t in miners.Values) if (t.WaitingSlot >= 0)
                {
                    if (t.WaitingSlot >= 1000000 || !slots.Add(t.WaitingSlot)) throw Data.Invalid(L.FleetStoredSizeInvalid);
                    nextWaitingSlot = Data.Max(nextWaitingSlot, t.WaitingSlot + 1);
                }
                foreach (var t in miners.Values) if (t.WaitingSlot < 0)
                {
                    if (nextWaitingSlot >= 1000000) throw Data.Invalid(L.FleetWaitingSlotsExhausted);
                    t.WaitingSlot = nextWaitingSlot++;
                }
                clearanceHolds.Clear(); int holds = (int)Data.ReadLong(ini, "fleet", "ClearanceCount"); if (holds < 0 || holds > 64) throw Data.Invalid(L.FleetClearanceStorageInvalid);
                for (int n = 0; n < holds; n++)
                {
                    string owner = Data.Text(ini,"fleet", "ClearanceOwner" + n), id = Data.Text(ini,"fleet", "ClearanceJob" + n); Job held = null;
                    if (ini.ContainsKey("fleet", "ClearanceData" + n)) { var body = new MyIni(); if (!body.TryParse(Data.Text(ini,"fleet", "ClearanceData" + n))) throw Data.Invalid(L.FleetClearanceStorageInvalid); held = Job.FromIni(body); if (held.Id != id) throw Data.Invalid(L.FleetClearanceStorageInvalid); }
                    else { Job job = planner.Find(id); if (job != null) held = Job.FromIni(job.ToIni()); }
                    if (held != null) { if (owner.Length == 0 || clearanceHolds.ContainsKey(held.Id)) throw Data.Invalid(L.FleetClearanceStorageInvalid); held.Owner = owner; clearanceHolds.Add(held.Id, held); }
                }
                markerReceipts.Clear(); markerReceiptSet.Clear(); int receiptCount = (int)Data.ReadLong(ini, "fleet", "MarkerCount"); if (receiptCount < 0 || receiptCount > 128) throw Data.Invalid(L.FleetMarkerReceiptsInvalid);
                for (int n = 0; n < receiptCount; n++) { var receipt = Data.Text(ini,"fleet", "MarkerReceipt" + n); markerReceipts.Enqueue(receipt); markerReceiptSet.Add(receipt); }
                foreach (var id in protectedOwners.Keys) P.Bus.ProtectPeer(id, 0); protectedOwners.Clear();
                int protectedCount = StoredCount(ini, "fleet", "ProtectedOwnerCount", 64); var protectedIds = new HashSet<string>();
                for (int n = 0; n < protectedCount; n++)
                {
                    var id = Data.Text(ini,"fleet", "ProtectedOwnerId" + n); var address = Data.ReadLong(ini, "fleet", "ProtectedOwnerAddress" + n);
                    if (id.Length == 0 || address == 0 || !protectedIds.Add(id)) throw Data.Invalid(L.FleetStoredSizeInvalid);
                    ProtectOwner(id, address);
                }
                requests.Clear();
                portYields.Clear();trafficOrder.Clear();
                // Occupancies persist, but no stale frame survives a reload.
                frames.Clear(); previousVelocity.Clear();
            }
        }
    }
}