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 trafficOrder=Data.CreateDictionary(); readonly Dictionary portYields=Data.CreateDictionary(); // 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 lanes = Data.CreateDictionary(); readonly Dictionary miners = Data.CreateDictionary(); readonly Dictionary requests = Data.CreateDictionary(); readonly List docks = Data.CreateList(); readonly Dictionary approachOffsets=Data.CreateDictionary(); readonly List cargo = Data.CreateList(); readonly Dictionary frames = Data.CreateDictionary(); readonly Dictionary previousVelocity = Data.CreateDictionary(); readonly Dictionary> connectedBlocks = Data.CreateDictionary>(); readonly Dictionary unloadProgress = Data.CreateDictionary(); readonly Dictionary unloadCursor = Data.CreateDictionary(); readonly Dictionary dispatchedAt = Data.CreateDictionary(); readonly Dictionary minerSessions = Data.CreateDictionary(); readonly Dictionary clearanceHolds = Data.CreateDictionary(); readonly Dictionary pendingCancellation = Data.CreateDictionary(); readonly Dictionary cancellationSent = Data.CreateDictionary(); readonly HashSet operatorStops = new HashSet(); readonly Dictionary protectedOwners = Data.CreateDictionary(); readonly HashSet persistedOccupancies = new HashSet(); readonly Dictionary restoringRemoved = Data.CreateDictionary(); int nextWaitingSlot; long cancelledThrough = -1; public int CancellationCount => pendingCancellation.Count; readonly Queue markerReceipts = new Queue(); readonly HashSet markerReceiptSet = new HashSet(); readonly List occupied = Data.CreateList(); readonly TaskPlanner planner; IMyShipController controller; string problem = ""; int tokenSerial, unloadMinerCursor, assignMinerCursor, assignmentJobCursor, poseCursor; readonly Dictionary poseSent = Data.CreateDictionary(); readonly Dictionary trafficSent=Data.CreateDictionary(); 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 GetTelemetry() => new List(miners.Values); public override List GetJobs() => planner.Jobs; public override MiningRegion MapRegion(string id) => planner.Region(id); public override string Diagnostics { get { if (Restoring) return RestoreStatus; int online = 0, pending = planner.PendingCount, unknown = 0; foreach (var t in miners.Values) if (P.Now - t.ReceivedAt <= OfflineSeconds) online++; else unknown++; return L.F(L.FleetDiagnostics, running ? L.FleetRunning : L.FleetPaused, online, miners.Count, unknown, docks.Count, requests.Count, LaneSummary(), pending, planner.Jobs.Count) + (planner.Generating ? L.F(L.FleetPlanningProgress, planner.PendingGeneration) + "\n" : "") + (pendingCancellation.Count > 0 ? L.F(L.FleetAwaitCancellation, pendingCancellation.Count) + "\n" : "") + (controller == null ? L.FleetControllerMissing + "\n" : "") + (docks.Count==0 ? MissingGroup("DockGroup","AMS Docks",L.FleetBerthsMissing)+"\n" : "") + (cargo.Count == 0 ? MissingGroup("CargoGroup","AMS Cargo",L.FleetCargoMissing) + "\n" : "") + problem; } } void Scan() { docks.Clear(); cargo.Clear(); connectedBlocks.Clear(); var group = FindGroup("DockGroup", "AMS Docks"); if (group != null) group.GetBlocksOfType(docks, b => b.CubeGrid == P.Me.CubeGrid); docks.Sort((a,b)=>a.EntityId.CompareTo(b.EntityId)); var order=new List(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 b.CubeGrid == P.Me.CubeGrid && b.HasInventory); var controls = Data.CreateList(); 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(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 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(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(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();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(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(lanes.Keys); foreach(var owner in owners) { var lane=lanes[owner];Telemetry t;if(!miners.TryGetValue(owner,out t))continue; if((lane.Kind=="DOCK_REQUEST" && ConnectedDock(t)!=null) || (lane.Kind=="DEPART_REQUEST" && !t.Connected && DepartureComplete(t.State) && FarFromPort(t)) || (t.State==FlightState.Fault && clearanceHolds.ContainsKey("fault-"+t.Id) && SafeStopped(t,lane))) ClearLane(owner); } // A stopped, known ship protects its actual location. An abandoned // task far away must not remain reserved on behalf of that ship. var jobs=new List(planner.OwnedJobs.Values); foreach(var j in jobs) { Telemetry t;if(!miners.TryGetValue(j.Owner,out t) || P.Now-t.ReceivedAt>3 || t.State!=FlightState.Fault || 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(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(); 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 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(); for(int n=0;n 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)(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(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(miners.Values); unloadMinerCursor %= list.Count; var t = list[unloadMinerCursor++]; var dock = ConnectedDock(t); if (dock == null || dock.OtherConnector == null) { unloadCursor.Remove(t.Id); unloadProgress.Remove(t.Id); return; } var grid = dock.OtherConnector.CubeGrid.EntityId; List blocks; if (!connectedBlocks.TryGetValue(grid, out blocks)) { blocks = Data.CreateList(); 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(); 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(protectedOwners.Keys); foreach (var id in ids) if (!HasOccupancy(id)) { protectedOwners.Remove(id); P.Bus.ProtectPeer(id, 0); } } void EnsureOccupancyCapacity() { var ids = new HashSet(clearanceHolds.Keys); foreach (var job in planner.OwnedJobs.Values) ids.Add(job.Id); if (ids.Count > 64) throw Data.Invalid(L.FleetUnknownOccupancyLimit); } void CapturePersistedOccupancies() { persistedOccupancies.Clear(); foreach (var hold in clearanceHolds.Values) persistedOccupancies.Add(hold.Id); foreach (var job in planner.OwnedJobs.Values) persistedOccupancies.Add(job.Id); } void ForgetMiner(string id, long address) { ProtectOwner(id, address); miners.Remove(id); requests.Remove(id); poseSent.Remove(id); minerSessions.Remove(id); unloadProgress.Remove(id); unloadCursor.Remove(id); 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(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(); foreach (var t in miners.Values) if (P.Now - t.ReceivedAt > OfflineSeconds) removed.Add(t); if (removed.Count == 0) { problem = L.F(L.FleetOfflineCleared, 0); P.Log(problem); return; } var verifiedClear = new HashSet(); foreach (var t in removed) if (RenamedShipIsDocked(t)) verifiedClear.Add(t.Id); EnsureOccupancyCapacity(); var protectedIds = new HashSet(protectedOwners.Keys); foreach (var t in removed) if (t.Address != 0 && HasOccupancy(t.Id)) protectedIds.Add(t.Id); if (protectedIds.Count > 64) throw Data.Invalid(L.FleetUnknownOccupancyLimit); if (!string.IsNullOrEmpty(P.Storage)) { if (!P.Storage.StartsWith(StoragePrefix, Data.Ordinal)) throw Data.Invalid(L.FleetStorageRecordInvalid); var record = new MyIni(); record.Set("remove", "Count", removed.Count); record.Set("remove", "NextWaitingSlot", nextWaitingSlot); record.Set("remove", "TaskSerial", planner.TaskSerial); var ids = new HashSet(); int n = 0; foreach (var t in removed) { ids.Add(t.Id); record.Set("remove", "Id" + n, t.Id); record.Set("remove", "Address" + n, t.Address); record.Set("remove", "Cleared" + n, verifiedClear.Contains(t.Id)); n++; } var retainedLanes=Data.CreateList();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(); chunks.Add(P.Storage); chunks.Add(TaskPlanner.StorageRecord(record, "RemoveMiners")); var captured = new HashSet(); foreach (var t in removed) { if (verifiedClear.Contains(t.Id)) continue; Job owned; if (planner.OwnedJobs.TryGetValue(t.Id, out owned) && captured.Add(owned.Id)) AppendRemovedHold(chunks, owned, t.Address); foreach (var hold in clearanceHolds.Values) if (hold.Owner == t.Id && captured.Add(hold.Id)) AppendRemovedHold(chunks, hold, t.Address); } var persisted = new HashSet(persistedOccupancies); foreach (var id in captured) persisted.Add(id); if (persisted.Count > 64) throw Data.Invalid(L.FleetUnknownOccupancyLimit); var committed = string.Concat(chunks); if (committed.Length > 1500000) throw Data.Invalid(L.FleetStorageRecordInvalid); // Persist the small removal journal before changing the registry. The // last completed planner checkpoint is never parsed in this command. P.Storage = committed; foreach (var id in captured) persistedOccupancies.Add(id); } foreach (var t in removed) { if (verifiedClear.Contains(t.Id)) ReleaseOwnerOccupancy(t.Id); ForgetMiner(t.Id, t.Address); } problem = L.F(L.FleetOfflineCleared, removed.Count); P.Log(problem); } static void AppendRemovedHold(List chunks, Job job, long address) { var record = job.ToIni(); record.Set("remove", "Address", address); chunks.Add(TaskPlanner.StorageRecord(record, "RemovedClearance")); } void ClearAllTasks() { EnsureOccupancyCapacity(); running = false; cancelledThrough = 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(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(); chunks.Add(StoragePrefix); chunks.Add(TaskPlanner.StorageRecord(fleetHeader, "Fleet")); foreach (var t in miners.Values) { var record = t.ToIni(); record.Set("record", "Address", t.Address); chunks.Add(TaskPlanner.StorageRecord(record, "Miner")); } AppendAuxiliaryRecords(chunks); var emptyPlanner = new MyIni(); planner.Save(emptyPlanner); chunks.Add(TaskPlanner.StorageRecord(emptyPlanner, "Planner")); P.Storage = string.Concat(chunks); CapturePersistedOccupancies(); P.Log(L.F(L.FleetTasksCleared, pendingCancellation.Count)); RetryCancellation(); } void SendCancellation(Telemetry t, bool force = false) { if (cancelledThrough < 0 || !pendingCancellation.ContainsKey(t.Id) || !P.HasBudget(0.65)) return; double last; if (!force && cancellationSent.TryGetValue(t.Id, out last) && P.Now - last < 2) return; var body = new MyIni(); body.Set("command", "Text", "cancel-through " + cancelledThrough.ToString(Data.Culture)); P.Bus.Send("COMMAND", t.Id, body, true, t.Address); cancellationSent[t.Id] = P.Now; } void RetryCancellation() { foreach (var t in miners.Values) if (pendingCancellation.ContainsKey(t.Id)) SendCancellation(t); } void ConfirmCancellation(Telemetry t) { string previous; if (!pendingCancellation.TryGetValue(t.Id, out previous) || t.CancelledThrough < cancelledThrough || P.Now - t.ReceivedAt > 3 || ConnectedDock(t) == null) return; if (previous.Length == 0 ? t.JobId.Length > 0 : t.JobId == previous) return; pendingCancellation.Remove(t.Id); cancellationSent.Remove(t.Id); ClearLane(t.Id); if (running && !operatorStops.Contains(t.Id) && t.State != FlightState.Manual) { var body = new MyIni(); body.Set("command", "Text", "resume"); P.Bus.Send("COMMAND", t.Id, body, true, t.Address); } if (pendingCancellation.Count == 0) P.Log(L.FleetCancellationComplete); } const string StoragePrefix = "XFEAMS-FLEET-3\n"; const string ResetPrefix = "XFEAMS-RESET-1\n"; public static string ResetStorage(long epoch) => ResetPrefix+epoch.ToString(Data.Culture); void RecallOldEpoch(Packet packet) { var body=new MyIni();body.Set("command","Text","return");body.Set("command","Reset",true);P.Bus.Send("COMMAND",packet.From,body,false,packet.Source); } string restoreValue; int restoreCursor, restorePhase, restoreAuxiliaryHolds, restoreAuxiliaryMarkers; MyIni restoreFleet; List storageMiners; List storageMinerSnapshot; int storageMinerCursor; public bool Restoring => restoreValue != null; public double RestoreProgress => !Restoring ? 1 : restoreValue.Length == 0 ? 0 : (double)restoreCursor / restoreValue.Length; public string RestoreStatus => L.F(L.FleetRestoreProgress,(RestoreProgress*100).ToString("0",Data.Culture)); public bool BeginRestoreStorage(string value) { if (string.IsNullOrWhiteSpace(value)) return false; if(value.StartsWith(ResetPrefix,Data.Ordinal)) { long epoch;if(!long.TryParse(value.Substring(ResetPrefix.Length),out epoch) || epoch<1 || epoch>DateTime.MaxValue.Ticks)throw Data.Invalid(L.FleetStorageRecordInvalid); P.Bus.Epoch=epoch;running=false;return true; } if(!value.StartsWith(StoragePrefix,Data.Ordinal))throw Data.Invalid(L.FleetLegacyResetRequired); if(value.Length>1500000)throw Data.Invalid(L.FleetStorageRecordInvalid); restoreValue=value;restorePhase=restoreAuxiliaryHolds=restoreAuxiliaryMarkers=0;restoreFleet=null;running=false;restoringRemoved.Clear();persistedOccupancies.Clear(); restoreCursor=StoragePrefix.Length; return true; } public void RestoreStorageStep() { if (!Restoring) return; int work = 0; while (restoreCursor < restoreValue.Length && work++ < 4 && P.HasBudget(0.55)) { int colon = restoreValue.IndexOf(':', restoreCursor), length; if (colon < restoreCursor || colon - restoreCursor > 8 || !int.TryParse(restoreValue.Substring(restoreCursor, colon - restoreCursor), out length) || length < 1 || length > 32768 || length > restoreValue.Length - colon - 1) throw Data.Invalid(L.FleetStorageRecordInvalid); var ini = new MyIni(); if (!ini.TryParse(restoreValue.Substring(colon + 1, length))) throw Data.Invalid(L.FleetStorageRecordInvalid); restoreCursor = colon + 1 + length; var kind = Data.Text(ini,"record", "Kind"); if (kind == "Fleet" && restorePhase == 0) { if (Data.Text(ini,"storage", "Role") != P.Config.Role || Data.Text(ini,"storage", "Id") != P.Config.Id) throw Data.Invalid(L.FleetStorageIdentityInvalid); restoreFleet = ini; miners.Clear(); restorePhase = 1; } else if (kind == "Miner" && restorePhase == 1) { var t = Telemetry.FromIni(ini); if (miners.Count >= 16 || miners.ContainsKey(t.Id)) throw Data.Invalid(L.FleetStoredSizeInvalid); t.Address = Data.ReadLong(ini, "record", "Address"); t.ReceivedAt = -1e9; miners.Add(t.Id, t); } else if (kind == "Clearance" && restorePhase == 1 && Data.Flag(restoreFleet,"fleet", "AuxRecords")) { if (restoreAuxiliaryHolds >= 64 || restoreAuxiliaryHolds >= Data.ReadLong(restoreFleet, "fleet", "ClearanceCount")) throw Data.Invalid(L.FleetClearanceStorageInvalid); Job hold = Job.FromIni(ini); int n = restoreAuxiliaryHolds++; restoreFleet.Set("fleet", "ClearanceOwner" + n, hold.Owner); restoreFleet.Set("fleet", "ClearanceJob" + n, hold.Id); restoreFleet.Set("fleet", "ClearanceData" + n, ini.ToString()); } else if (kind == "MarkerReceipt" && restorePhase == 1 && Data.Flag(restoreFleet,"fleet", "AuxRecords")) { var receipt = Data.Text(ini,"receipt", "Value"); if (receipt.Length > 256 || restoreAuxiliaryMarkers >= 128 || restoreAuxiliaryMarkers >= Data.ReadLong(restoreFleet, "fleet", "MarkerCount")) throw Data.Invalid(L.FleetMarkerReceiptsInvalid); restoreFleet.Set("fleet", "MarkerReceipt" + (restoreAuxiliaryMarkers++), receipt); } else if (kind == "Planner" && restorePhase == 1) { planner.BeginRecordLoad(ini); restorePhase = 2; } else if ((kind == "Job" || kind == "Region") && restorePhase == 2) planner.LoadRecord(ini, kind); else if (kind == "RemoveMiners" && (restorePhase == 2 || restorePhase == 3)) { if (restorePhase == 2) FinishFleetRecordLoad(); RestoreRemovedMiners(ini); restorePhase = 3; } else if (kind == "RemovedClearance" && restorePhase == 3) { Job hold = Job.FromIni(ini); long address; if (!restoringRemoved.TryGetValue(hold.Owner, out address) || address != Data.ReadLong(ini, "remove", "Address")) throw Data.Invalid(L.FleetClearanceStorageInvalid); if (!clearanceHolds.ContainsKey(hold.Id) && clearanceHolds.Count >= 64) throw Data.Invalid(L.FleetClearanceStorageInvalid); clearanceHolds[hold.Id] = hold; persistedOccupancies.Add(hold.Id); ProtectOwner(hold.Owner, address); } else throw Data.Invalid(L.FleetStorageRecordInvalid); } if (restoreCursor != restoreValue.Length || !P.HasBudget(0.4)) return; if ((restorePhase != 2 && restorePhase != 3) || restoreFleet == null) throw Data.Invalid(L.FleetStorageRecordInvalid); if (restorePhase == 2) FinishFleetRecordLoad(); restoreValue = null; restoreFleet = null; restoringRemoved.Clear(); P.Log(L.FleetStorageRestored); } void FinishFleetRecordLoad() { if (Data.Flag(restoreFleet,"fleet", "AuxRecords") && (restoreAuxiliaryHolds != Data.ReadLong(restoreFleet, "fleet", "ClearanceCount") || restoreAuxiliaryMarkers != Data.ReadLong(restoreFleet, "fleet", "MarkerCount"))) throw Data.Invalid(L.FleetStorageRecordInvalid); planner.FinishRecordLoad(); LoadFleet(restoreFleet, false); CapturePersistedOccupancies(); } void RestoreRemovedMiners(MyIni ini) { int count = StoredCount(ini, "remove", "Count", 16), slot = StoredCount(ini, "remove", "NextWaitingSlot", 1000000); planner.PreserveSerial(Data.ReadLong(ini, "remove", "TaskSerial")); nextWaitingSlot = 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(); storageMinerSnapshot = new List(miners.Values); storageMinerCursor = 0; } int work = 0; while (storageMinerCursor < storageMinerSnapshot.Count && work++ < 2 && P.HasBudget(0.5)) { var t = storageMinerSnapshot[storageMinerCursor++]; var ini = t.ToIni(); ini.Set("record", "Address", t.Address); storageMiners.Add(TaskPlanner.StorageRecord(ini, "Miner")); } if (storageMinerCursor < storageMinerSnapshot.Count) return false; List records; if (!planner.CheckpointRecords(out records)) return false; // Fleet metadata and ownership are captured on this same final frame. Telemetry // is restored as stale; its age and connector claims never grant a flight lane. var header = new MyIni(); SaveFleet(header, false, false); header.Set("fleet", "MinerCount", storageMiners.Count); header.Set("storage", "Role", P.Config.Role); header.Set("storage", "Id", P.Config.Id); var chunks = Data.CreateList(); chunks.Add(StoragePrefix); chunks.Add(TaskPlanner.StorageRecord(header, "Fleet")); chunks.AddRange(storageMiners); AppendAuxiliaryRecords(chunks); chunks.AddRange(records); value = string.Concat(chunks); storageMiners = null; storageMinerSnapshot = null; CapturePersistedOccupancies(); return true; } public override void Save(MyIni ini) { planner.Save(ini); SaveFleet(ini); } public override bool Checkpoint(MyIni ini) { if (!planner.Checkpoint(ini)) return false; SaveFleet(ini); return true; } void AppendAuxiliaryRecords(List chunks) { foreach (var hold in clearanceHolds.Values) chunks.Add(TaskPlanner.StorageRecord(hold.ToIni(), "Clearance")); foreach (var receipt in markerReceipts) { var ini = new MyIni(); ini.Set("receipt", "Value", receipt); chunks.Add(TaskPlanner.StorageRecord(ini, "MarkerReceipt")); } } void SaveFleet(MyIni ini, bool includeMiners = true, bool includeAuxiliary = true) { ini.Set("fleet","Epoch",P.Bus.Epoch); ini.Set("fleet", "Running", running); WriteLanes(ini,"fleet",lanes.Values); ini.Set("fleet", "TokenSerial", tokenSerial); ini.Set("fleet", "OperatorStopCount", operatorStops.Count); int stopIndex = 0; foreach (var id in operatorStops) ini.Set("fleet", "OperatorStop" + stopIndex++, id); ini.Set("fleet", "NextWaitingSlot", nextWaitingSlot); ini.Set("fleet", "ProtectedOwnerCount", protectedOwners.Count); int protectedIndex = 0; foreach (var owner in protectedOwners) { ini.Set("fleet", "ProtectedOwnerId" + protectedIndex, owner.Key); ini.Set("fleet", "ProtectedOwnerAddress" + protectedIndex, owner.Value); protectedIndex++; } ini.Set("fleet", "CancelledThrough", cancelledThrough); ini.Set("fleet", "CancellationCount", pendingCancellation.Count); int cancelIndex = 0; foreach (var cancellation in pendingCancellation) { ini.Set("fleet", "CancellationOwner" + cancelIndex, cancellation.Key); ini.Set("fleet", "CancellationJob" + cancelIndex, cancellation.Value); cancelIndex++; } ini.Set("fleet", "MinerCount", miners.Count); int n = 0; if (includeMiners) foreach (var t in miners.Values) { ini.Set("fleet", "Miner" + n, t.ToIni().ToString()); ini.Set("fleet", "Address" + n, t.Address); n++; } ini.Set("fleet", "AuxRecords", !includeAuxiliary); ini.Set("fleet", "ClearanceCount", clearanceHolds.Count); n = 0; if (includeAuxiliary) foreach (var hold in clearanceHolds.Values) { ini.Set("fleet", "ClearanceOwner" + n, hold.Owner); ini.Set("fleet", "ClearanceJob" + n, hold.Id); ini.Set("fleet", "ClearanceData" + n, hold.ToIni().ToString()); n++; } ini.Set("fleet", "MarkerCount", markerReceipts.Count); n = 0; if (includeAuxiliary) foreach (var receipt in markerReceipts) ini.Set("fleet", "MarkerReceipt" + (n++), receipt); } public override void Load(MyIni ini) { planner.Load(ini); LoadFleet(ini); } void LoadFleet(MyIni ini, bool includeMiners = true) { var epoch=Data.ReadLong(ini,"fleet","Epoch");if(epoch<0 || epoch>DateTime.MaxValue.Ticks)throw Data.Invalid(L.FleetStorageIdentityInvalid);P.Bus.Epoch=epoch; running = Data.Flag(ini,"fleet", "Running");ReadLanes(ini,"fleet",true);tokenSerial = (int)Data.ReadLong(ini, "fleet", "TokenSerial"); cancelledThrough = Data.ReadLong(ini, "fleet", "CancelledThrough", -1); int cancellationCount = (int)Data.ReadLong(ini, "fleet", "CancellationCount"); if (cancelledThrough < -1 || cancelledThrough > planner.TaskSerial || cancellationCount < 0 || cancellationCount > 16) throw Data.Invalid(L.FleetCancellationStorageInvalid); pendingCancellation.Clear(); cancellationSent.Clear(); for (int n = 0; n < cancellationCount; n++) { var owner = Data.Text(ini,"fleet", "CancellationOwner" + n); if (owner.Length == 0 || pendingCancellation.ContainsKey(owner)) throw Data.Invalid(L.FleetCancellationStorageInvalid); pendingCancellation[owner] = Data.Text(ini,"fleet", "CancellationJob" + n); } int count = (int)Data.ReadLong(ini, "fleet", "MinerCount"); if (count < 0 || count > 16) throw Data.Invalid(L.FleetStoredSizeInvalid); if (includeMiners) miners.Clear(); if (includeMiners) for (int n = 0; n < count; n++) { var body = new MyIni(); if (!body.TryParse(Data.Text(ini,"fleet", "Miner" + n))) continue; var t = Telemetry.FromIni(body); t.Address = Data.ReadLong(ini, "fleet", "Address" + n); t.ReceivedAt = -1e9; miners[t.Id] = t; } if (!includeMiners && miners.Count != count) throw Data.Invalid(L.FleetStoredSizeInvalid); operatorStops.Clear(); int stops = StoredCount(ini, "fleet", "OperatorStopCount", 16); for (int n = 0; n < stops; n++) { var id = Data.Text(ini,"fleet", "OperatorStop" + n); if (id.Length == 0 || !operatorStops.Add(id)) throw Data.Invalid(L.FleetStoredSizeInvalid); } nextWaitingSlot = StoredCount(ini, "fleet", "NextWaitingSlot", 1000000); var slots = new HashSet(); foreach (var t in miners.Values) if (t.WaitingSlot >= 0) { if (t.WaitingSlot >= 1000000 || !slots.Add(t.WaitingSlot)) throw Data.Invalid(L.FleetStoredSizeInvalid); nextWaitingSlot = 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(); 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(); } } } }