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();
}
}
}
}
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();
}
}
}
}