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; public double At; } class Lane { public string Owner="", Kind="", Token=""; public long Dock; public bool Renew, Consent; public Vector3D Start, Position, Forward=Data.Forward; public double Radius=5; } readonly Dictionary lanes = new Dictionary(); readonly Dictionary miners = new Dictionary(); readonly Dictionary requests = new Dictionary(); readonly List docks = new List(); readonly List cargo = new List(); readonly Dictionary frames = new Dictionary(); readonly Dictionary stableSince = new Dictionary(); readonly Dictionary previousVelocity = new Dictionary(); readonly Dictionary> connectedBlocks = new Dictionary>(); readonly Dictionary unloadProgress = new Dictionary(); readonly Dictionary unloadCursor = new Dictionary(); readonly Dictionary dispatchedAt = new Dictionary(); readonly Dictionary minerSessions = new Dictionary(); readonly Dictionary clearanceHolds = new Dictionary(); readonly Dictionary pendingCancellation = new Dictionary(); readonly Dictionary cancellationSent = new Dictionary(); readonly HashSet operatorStops = new HashSet(); readonly Dictionary protectedOwners = new Dictionary(); readonly HashSet persistedOccupancies = new HashSet(); readonly Dictionary restoringRemoved = new Dictionary(); int nextWaitingSlot; long cancelledThrough = -1; public int CancellationCount => pendingCancellation.Count; readonly Queue markerReceipts = new Queue(); readonly HashSet markerReceiptSet = new HashSet(); readonly List occupied = new List(); readonly TaskPlanner planner; IMyShipController controller; string problem = ""; int tokenSerial, unloadMinerCursor, assignMinerCursor, assignmentJobCursor, poseCursor; readonly Dictionary poseSent = new Dictionary(); readonly Dictionary trafficSent=new Dictionary(); 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(); } public override List GetTelemetry() => new List(miners.Values); public override List 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); var cg = FindGroup("CargoGroup", "AMS Cargo"); if (cg != null) cg.GetBlocks(cargo, b => b.CubeGrid == P.Me.CubeGrid && b.HasInventory); var controls = new List(); 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; } 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) { 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) / Math.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; if (controller != null && frame.IsWindow(P.Config, P.Now)) { if (!stableSince.ContainsKey(dock.EntityId)) stableSince[dock.EntityId] = P.Now; } else stableSince.Remove(dock.EntityId); } } bool Window(long connector) { double since; DockFrame frame; return frames.TryGetValue(connector, out frame) && frame.IsWindow(P.Config, P.Now) && stableSince.TryGetValue(connector, out since) && P.Now - since >= P.Config.Number("Dock", "StableSeconds", 2, 0.5, 30); } 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); copy.WaitingLocal = new Vector3D(((slot % 4) - 1.5) * spacing, (1 + slot / 4) * spacing, -distance); copy.LaneClearance = P.Config.Number("Dock", "LaneClearance", 60, 20, 300); return copy; } void SendPoses() { if (miners.Count == 0) return; var list = new List(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>=.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[0].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; Vector3D TrafficEnd(Telemetry t) { if(!t.HasRoute)return t.Position; double speed=Math.Max(2,t.Velocity.Length()),range=Math.Max(30,speed*(speed/Math.Max(.1,t.ThrustMargin*.65)+3)+t.Radius*2); return t.Position+Data.Unit(t.RouteTarget-t.Position,Data.Zero)*Math.Min(range,Data.Distance(t.Position,t.RouteTarget)); } int TrafficPriority(Telemetry t) => lanes.ContainsKey(t.Id)?3:t.State==FlightState.Returning || t.State==FlightState.Retreat?2:1; void SendTraffic(Telemetry t) { if(t.Connected || !t.HasRoute || t.State==FlightState.DockApproach)return; bool wait=false,avoid=false;Vector3D point=Data.Zero,end=TrafficEnd(t);var peer=""; foreach(var other in miners.Values) { if(other.Id==t.Id || ConnectedDock(other)!=null)continue; var gap=Math.Max(3,t.Radius)+Math.Max(3,other.Radius)+4; var otherEnd=P.Now-other.ReceivedAt>3 && other.HasRoute?other.RouteTarget:TrafficEnd(other); if(Data.SegmentDistance(t.Position,end,other.Position,otherEnd)>=gap)continue; if(other.HasRoute && Data.Dot(Data.Unit(end-t.Position,Data.Zero),Data.Unit(otherEnd-other.Position,Data.Zero))>.8 && Data.Dot(other.Position-t.Position,end-t.Position)<0)continue; peer=other.Id; var stationary=!other.HasRoute || other.Velocity.Length()<.3; var inPath=Data.SegmentDistance(t.Position,end,other.Position,other.Position)0) || P.Now-other.ReceivedAt>3) {wait=true;break;} } // Formal docking is direct. Other ships yield before entering a // reserved approach, including a corridor temporarily empty ahead. if(!lanes.ContainsKey(t.Id))foreach(var lane in lanes.Values) { var gap=Math.Max(3,t.Radius)+lane.Radius+2; if(Data.SegmentDistance(t.Position,end,lane.Position,LaneEnd(lane))= P.Config.Integer("Fleet", "MaxMiners", 16, 1, 16)) { problem = L.FleetRegistrationLimit; return; } // 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; } t.WaitingSlot = nextWaitingSlot++; } t.ReceivedAt = P.Now; t.Address = packet.Source; t.Epoch=packet.Epoch; miners[t.Id] = t; 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(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); } public override void Receive(Packet packet) { if (Restoring) return; if(packet.Epoch!=P.Bus.Epoch) {RecallOldEpoch(packet);return;} if (packet.Kind == "HELLO" || packet.Kind == "TELEMETRY") { Register(packet); 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 (packet.Kind == "DOCK_REQUEST" || packet.Kind == "DEPART_REQUEST") { Register(packet); t=miners[packet.From]; 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) { lane.Renew = true; lane.Token = ""; } if (lane.Renew) { lane.Consent = true; lane.Kind = packet.Kind; RenewLane(t,lane); } else SendGrant(t); } } 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 (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(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", Data.Zero), direction = Data.ReadVector(packet.Body, "mark", "Direction", Data.Zero), up = Data.ReadVector(packet.Body, "mark", "Up", Data.Up); P.Log(planner.Manual(entry, direction, up, Data.ReadNumber(packet.Body, "mark", "Width", 0), Data.ReadNumber(packet.Body, "mark", "Height", 0), Data.ReadNumber(packet.Body, "mark", "Depth", 0))); } 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", 0))); } 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) + Math.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=new List();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 && t.Velocity.Length()<.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(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 && t.Velocity.Length()<.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=Math.Max(3,t.Radius),Depth=1,Outcome="Blocked"}; } var owners=new List(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(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 || t.Velocity.Length()>=.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; void ScheduleLane() { foreach(var lane in lanes.Values) {Telemetry held;if(lane.Renew && lane.Consent && miners.TryGetValue(lane.Owner,out held))RenewLane(held,lane);} if(lanes.Count>=16)return; Request best = null; var score = double.MinValue; IMyShipConnector berth = null; 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; IMyShipConnector candidate = null; if (q.Kind == "DEPART_REQUEST") { if (!running || operatorStops.Contains(t.Id) || pendingCancellation.ContainsKey(t.Id)) continue; candidate = ConnectedDock(t); if (candidate == null || !HasOwnedJob(t.Id)) continue; } else foreach (var d in docks) if (d.IsFunctional && d.Status != MyShipConnectorStatus.Connected && Window(d.EntityId) && LaneFree(NewLane(q,t,d.EntityId)) && (candidate==null || Data.DistanceSquared(t.Position,d.GetPosition()) score) { best = q; score = current; berth = candidate; } } if (best == null) return; var owner = miners[best.Owner];var granted=NewLane(best,owner,berth.EntityId);granted.Token=NewLaneToken();lanes.Add(owner.Id,granted);SendGrant(owner); } string NewLaneToken() => P.Config.Id+"-"+P.Bus.Session+"-lane-"+(++tokenSerial); void WriteLanes(MyIni ini,string section,IEnumerable 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,"LaneStart",lane.Start);Data.PutVector(ini,s,"LanePosition",lane.Position);Data.PutVector(ini,s,"LaneForward",lane.Forward);ini.Set(s,"LaneRadius",lane.Radius); } 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(); for(int n=0;n lane.Position+lane.Forward*(P.Config.Number("Dock","LaneClearance",60,20,300)+lane.Radius+2); Vector3D LaneEntry(Lane lane) => lane.Position+lane.Forward*Math.Max(P.Config.Number("Dock","ApproachDistance",30,10,300),lane.Radius*2); bool LaneFree(Lane candidate) { DockFrame current;if(!frames.TryGetValue(candidate.Dock,out current))return false; candidate.Position=current.Position;candidate.Forward=current.Forward; Vector3D end=LaneEnd(candidate),entry=LaneEntry(candidate); foreach(var other in lanes.Values) { if(other.Owner==candidate.Owner)continue; DockFrame frame;if(frames.TryGetValue(other.Dock,out frame)) {other.Position=frame.Position;other.Forward=frame.Forward;} var gap=candidate.Radius+other.Radius+2; if(other.Dock==candidate.Dock || Data.SegmentDistance(candidate.Position,end,other.Position,LaneEnd(other)) 3 || !Window(lane.Dock)) return; IMyShipConnector berth = null; foreach (var d in docks) if (d.EntityId == lane.Dock) berth = d; if (berth == null || !berth.IsFunctional) return; if (lane.Kind == "DEPART_REQUEST") { if (ConnectedDock(t) != berth) return; } else if (berth.Status == MyShipConnectorStatus.Connected) return; lane.Start=t.Position;lane.Radius=Math.Max(3,t.Radius); if(!LaneFree(lane))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 = Math.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)(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; assignmentPending=true;if(j.Owner.Length>0)continue; var score = j.Priority * 100000 - Data.Distance(t.Position, j.Entry) - j.HoleIndex * 0.001; if (score <= assignmentScore || Conflict(j, Math.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, Math.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 = ""; } 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(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 blocks; if (!connectedBlocks.TryGetValue(grid, out blocks)) { blocks = new List(); P.GridTerminalSystem.GetBlocksOfType(blocks, b => b.CubeGrid.EntityId == grid && b.HasInventory); 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 < Math.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) continue; for (int inv = 0; inv < block.InventoryCount; inv++) { var source = block.GetInventory(inv); var items = new List(); 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.Zero), Data.ReadVector(mark, "mark", "Direction", Data.Zero), Data.ReadVector(mark, "mark", "Up", Data.Up), Data.ReadNumber(mark, "mark", "Width", 0), Data.ReadNumber(mark, "mark", "Height", 0), Data.ReadNumber(mark, "mark", "Depth", 0))); 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.State) + " / " + 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(protectedOwners.Keys); foreach (var id in ids) if (!HasOccupancy(id)) { protectedOwners.Remove(id); P.Bus.ProtectPeer(id, 0); } } void EnsureOccupancyCapacity() { var ids = new HashSet(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); 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(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 = new List(); 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(); foreach (var t in removed) if (RenamedShipIsDocked(t)) verifiedClear.Add(t.Id); EnsureOccupancyCapacity(); var protectedIds = new HashSet(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(); 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=new List();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 = new List(); chunks.Add(P.Storage); chunks.Add(TaskPlanner.StorageRecord(record, "RemoveMiners")); var captured = new HashSet(); 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(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 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 = Math.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(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 = new List(); 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 storageMiners; List 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 = Math.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=Math.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 = new List(); storageMinerSnapshot = new List(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 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 = new List(); 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 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(); foreach (var t in miners.Values) if (t.WaitingSlot >= 0) { if (t.WaitingSlot >= 1000000 || !slots.Add(t.WaitingSlot)) throw Data.Invalid(L.FleetStoredSizeInvalid); nextWaitingSlot = Math.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(); 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(); // Occupancies persist, but no stale frame survives a reload. frames.Clear(); previousVelocity.Clear(); stableSince.Clear(); } } } }