using System;
using System.Collections.Generic;
using Sandbox.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame.Utilities;
using VRageMath;
namespace AutoMiningScript
{
public partial class Program
{
// All coordinates are frozen in world space. A moving base cannot drag a mine.
public class MiningRegion
{
public string Id = "", Parent = "", Ore = "", Status = "Pending";
public Vector3D Center, Direction = Data.Forward, Up = Data.Up;
public double Radius, Width, Height, Depth;
public long EntityId;
public bool Gravity, Search, Patch, RingFound;
public int Ring, EmptyRings;
// A persisted cursor replaces command-time loops over hundreds of holes.
public string Generation="";
public int Cursor, Total, Columns, Rows, Added;
public double Spacing, ScanDistance, BaseClearance;
public void WriteGeneration(MyIni i,string s)
{
i.Set(s,"Generation",Generation);i.Set(s,"Cursor",Cursor);i.Set(s,"Total",Total);i.Set(s,"Columns",Columns);i.Set(s,"Rows",Rows);i.Set(s,"Added",Added);
i.Set(s,"Spacing",Spacing);i.Set(s,"ScanDistance",ScanDistance);i.Set(s,"BaseClearance",BaseClearance);
}
public void Write(MyIni i, string s)
{
i.Set(s,"Id",Id); i.Set(s,"Parent",Parent); i.Set(s,"Ore",Ore); i.Set(s,"Status",Status);
Data.PutVector(i,s,"Center",Center); Data.PutVector(i,s,"Direction",Direction); Data.PutVector(i,s,"Up",Up);
i.Set(s,"Radius",Radius); i.Set(s,"Width",Width); i.Set(s,"Height",Height); i.Set(s,"Depth",Depth);
i.Set(s,"EntityId",EntityId);WriteGeneration(i,s);
i.Set(s,"Gravity",Gravity); i.Set(s,"Search",Search); i.Set(s,"Patch",Patch); i.Set(s,"RingFound",RingFound); i.Set(s,"Ring",Ring); i.Set(s,"EmptyRings",EmptyRings);
}
public static MiningRegion Read(MyIni i,string s)
=> new MiningRegion { Id=Data.Text(i,s,"Id"),Parent=Data.Text(i,s,"Parent"),Ore=Data.Text(i,s,"Ore"),Status=Data.Text(i,s,"Status"),
Center=Data.ReadVector(i,s,"Center"),Direction=Data.Unit(Data.ReadVector(i,s,"Direction",Data.Forward),Data.Forward),Up=Data.ReadVector(i,s,"Up",Data.Up),
Radius=Data.ReadNumber(i,s,"Radius",300),Width=Data.ReadNumber(i,s,"Width",60),Height=Data.ReadNumber(i,s,"Height",60),Depth=Data.ReadNumber(i,s,"Depth",40),EntityId=Data.ReadLong(i,s,"EntityId"),
Generation=Data.Text(i,s,"Generation"),Cursor=(int)Data.ReadLong(i,s,"Cursor"),Total=(int)Data.ReadLong(i,s,"Total"),Columns=(int)Data.ReadLong(i,s,"Columns"),Rows=(int)Data.ReadLong(i,s,"Rows"),Added=(int)Data.ReadLong(i,s,"Added"),Spacing=Data.ReadNumber(i,s,"Spacing"),ScanDistance=Data.ReadNumber(i,s,"ScanDistance"),BaseClearance=Data.ReadNumber(i,s,"BaseClearance"),
Gravity=Data.Flag(i,s,"Gravity"),Search=Data.Flag(i,s,"Search"),Patch=Data.Flag(i,s,"Patch"),RingFound=Data.Flag(i,s,"RingFound"),Ring=(int)Data.ReadLong(i,s,"Ring"),EmptyRings=(int)Data.ReadLong(i,s,"EmptyRings") };
}
public class TaskPlanner
{
readonly Program p;
public readonly List<Job> Jobs = new List<Job>();
public readonly List<MiningRegion> Regions = new List<MiningRegion>();
readonly HashSet<string> discoveries = new HashSet<string>();
readonly Queue<string> advanceQueue=new Queue<string>();
readonly HashSet<string> advanceQueued=new HashSet<string>();
MyIni checkpointTarget;
int checkpointJobs,checkpointRegions,checkpointJobCursor,checkpointRegionCursor;
readonly HashSet<Job> checkpointDirtyJobs=new HashSet<Job>();
readonly HashSet<MiningRegion> checkpointDirtyRegions=new HashSet<MiningRegion>();
readonly Dictionary<MiningRegion,int> checkpointRegionIndexes=new Dictionary<MiningRegion,int>();
readonly Queue<MiningRegion> generation=new Queue<MiningRegion>();
readonly Dictionary<string,Job> jobsById=new Dictionary<string,Job>();
readonly Dictionary<string,List<Job>> surfaceCells=new Dictionary<string,List<Job>>();
readonly Dictionary<string,MiningRegion> regionsById=new Dictionary<string,MiningRegion>();
int reserved,serial,advanceCursor,advanceJobCount,advanceRegionCount;
MiningRegion advancing;
bool advancePending,advanceBlocked;
public bool Generating => generation.Count>0;
public int PendingCount {get;private set;}
public readonly Dictionary<string,Job> OwnedJobs=new Dictionary<string,Job>();
public void SetOwner(Job j,string owner) {if(j.Owner.Length>0)OwnedJobs.Remove(j.Owner);j.Owner=owner;if(owner.Length>0) {OwnedJobs[owner]=j;j.LastOwner=owner;Dirty(j);}}
public int PendingGeneration => reserved;
public int TaskSerial => serial;
public void PreserveSerial(long value)
{
if(value<0 || value>1000000000)throw Data.Invalid(L.FleetPlannerSerialInvalid);
serial=Data.Max(serial,(int)value);
}
public void ClearTasks()
{
// Task identifiers remain monotonic across clear/reload, so late packets
// cannot alias a newly generated hole. Configuration is not touched.
ClearLoadedTasks();advanceCursor=advanceJobCount=advanceRegionCount=0;advancePending=advanceBlocked=false;
if(checkpointTarget!=null)checkpointTarget.Clear();checkpointTarget=null;
checkpointJobs=checkpointRegions=checkpointJobCursor=checkpointRegionCursor=0;
checkpointDirtyJobs.Clear();checkpointDirtyRegions.Clear();checkpointRegionIndexes.Clear();
recordSaving=false;recordJobCursor=recordRegionCursor=0;savedJobs.Clear();savedRegions.Clear();restoreHeader=null;
}
void AddRegion(MiningRegion r) {Regions.Add(r);regionsById.Add(r.Id,r);}
void Generate(MiningRegion r) {generation.Enqueue(r);reserved+=r.Total-r.Cursor;}
void Dirty(MiningRegion r) {if(checkpointTarget!=null||recordSaving)checkpointDirtyRegions.Add(r);}
void Dirty(Job j) {if(checkpointTarget!=null||recordSaving)checkpointDirtyJobs.Add(j);}
public TaskPlanner(Program program) { p=program; }
double N(string section,string key,double fallback,double min,double max) => p.Config.Number(section,key,fallback,min,max);
int Limit => p.Config.Integer("Survey","MaxJobs",2048,32,4096);
public double FootprintWidth => N("Mining","FootprintWidth",6,0.5,100);
public double FootprintHeight => N("Mining","FootprintHeight",6,0.5,100);
double StepX => FootprintWidth*(1-N("Mining","Overlap",0.15,0,0.8));
double StepY => FootprintHeight*(1-N("Mining","Overlap",0.15,0,0.8));
public string[] Ores => p.Config.Text("Mining","TargetOres","").Split(',');
string Id(string type) => p.Config.Id+"-"+type+"-"+(++serial).ToString(Data.Culture);
public Job Find(string id) {Job j;return jobsById.TryGetValue(id,out j)?j:null;}
public MiningRegion Region(string id) {MiningRegion r;return regionsById.TryGetValue(id,out r)?r:null;}
public static bool Finished(Job j) => j.Outcome=="Complete" || j.Outcome=="Blocked" || j.Outcome=="SurveyHit" || j.Outcome=="SurveyEmpty" || j.Outcome=="InvalidSample";
public int OrePriority(string ore)
{
string[] ores=p.Config.Text("Mining","PriorityOres","Iron,Cobalt,Nickel,Silicon").Split(','), found=ore.Split(',');
for(int n=0;n<ores.Length;n++) for(int f=0;f<found.Length;f++) if(found[f].Trim().Length>0 && IsTarget(found[f].Trim()) && string.Equals(ores[n].Trim(),found[f].Trim(),Data.IgnoreCase)) return ores.Length-n;
return 0;
}
bool IsTarget(string ore)
{
bool any=false;foreach(var raw in Ores) {var target=raw.Trim();if(target.Length==0)continue;any=true;if(string.Equals(target,ore,Data.IgnoreCase))return true;}return !any;
}
public string PreferredOre(string ore)
{
string best="";int priority=-1;
foreach(var raw in ore.Split(',')) {var found=raw.Trim();if(found.Length==0||!IsTarget(found))continue;var score=OrePriority(found);if(score>priority){best=found;priority=score;}}
return best;
}
public int JobPriority(Job j)
{
if(j.Kind==JobKind.Manual)return 100;if(j.Kind==JobKind.Survey)return 0;
var r=Region(j.RegionId);var ore=r!=null&&r.Patch?r.Ore:j.TargetOre;
return ore.Length>0&&PreferredOre(ore).Length==0?-1:10+OrePriority(ore);
}
static string MergeOres(string first,string second)
{
var result=new List<string>();var seen=new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach(var raw in (first+","+second).Split(',')) {var ore=raw.Trim();if(ore.Length>0&&seen.Add(ore))result.Add(ore);}return string.Join(",",result);
}
string SurfaceKey(long entity,double x,double y,double z) => entity.ToString(Data.Culture)+":"+x.ToString(Data.Culture)+":"+y.ToString(Data.Culture)+":"+z.ToString(Data.Culture);
void IndexSurface(Job j)
{
if(j.Kind==JobKind.Survey||j.EntityId==0)return;
var cell=Data.Max(StepX,StepY);var key=SurfaceKey(j.EntityId,Math.Floor(j.Entry.X/cell),Math.Floor(j.Entry.Y/cell),Math.Floor(j.Entry.Z/cell));List<Job> bucket;
if(!surfaceCells.TryGetValue(key,out bucket))surfaceCells[key]=bucket=new List<Job>();bucket.Add(j);
}
bool KnownSurface(JobReport report)
{
double cell=Data.Max(StepX,StepY),x=Math.Floor(report.Position.X/cell),y=Math.Floor(report.Position.Y/cell),z=Math.Floor(report.Position.Z/cell);
for(int dx=-1;dx<=1;dx++)for(int dy=-1;dy<=1;dy++)for(int dz=-1;dz<=1;dz++)
{
List<Job> bucket;if(!surfaceCells.TryGetValue(SurfaceKey(report.EntityId,x+dx,y+dy,z+dz),out bucket))continue;
foreach(Job j in bucket)if(Data.DistanceSquared(j.Entry,report.Position)<cell*cell)return true;
}
return false;
}
Job Add(MiningRegion r,JobKind kind,Vector3D entry,Vector3D direction,double depth,long entity=0)
{
if(Jobs.Count+reserved>=Limit) { r.Status="Sampling incomplete: job budget limit"; return null; }
var j=new Job {Id=Id("job"),RegionId=r.Id,Kind=kind,Entry=entry,Direction=Data.Unit(direction,Data.Forward),Up=Data.Perpendicular(direction,r.Up),Depth=depth,
Radius=N("Mining","HoleRadius",3,0.25,50),HoleIndex=Jobs.Count,EntityId=entity==0?r.EntityId:entity,Priority=kind==JobKind.Manual?100:kind==JobKind.Probe?10+OrePriority(r.Ore):0,TargetOre=r.Ore};
Jobs.Add(j);jobsById.Add(j.Id,j);IndexSurface(j);PendingCount++; return j;
}
public string Manual(Vector3D entry,Vector3D inward,Vector3D up,double width,double height,double depth)
{
if(!Data.Finite(width)||!Data.Finite(height)||!Data.Finite(depth)||width<=0||height<=0||depth<=0||width>1000||height>1000||depth>1000) throw Data.Invalid(L.FleetMarkDimensions);
if(inward.LengthSquared()<0.01) throw Data.Invalid(L.FleetGpsPointsDiffer);
int nx=Data.Max(1,1+(int)Math.Ceiling(Data.Max(0,width-FootprintWidth)/StepX));
int ny=Data.Max(1,1+(int)Math.Ceiling(Data.Max(0,height-FootprintHeight)/StepY));
if((long)nx*ny+Jobs.Count+reserved>Limit) throw Data.Invalid(L.FleetManualJobLimit);
var r=new MiningRegion {Id=Id("manual"),Center=entry,Direction=Data.Unit(inward,Data.Forward),Up=Data.Perpendicular(inward,up),Width=width,Height=height,Depth=depth};
r.Generation="Manual";r.Columns=nx;r.Rows=ny;r.Total=nx*ny;
AddRegion(r);Generate(r);GenerateStep();
return L.F(L.FleetManualQueued,r.Id,nx*ny);
}
public string Survey(Vector3D center,Vector3D gravity,Vector3D forward,Vector3D up,double requestedRadius)
{
var planet=gravity.LengthSquared()>0.01;
var radius=requestedRadius>0?requestedRadius:N("Survey","Radius",planet?300:2000,20,20000);
if(!Data.Finite(radius)||radius<20||radius>20000) throw Data.Invalid(L.FleetSurveyRadius);
var r=new MiningRegion {Id=Id("search"),Center=center,Direction=Data.Unit(planet?gravity:forward,Data.Forward),Up=Data.Perpendicular(planet?gravity:forward,up),Radius=radius,Gravity=planet,Search=true,Depth=N("Survey","ProbeDepth",planet?60:40,1,100),BaseClearance=N("Survey","BaseClearance",80,10,2000)};
if(planet)
{
r.Spacing=N("Survey","Spacing",60,5,2000);int cells=(int)Math.Floor(radius/r.Spacing);
var estimate=(long)(cells*2+1)*(cells*2+1);
if(estimate>Limit-Jobs.Count-reserved) throw Data.Invalid(L.FleetSurveyGridLimit);
r.Generation="Ground";r.Columns=cells;r.Total=(int)estimate;r.ScanDistance=N("Survey","ScanDistance",Data.Min(10000,Data.Max(300,radius)),50,10000);
}
else
{
r.Columns=p.Config.Integer("Survey","SpaceRays",42,12,128);r.Total=7*r.Columns;
if(Jobs.Count+reserved+r.Total>Limit) throw Data.Invalid(L.FleetSpaceSurveyLimit);
r.Generation="Space";
}
AddRegion(r);Generate(r);GenerateStep();
return L.F(L.FleetSurveyQueued,r.Id);
}
// Enumerate each ring perimeter once, rather than scanning every inner square.
static void RingPoint(int index,out int x,out int y)
{
if(index==0) {x=y=0;return;}
int ring=(int)Math.Ceiling((Math.Sqrt(index+1)-1)/2),side=ring*2;
int k=index-(side-1)*(side-1);
if(k<side) {x=-ring+k;y=-ring;}
else if(k<side*2) {x=ring;y=-ring+k-side;}
else if(k<side*3) {x=ring-(k-side*2);y=ring;}
else {x=-ring;y=ring-(k-side*3);}
}
public void GenerateStep()
{
int work=0;
while(generation.Count>0&&work++<8&&p.HasBudget(0.55))
{
var r=generation.Peek();int index=r.Cursor++;reserved--;Dirty(r);
Vector3D right=Data.Cross(r.Direction,r.Up),entry=r.Center,dir=r.Direction;var depth=r.Depth;var kind=JobKind.Probe;var add=true;
if(r.Generation=="Manual")
{
int y=index/r.Columns,column=index%r.Columns,x=y%2==0?column:r.Columns-column-1;
double spanX=Data.Max(0,r.Width-FootprintWidth),spanY=Data.Max(0,r.Height-FootprintHeight);
entry+=right*(r.Columns==1?0:-spanX/2+spanX*x/(r.Columns-1))+r.Up*(r.Rows==1?0:-spanY/2+spanY*y/(r.Rows-1));kind=JobKind.Manual;
}
else if(r.Generation=="Ground")
{
int x,y;RingPoint(index,out x,out y);add=x*x+y*y<=r.Columns*r.Columns;
entry+=-r.Direction*r.BaseClearance+right*x*r.Spacing+r.Up*y*r.Spacing;depth=r.ScanDistance;kind=JobKind.Survey;
}
else if(r.Generation=="Space")
{
int point=index/r.Columns,ray=index%r.Columns;var basis=Data.Frame(r.Center,r.Direction,r.Up);
var offset=point==1?basis.Right:point==2?basis.Left:point==4?basis.Down:point==5?basis.Forward:point==6?basis.Backward:basis.Up;
double y=1-2*(ray+0.5)/r.Columns,phi=ray*2.399963229728653,radial=Math.Sqrt(Data.Max(0,1-y*y));
dir=basis.Right*(Math.Cos(phi)*radial)+basis.Up*y+basis.Forward*(Math.Sin(phi)*radial);
entry+=offset*(point==0?Data.Min(r.BaseClearance,r.Radius*0.2):r.Radius*0.35);
var local=entry-r.Center;var dot=Data.Dot(local,dir);depth=Data.Min(10000,-dot+Math.Sqrt(dot*dot+r.Radius*r.Radius-local.LengthSquared()));kind=JobKind.Survey;
}
else if(r.Generation=="Ring")
{
int x,y;RingPoint((2*r.Ring-1)*(2*r.Ring-1)+index,out x,out y);
entry+=right*x*StepX+r.Up*y*StepY;var search=Region(r.Parent);
add=Math.Abs(x)<=r.Columns&&Math.Abs(y)<=r.Rows&&(search==null||Inside(search,entry));depth=BoundDepth(search,entry,dir,depth);add=add&&depth>=1;
}
else throw Data.Invalid(L.FleetStoredRegionInvalid);
if(add&&Add(r,kind,entry,dir,depth)!=null)r.Added++;
if(r.Cursor>=r.Total)
{
generation.Dequeue();var emptyRing=r.Generation=="Ring"&&r.Added==0;r.Generation="";
if(emptyRing) {r.Status="Sampling complete: boundary";QueueAdvance(r.Parent);}
else QueueAdvance(r.Id);
}
}
}
bool Inside(MiningRegion r,Vector3D point)
{
var d=point-r.Center;
if(r.Gravity) {d-=r.Direction*Data.Dot(d,r.Direction); return d.LengthSquared()<=r.Radius*r.Radius;}
return d.LengthSquared()<=r.Radius*r.Radius;
}
double BoundDepth(MiningRegion r,Vector3D entry,Vector3D direction,double depth)
{
if(r==null||r.Gravity) return depth;
var d=entry-r.Center;double along=Data.Dot(d,direction),disc=along*along+r.Radius*r.Radius-d.LengthSquared();
return Data.Min(depth,Data.Max(0,-along+Math.Sqrt(Data.Max(0,disc))));
}
public void Report(Job j,JobReport report)
{
Dirty(j);
j.ConfirmedOres=MergeOres(j.ConfirmedOres,report.Ore);
if(j.EntityId==0&&report.EntityId!=0){j.EntityId=report.EntityId;IndexSurface(j);}
var existing=Region(j.RegionId);
if(report.Ore.Length>0&&existing!=null&&existing.Patch) {existing.Ore=MergeOres(existing.Ore,j.ConfirmedOres);Dirty(existing);}
if(Finished(j)) return;
if (j.Kind == JobKind.Survey && report.Scan != null) j.Scan = report.Scan;
j.Progress=Data.Max(j.Progress,Data.Clamp(report.Progress,0,j.Depth));
// A terminal report carries accumulated ores, so a reordered/dropped Discovery
// message cannot erase a confirmed find. Processing is idempotent per hole.
if(report.Outcome=="Complete"&&report.Ore.Length>0)
Report(j,new JobReport {JobId=j.Id,Outcome="Discovery",Ore=report.Ore,Progress=report.Progress,Position=report.Position,Direction=report.Direction,Up=report.Up,EntityId=report.EntityId});
if(report.Outcome=="Discovery")
{
var ore=PreferredOre(report.Ore); if(ore.Length==0) return;
var first=discoveries.Add(j.Id);
if(existing!=null && existing.Patch) {existing.RingFound=true;Dirty(existing);return;}
if(!first) return;
if(j.Kind!=JobKind.Probe || existing==null) return;
var patch=new MiningRegion {Id=Id("deposit"),Parent=existing.Id,Center=j.Entry,Direction=j.Direction,Up=j.Up,Patch=true,RingFound=true,Ore=j.ConfirmedOres,
Width=N("Survey","ExpansionWidth",60,1,100),Height=N("Survey","ExpansionHeight",60,1,100),Depth=N("Survey","MaxDepth",100,1,100),Radius=existing.Radius,Gravity=existing.Gravity,EntityId=j.EntityId};
AddRegion(patch); j.RegionId=patch.Id; j.TargetOre=ore; return;
}
if(report.Outcome=="Paused") return;
if(report.Outcome=="SurveyHit" && j.Kind==JobKind.Survey)
{
var r=Region(j.RegionId);
var known=KnownSurface(report);
if(r!=null&&!known&&Inside(r,report.Position)&&report.EntityId!=0&&report.Direction.LengthSquared()>0.1)
{
var direction=Data.Unit(report.Direction,Data.Forward);var depth=BoundDepth(r,report.Position,direction,r.Depth);
if(depth>=1) Add(r,JobKind.Probe,report.Position,direction,depth,report.EntityId);
}
}
j.Outcome=report.Outcome;
if(Finished(j)) {PendingCount--;SetOwner(j,"");QueueAdvance(j.RegionId);}
}
void QueueAdvance(string id) {if(id.Length>0&&advanceQueued.Add(id)) advanceQueue.Enqueue(id);}
public void Advance()
{
GenerateStep();
if(!p.HasBudget(0.55))return;
if(advancing==null)
{
if(advanceQueue.Count==0)return;
var id=advanceQueue.Dequeue();advanceQueued.Remove(id);advancing=Region(id);advanceCursor=0;advanceJobCount=Jobs.Count;advanceRegionCount=Regions.Count;advancePending=advanceBlocked=false;
if(advancing==null||advancing.Status!="Pending"||advancing.Generation.Length>0) {advancing=null;return;}
}
// Appended jobs must not move the boundary under a cursor already scanning
// child regions. Restart the summary before considering a region complete.
if(advanceJobCount!=Jobs.Count||advanceRegionCount!=Regions.Count) {advanceCursor=0;advanceJobCount=Jobs.Count;advanceRegionCount=Regions.Count;advancePending=advanceBlocked=false;}
var r=advancing;int work=0;
while(advanceCursor<Jobs.Count+Regions.Count&&work++<64&&p.HasBudget(0.55))
{
int index=advanceCursor++;
if(index<Jobs.Count)
{
Job j=Jobs[index];if(j.RegionId==r.Id) {if(!Finished(j))advancePending=true;if(j.Outcome=="Blocked"||j.Outcome=="InvalidSample")advanceBlocked=true;}
}
else if(r.Search)
{
var child=Regions[index-Jobs.Count];if(child.Parent==r.Id) {if(child.Status=="Pending")advancePending=true;if(child.Status.IndexOf("incomplete",Data.IgnoreCase)>=0)advanceBlocked=true;}
}
}
if(advanceCursor<Jobs.Count+Regions.Count)return;
advancing=null;Dirty(r);
if(r.Search) {if(!advancePending)r.Status=advanceBlocked?"Sampling incomplete: blocked observations":"Sampling complete (not proof of no ore)";return;}
if(advancePending)return;
if(!r.Patch) {r.Status=advanceBlocked?"Range incomplete: blocked":"Specified range complete";return;}
if(advanceBlocked) {r.Status="Sampling incomplete: blocked or invalid sample";QueueAdvance(r.Parent);return;}
if(!r.RingFound)r.EmptyRings++;else r.EmptyRings=0;
if(r.EmptyRings>=2) {r.Status="Sampling complete: two empty rings";QueueAdvance(r.Parent);return;}
r.RingFound=false;r.Ring++;
r.Columns=(int)Math.Floor(r.Width/(2*StepX));r.Rows=(int)Math.Floor(r.Height/(2*StepY));
if(r.Ring>Data.Max(r.Columns,r.Rows)) {r.Status="Sampling complete: range limit";QueueAdvance(r.Parent);return;}
r.Total=8*r.Ring;r.Cursor=r.Added=0;
if(Jobs.Count+reserved+r.Total>Limit) {r.Status="Sampling incomplete: job budget limit";QueueAdvance(r.Parent);return;}
r.Generation="Ring";Generate(r);
}
public void Save(MyIni ini)
{
for(int n=0;n<Jobs.Count;n++) Jobs[n].Write(ini,"job"+n);
for(int n=0;n<Regions.Count;n++) Regions[n].Write(ini,"region"+n);
WriteCheckpointHeader(ini);
}
void WriteCheckpointHeader(MyIni ini,bool includeDiscoveries=true)
{
ini.Set("planner","Serial",serial);ini.Set("planner","Jobs",Jobs.Count);ini.Set("planner","Regions",Regions.Count);
if(includeDiscoveries)ini.Set("planner","Discoveries",string.Join(";",new List<string>(discoveries)));
// This small final ownership table overrides owners copied in earlier frames.
var ownership=includeDiscoveries?(IEnumerable<Job>)Jobs:OwnedJobs.Values;
int owners=0;foreach(Job j in ownership) if(j.Owner.Length>0&&!Finished(j)) {ini.Set("planner","OwnerJob"+owners,j.Id);ini.Set("planner","OwnerId"+owners,j.Owner);owners++;}
ini.Set("planner","Owners",owners);
}
public bool Checkpoint(MyIni ini)
{
if(checkpointTarget!=ini)
{
checkpointTarget=ini;checkpointJobs=Jobs.Count;checkpointRegions=Regions.Count;checkpointJobCursor=checkpointRegionCursor=0;checkpointDirtyJobs.Clear();checkpointDirtyRegions.Clear();checkpointRegionIndexes.Clear();
}
// Lists are append-only: extend the tail without restarting already copied work.
checkpointJobs=Jobs.Count;checkpointRegions=Regions.Count;
int work=0;
while(checkpointJobCursor<checkpointJobs&&work++<4&&p.HasBudget(0.55)) {Jobs[checkpointJobCursor].Write(ini,"job"+checkpointJobCursor);checkpointJobCursor++;}
if(checkpointJobCursor<checkpointJobs) return false;
work=0;
while(checkpointRegionCursor<checkpointRegions&&work++<4&&p.HasBudget(0.55)) {checkpointRegionIndexes[Regions[checkpointRegionCursor]]=checkpointRegionCursor;Regions[checkpointRegionCursor].Write(ini,"region"+checkpointRegionCursor);checkpointRegionCursor++;}
if(checkpointRegionCursor<checkpointRegions) return false;
if(checkpointDirtyJobs.Count>0)
{
var dirty=new List<Job>(checkpointDirtyJobs);int count=Data.Min(4,dirty.Count);
for(int n=0;n<count&&p.HasBudget(0.55);n++) {int index=dirty[n].HoleIndex;if(index>=0&&index<Jobs.Count&&Jobs[index]==dirty[n])dirty[n].Write(ini,"job"+index);else throw Data.Invalid(L.FleetCheckpointJobIndexMismatch);checkpointDirtyJobs.Remove(dirty[n]);}
return false;
}
if(checkpointDirtyRegions.Count>0)
{
var dirty=new List<MiningRegion>(checkpointDirtyRegions);int count=Data.Min(4,dirty.Count);
for(int n=0;n<count;n++) {int index;if(checkpointRegionIndexes.TryGetValue(dirty[n],out index))dirty[n].Write(ini,"region"+index);checkpointDirtyRegions.Remove(dirty[n]);}
return false;
}
WriteCheckpointHeader(ini);checkpointTarget=null;return true;
}
bool recordSaving;
readonly List<string> savedJobs=new List<string>(),savedRegions=new List<string>();
int recordJobCursor,recordRegionCursor;
MyIni restoreHeader;
public static string StorageRecord(MyIni ini,string kind)
{
ini.Set("record","Kind",kind);var value=ini.ToString();if(value.Length>32768)throw Data.Invalid(L.FleetStorageRecordInvalid);return value.Length.ToString(Data.Culture)+":"+value;
}
string JobRecord(Job j)
{
var ini=new MyIni();j.Write(ini,"job");ini.Set("record","Discovered",discoveries.Contains(j.Id));return StorageRecord(ini,"Job");
}
string RegionRecord(MiningRegion r) {var ini=new MyIni();r.Write(ini,"region");return StorageRecord(ini,"Region");}
public bool CheckpointRecords(out List<string> records)
{
records=null;
if(!recordSaving)
{
recordSaving=true;recordJobCursor=recordRegionCursor=0;savedJobs.Clear();savedRegions.Clear();checkpointDirtyJobs.Clear();checkpointDirtyRegions.Clear();checkpointRegionIndexes.Clear();
}
int work=0;
while(recordJobCursor<Jobs.Count&&work++<4&&p.HasBudget(0.55)) {savedJobs.Add(JobRecord(Jobs[recordJobCursor++]));}
if(recordJobCursor<Jobs.Count)return false;
work=0;
while(recordRegionCursor<Regions.Count&&work++<4&&p.HasBudget(0.55)) {var r=Regions[recordRegionCursor];checkpointRegionIndexes[r]=recordRegionCursor++;savedRegions.Add(RegionRecord(r));}
if(recordRegionCursor<Regions.Count)return false;
if(checkpointDirtyJobs.Count>0)
{
var dirty=new List<Job>(checkpointDirtyJobs);work=0;
foreach(Job j in dirty) {if(work++>=4||!p.HasBudget(0.55))break;savedJobs[j.HoleIndex]=JobRecord(j);checkpointDirtyJobs.Remove(j);}return false;
}
if(checkpointDirtyRegions.Count>0)
{
var dirty=new List<MiningRegion>(checkpointDirtyRegions);work=0;
foreach(var r in dirty) {if(work++>=4||!p.HasBudget(0.55))break;savedRegions[checkpointRegionIndexes[r]]=RegionRecord(r);checkpointDirtyRegions.Remove(r);}return false;
}
if(!p.HasBudget(0.35))return false;
var header=new MyIni();WriteCheckpointHeader(header,false);records=new List<string>();records.Add(StorageRecord(header,"Planner"));records.AddRange(savedJobs);records.AddRange(savedRegions);recordSaving=false;return true;
}
public void BeginRecordLoad(MyIni header)
{
var storedSerial=Data.ReadLong(header,"planner","Serial");int jobs=(int)Data.ReadLong(header,"planner","Jobs"),regions=(int)Data.ReadLong(header,"planner","Regions");
if(storedSerial<0||storedSerial>1000000000||jobs<0||jobs>4096||regions<0||regions>4096)throw Data.Invalid(L.FleetPlannerCountsInvalid);
serial=(int)storedSerial;restoreHeader=header;ClearLoadedTasks();
}
void ClearLoadedTasks()
{
Jobs.Clear();Regions.Clear();jobsById.Clear();regionsById.Clear();surfaceCells.Clear();OwnedJobs.Clear();PendingCount=0;generation.Clear();reserved=0;advancing=null;discoveries.Clear();advanceQueue.Clear();advanceQueued.Clear();
}
void RestoreJob(Job j)
{
Jobs.Add(j);jobsById.Add(j.Id,j);IndexSurface(j);
if(!Finished(j)){PendingCount++;if(j.Owner.Length>0)OwnedJobs[j.Owner]=j;}
}
void RestoreRegion(MiningRegion r)
{
if(r.Id.Length==0||r.Width<0||r.Height<0||r.Depth<0||r.Radius<0||r.Ring<0||r.Ring>1000)throw Data.Invalid(L.FleetStoredRegionInvalid);
AddRegion(r);RestoreGeneration(r);if(r.Status=="Pending")QueueAdvance(r.Id);
}
public void LoadRecord(MyIni ini,string kind)
{
if(restoreHeader==null)throw Data.Invalid(L.FleetStoredJobInvalid);
if(kind=="Job")
{
Job j=Job.Read(ini,"job");if(jobsById.ContainsKey(j.Id)||j.HoleIndex!=Jobs.Count||Jobs.Count>=Data.ReadLong(restoreHeader,"planner","Jobs"))throw Data.Invalid(L.FleetStoredJobInvalid);
j.Owner="";RestoreJob(j);if(Data.Flag(ini,"record","Discovered"))discoveries.Add(j.Id);
}
else
{
var r=MiningRegion.Read(ini,"region");if(regionsById.ContainsKey(r.Id)||Regions.Count>=Data.ReadLong(restoreHeader,"planner","Regions"))throw Data.Invalid(L.FleetStoredRegionInvalid);
RestoreRegion(r);
}
}
public void FinishRecordLoad()
{
if(restoreHeader==null||Jobs.Count!=Data.ReadLong(restoreHeader,"planner","Jobs")||Regions.Count!=Data.ReadLong(restoreHeader,"planner","Regions")||Jobs.Count+reserved>4096)throw Data.Invalid(L.FleetPlannerCountsInvalid);
LoadOwners(restoreHeader);
restoreHeader=null;
}
void LoadOwners(MyIni ini)
{
var ids=new HashSet<string>();int owners=(int)Data.ReadLong(ini,"planner","Owners");if(owners<0||owners>16)throw Data.Invalid(L.FleetCheckpointOwnerCountInvalid);
for(int n=0;n<owners;n++) {var owner=Data.Text(ini,"planner","OwnerId"+n);Job j=Find(Data.Text(ini,"planner","OwnerJob"+n));if(owner.Length==0||!ids.Add(owner)||j==null||Finished(j))throw Data.Invalid(L.FleetCheckpointOwnerInvalid);SetOwner(j,owner);}
}
void RestoreGeneration(MiningRegion r)
{
if(r.Generation.Length==0)return;
if((r.Generation!="Manual"&&r.Generation!="Ground"&&r.Generation!="Space"&&r.Generation!="Ring")||r.Cursor<0||r.Total<1||r.Total>4096||r.Cursor>=r.Total||r.Columns<0||r.Columns>4096||r.Rows<0||r.Rows>4096||r.Spacing<0||r.BaseClearance<0||r.ScanDistance<0)throw Data.Invalid(L.FleetStoredRegionInvalid);
if((r.Generation=="Manual"&&(r.Columns<1||r.Rows<1||(long)r.Columns*r.Rows!=r.Total))||(r.Generation=="Space"&&(r.Columns<12||r.Columns>128||r.Total!=7*r.Columns)))throw Data.Invalid(L.FleetStoredRegionInvalid);
Generate(r);
}
public void Load(MyIni ini)
{
var storedSerial=Data.ReadLong(ini,"planner","Serial");if(storedSerial<0||storedSerial>1000000000)throw Data.Invalid(L.FleetPlannerSerialInvalid);serial=(int)storedSerial;
int count=(int)Data.ReadLong(ini,"planner","Jobs"),regions=(int)Data.ReadLong(ini,"planner","Regions");
if(count<0||count>4096||regions<0||regions>4096) throw Data.Invalid(L.FleetPlannerCountsInvalid);
ClearLoadedTasks();
var ids=new HashSet<string>();
for(int n=0;n<count;n++) {Job j=Job.Read(ini,"job"+n);if(!ids.Add(j.Id)||j.HoleIndex!=n)throw Data.Invalid(L.FleetStoredJobInvalid);RestoreJob(j);}
ids.Clear();
for(int n=0;n<regions;n++) {var r=MiningRegion.Read(ini,"region"+n);if(!ids.Add(r.Id))throw Data.Invalid(L.FleetStoredRegionInvalid);RestoreRegion(r);}
if(ini.ContainsKey("planner","Owners"))
{
foreach(Job j in Jobs)j.Owner="";OwnedJobs.Clear();LoadOwners(ini);
}
foreach(var key in Data.Text(ini,"planner","Discoveries").Split(';')) if(key.Length>0) discoveries.Add(key);
}
}
}
}
using System;
using System.Collections.Generic;
using Sandbox.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame.Utilities;
using VRageMath;
namespace AutoMiningScript
{
public partial class Program
{
// All coordinates are frozen in world space. A moving base cannot drag a mine.
public class MiningRegion
{
public string Id = "", Parent = "", Ore = "", Status = "Pending";
public Vector3D Center, Direction = Data.Forward, Up = Data.Up;
public double Radius, Width, Height, Depth;
public long EntityId;
public bool Gravity, Search, Patch, RingFound;
public int Ring, EmptyRings;
// A persisted cursor replaces command-time loops over hundreds of holes.
public string Generation="";
public int Cursor, Total, Columns, Rows, Added;
public double Spacing, ScanDistance, BaseClearance;
public void WriteGeneration(MyIni i,string s)
{
i.Set(s,"Generation",Generation);i.Set(s,"Cursor",Cursor);i.Set(s,"Total",Total);i.Set(s,"Columns",Columns);i.Set(s,"Rows",Rows);i.Set(s,"Added",Added);
i.Set(s,"Spacing",Spacing);i.Set(s,"ScanDistance",ScanDistance);i.Set(s,"BaseClearance",BaseClearance);
}
public void Write(MyIni i, string s)
{
i.Set(s,"Id",Id); i.Set(s,"Parent",Parent); i.Set(s,"Ore",Ore); i.Set(s,"Status",Status);
Data.PutVector(i,s,"Center",Center); Data.PutVector(i,s,"Direction",Direction); Data.PutVector(i,s,"Up",Up);
i.Set(s,"Radius",Radius); i.Set(s,"Width",Width); i.Set(s,"Height",Height); i.Set(s,"Depth",Depth);
i.Set(s,"EntityId",EntityId);WriteGeneration(i,s);
i.Set(s,"Gravity",Gravity); i.Set(s,"Search",Search); i.Set(s,"Patch",Patch); i.Set(s,"RingFound",RingFound); i.Set(s,"Ring",Ring); i.Set(s,"EmptyRings",EmptyRings);
}
public static MiningRegion Read(MyIni i,string s)
=> new MiningRegion { Id=Data.Text(i,s,"Id"),Parent=Data.Text(i,s,"Parent"),Ore=Data.Text(i,s,"Ore"),Status=Data.Text(i,s,"Status"),
Center=Data.ReadVector(i,s,"Center"),Direction=Data.Unit(Data.ReadVector(i,s,"Direction",Data.Forward),Data.Forward),Up=Data.ReadVector(i,s,"Up",Data.Up),
Radius=Data.ReadNumber(i,s,"Radius",300),Width=Data.ReadNumber(i,s,"Width",60),Height=Data.ReadNumber(i,s,"Height",60),Depth=Data.ReadNumber(i,s,"Depth",40),EntityId=Data.ReadLong(i,s,"EntityId"),
Generation=Data.Text(i,s,"Generation"),Cursor=(int)Data.ReadLong(i,s,"Cursor"),Total=(int)Data.ReadLong(i,s,"Total"),Columns=(int)Data.ReadLong(i,s,"Columns"),Rows=(int)Data.ReadLong(i,s,"Rows"),Added=(int)Data.ReadLong(i,s,"Added"),Spacing=Data.ReadNumber(i,s,"Spacing"),ScanDistance=Data.ReadNumber(i,s,"ScanDistance"),BaseClearance=Data.ReadNumber(i,s,"BaseClearance"),
Gravity=Data.Flag(i,s,"Gravity"),Search=Data.Flag(i,s,"Search"),Patch=Data.Flag(i,s,"Patch"),RingFound=Data.Flag(i,s,"RingFound"),Ring=(int)Data.ReadLong(i,s,"Ring"),EmptyRings=(int)Data.ReadLong(i,s,"EmptyRings") };
}
public class TaskPlanner
{
readonly Program p;
public readonly List<Job> Jobs = new List<Job>();
public readonly List<MiningRegion> Regions = new List<MiningRegion>();
readonly HashSet<string> discoveries = new HashSet<string>();
readonly Queue<string> advanceQueue=new Queue<string>();
readonly HashSet<string> advanceQueued=new HashSet<string>();
MyIni checkpointTarget;
int checkpointJobs,checkpointRegions,checkpointJobCursor,checkpointRegionCursor;
readonly HashSet<Job> checkpointDirtyJobs=new HashSet<Job>();
readonly HashSet<MiningRegion> checkpointDirtyRegions=new HashSet<MiningRegion>();
readonly Dictionary<MiningRegion,int> checkpointRegionIndexes=new Dictionary<MiningRegion,int>();
readonly Queue<MiningRegion> generation=new Queue<MiningRegion>();
readonly Dictionary<string,Job> jobsById=new Dictionary<string,Job>();
readonly Dictionary<string,List<Job>> surfaceCells=new Dictionary<string,List<Job>>();
readonly Dictionary<string,MiningRegion> regionsById=new Dictionary<string,MiningRegion>();
int reserved,serial,advanceCursor,advanceJobCount,advanceRegionCount;
MiningRegion advancing;
bool advancePending,advanceBlocked;
public bool Generating => generation.Count>0;
public int PendingCount {get;private set;}
public readonly Dictionary<string,Job> OwnedJobs=new Dictionary<string,Job>();
public void SetOwner(Job j,string owner) {if(j.Owner.Length>0)OwnedJobs.Remove(j.Owner);j.Owner=owner;if(owner.Length>0) {OwnedJobs[owner]=j;j.LastOwner=owner;Dirty(j);}}
public int PendingGeneration => reserved;
public int TaskSerial => serial;
public void PreserveSerial(long value)
{
if(value<0 || value>1000000000)throw Data.Invalid(L.FleetPlannerSerialInvalid);
serial=Data.Max(serial,(int)value);
}
public void ClearTasks()
{
// Task identifiers remain monotonic across clear/reload, so late packets
// cannot alias a newly generated hole. Configuration is not touched.
ClearLoadedTasks();advanceCursor=advanceJobCount=advanceRegionCount=0;advancePending=advanceBlocked=false;
if(checkpointTarget!=null)checkpointTarget.Clear();checkpointTarget=null;
checkpointJobs=checkpointRegions=checkpointJobCursor=checkpointRegionCursor=0;
checkpointDirtyJobs.Clear();checkpointDirtyRegions.Clear();checkpointRegionIndexes.Clear();
recordSaving=false;recordJobCursor=recordRegionCursor=0;savedJobs.Clear();savedRegions.Clear();restoreHeader=null;
}
void AddRegion(MiningRegion r) {Regions.Add(r);regionsById.Add(r.Id,r);}
void Generate(MiningRegion r) {generation.Enqueue(r);reserved+=r.Total-r.Cursor;}
void Dirty(MiningRegion r) {if(checkpointTarget!=null||recordSaving)checkpointDirtyRegions.Add(r);}
void Dirty(Job j) {if(checkpointTarget!=null||recordSaving)checkpointDirtyJobs.Add(j);}
public TaskPlanner(Program program) { p=program; }
double N(string section,string key,double fallback,double min,double max) => p.Config.Number(section,key,fallback,min,max);
int Limit => p.Config.Integer("Survey","MaxJobs",2048,32,4096);
public double FootprintWidth => N("Mining","FootprintWidth",6,0.5,100);
public double FootprintHeight => N("Mining","FootprintHeight",6,0.5,100);
double StepX => FootprintWidth*(1-N("Mining","Overlap",0.15,0,0.8));
double StepY => FootprintHeight*(1-N("Mining","Overlap",0.15,0,0.8));
public string[] Ores => p.Config.Text("Mining","TargetOres","").Split(',');
string Id(string type) => p.Config.Id+"-"+type+"-"+(++serial).ToString(Data.Culture);
public Job Find(string id) {Job j;return jobsById.TryGetValue(id,out j)?j:null;}
public MiningRegion Region(string id) {MiningRegion r;return regionsById.TryGetValue(id,out r)?r:null;}
public static bool Finished(Job j) => j.Outcome=="Complete" || j.Outcome=="Blocked" || j.Outcome=="SurveyHit" || j.Outcome=="SurveyEmpty" || j.Outcome=="InvalidSample";
public int OrePriority(string ore)
{
string[] ores=p.Config.Text("Mining","PriorityOres","Iron,Cobalt,Nickel,Silicon").Split(','), found=ore.Split(',');
for(int n=0;n<ores.Length;n++) for(int f=0;f<found.Length;f++) if(found[f].Trim().Length>0 && IsTarget(found[f].Trim()) && string.Equals(ores[n].Trim(),found[f].Trim(),Data.IgnoreCase)) return ores.Length-n;
return 0;
}
bool IsTarget(string ore)
{
bool any=false;foreach(var raw in Ores) {var target=raw.Trim();if(target.Length==0)continue;any=true;if(string.Equals(target,ore,Data.IgnoreCase))return true;}return !any;
}
public string PreferredOre(string ore)
{
string best="";int priority=-1;
foreach(var raw in ore.Split(',')) {var found=raw.Trim();if(found.Length==0||!IsTarget(found))continue;var score=OrePriority(found);if(score>priority){best=found;priority=score;}}
return best;
}
public int JobPriority(Job j)
{
if(j.Kind==JobKind.Manual)return 100;if(j.Kind==JobKind.Survey)return 0;
var r=Region(j.RegionId);var ore=r!=null&&r.Patch?r.Ore:j.TargetOre;
return ore.Length>0&&PreferredOre(ore).Length==0?-1:10+OrePriority(ore);
}
static string MergeOres(string first,string second)
{
var result=new List<string>();var seen=new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach(var raw in (first+","+second).Split(',')) {var ore=raw.Trim();if(ore.Length>0&&seen.Add(ore))result.Add(ore);}return string.Join(",",result);
}
string SurfaceKey(long entity,double x,double y,double z) => entity.ToString(Data.Culture)+":"+x.ToString(Data.Culture)+":"+y.ToString(Data.Culture)+":"+z.ToString(Data.Culture);
void IndexSurface(Job j)
{
if(j.Kind==JobKind.Survey||j.EntityId==0)return;
var cell=Data.Max(StepX,StepY);var key=SurfaceKey(j.EntityId,Math.Floor(j.Entry.X/cell),Math.Floor(j.Entry.Y/cell),Math.Floor(j.Entry.Z/cell));List<Job> bucket;
if(!surfaceCells.TryGetValue(key,out bucket))surfaceCells[key]=bucket=new List<Job>();bucket.Add(j);
}
bool KnownSurface(JobReport report)
{
double cell=Data.Max(StepX,StepY),x=Math.Floor(report.Position.X/cell),y=Math.Floor(report.Position.Y/cell),z=Math.Floor(report.Position.Z/cell);
for(int dx=-1;dx<=1;dx++)for(int dy=-1;dy<=1;dy++)for(int dz=-1;dz<=1;dz++)
{
List<Job> bucket;if(!surfaceCells.TryGetValue(SurfaceKey(report.EntityId,x+dx,y+dy,z+dz),out bucket))continue;
foreach(Job j in bucket)if(Data.DistanceSquared(j.Entry,report.Position)<cell*cell)return true;
}
return false;
}
Job Add(MiningRegion r,JobKind kind,Vector3D entry,Vector3D direction,double depth,long entity=0)
{
if(Jobs.Count+reserved>=Limit) { r.Status="Sampling incomplete: job budget limit"; return null; }
var j=new Job {Id=Id("job"),RegionId=r.Id,Kind=kind,Entry=entry,Direction=Data.Unit(direction,Data.Forward),Up=Data.Perpendicular(direction,r.Up),Depth=depth,
Radius=N("Mining","HoleRadius",3,0.25,50),HoleIndex=Jobs.Count,EntityId=entity==0?r.EntityId:entity,Priority=kind==JobKind.Manual?100:kind==JobKind.Probe?10+OrePriority(r.Ore):0,TargetOre=r.Ore};
Jobs.Add(j);jobsById.Add(j.Id,j);IndexSurface(j);PendingCount++; return j;
}
public string Manual(Vector3D entry,Vector3D inward,Vector3D up,double width,double height,double depth)
{
if(!Data.Finite(width)||!Data.Finite(height)||!Data.Finite(depth)||width<=0||height<=0||depth<=0||width>1000||height>1000||depth>1000) throw Data.Invalid(L.FleetMarkDimensions);
if(inward.LengthSquared()<0.01) throw Data.Invalid(L.FleetGpsPointsDiffer);
int nx=Data.Max(1,1+(int)Math.Ceiling(Data.Max(0,width-FootprintWidth)/StepX));
int ny=Data.Max(1,1+(int)Math.Ceiling(Data.Max(0,height-FootprintHeight)/StepY));
if((long)nx*ny+Jobs.Count+reserved>Limit) throw Data.Invalid(L.FleetManualJobLimit);
var r=new MiningRegion {Id=Id("manual"),Center=entry,Direction=Data.Unit(inward,Data.Forward),Up=Data.Perpendicular(inward,up),Width=width,Height=height,Depth=depth};
r.Generation="Manual";r.Columns=nx;r.Rows=ny;r.Total=nx*ny;
AddRegion(r);Generate(r);GenerateStep();
return L.F(L.FleetManualQueued,r.Id,nx*ny);
}
public string Survey(Vector3D center,Vector3D gravity,Vector3D forward,Vector3D up,double requestedRadius)
{
var planet=gravity.LengthSquared()>0.01;
var radius=requestedRadius>0?requestedRadius:N("Survey","Radius",planet?300:2000,20,20000);
if(!Data.Finite(radius)||radius<20||radius>20000) throw Data.Invalid(L.FleetSurveyRadius);
var r=new MiningRegion {Id=Id("search"),Center=center,Direction=Data.Unit(planet?gravity:forward,Data.Forward),Up=Data.Perpendicular(planet?gravity:forward,up),Radius=radius,Gravity=planet,Search=true,Depth=N("Survey","ProbeDepth",planet?60:40,1,100),BaseClearance=N("Survey","BaseClearance",80,10,2000)};
if(planet)
{
r.Spacing=N("Survey","Spacing",60,5,2000);int cells=(int)Math.Floor(radius/r.Spacing);
var estimate=(long)(cells*2+1)*(cells*2+1);
if(estimate>Limit-Jobs.Count-reserved) throw Data.Invalid(L.FleetSurveyGridLimit);
r.Generation="Ground";r.Columns=cells;r.Total=(int)estimate;r.ScanDistance=N("Survey","ScanDistance",Data.Min(10000,Data.Max(300,radius)),50,10000);
}
else
{
r.Columns=p.Config.Integer("Survey","SpaceRays",42,12,128);r.Total=7*r.Columns;
if(Jobs.Count+reserved+r.Total>Limit) throw Data.Invalid(L.FleetSpaceSurveyLimit);
r.Generation="Space";
}
AddRegion(r);Generate(r);GenerateStep();
return L.F(L.FleetSurveyQueued,r.Id);
}
// Enumerate each ring perimeter once, rather than scanning every inner square.
static void RingPoint(int index,out int x,out int y)
{
if(index==0) {x=y=0;return;}
int ring=(int)Math.Ceiling((Math.Sqrt(index+1)-1)/2),side=ring*2;
int k=index-(side-1)*(side-1);
if(k<side) {x=-ring+k;y=-ring;}
else if(k<side*2) {x=ring;y=-ring+k-side;}
else if(k<side*3) {x=ring-(k-side*2);y=ring;}
else {x=-ring;y=ring-(k-side*3);}
}
public void GenerateStep()
{
int work=0;
while(generation.Count>0&&work++<8&&p.HasBudget(0.55))
{
var r=generation.Peek();int index=r.Cursor++;reserved--;Dirty(r);
Vector3D right=Data.Cross(r.Direction,r.Up),entry=r.Center,dir=r.Direction;var depth=r.Depth;var kind=JobKind.Probe;var add=true;
if(r.Generation=="Manual")
{
int y=index/r.Columns,column=index%r.Columns,x=y%2==0?column:r.Columns-column-1;
double spanX=Data.Max(0,r.Width-FootprintWidth),spanY=Data.Max(0,r.Height-FootprintHeight);
entry+=right*(r.Columns==1?0:-spanX/2+spanX*x/(r.Columns-1))+r.Up*(r.Rows==1?0:-spanY/2+spanY*y/(r.Rows-1));kind=JobKind.Manual;
}
else if(r.Generation=="Ground")
{
int x,y;RingPoint(index,out x,out y);add=x*x+y*y<=r.Columns*r.Columns;
entry+=-r.Direction*r.BaseClearance+right*x*r.Spacing+r.Up*y*r.Spacing;depth=r.ScanDistance;kind=JobKind.Survey;
}
else if(r.Generation=="Space")
{
int point=index/r.Columns,ray=index%r.Columns;var basis=Data.Frame(r.Center,r.Direction,r.Up);
var offset=point==1?basis.Right:point==2?basis.Left:point==4?basis.Down:point==5?basis.Forward:point==6?basis.Backward:basis.Up;
double y=1-2*(ray+0.5)/r.Columns,phi=ray*2.399963229728653,radial=Math.Sqrt(Data.Max(0,1-y*y));
dir=basis.Right*(Math.Cos(phi)*radial)+basis.Up*y+basis.Forward*(Math.Sin(phi)*radial);
entry+=offset*(point==0?Data.Min(r.BaseClearance,r.Radius*0.2):r.Radius*0.35);
var local=entry-r.Center;var dot=Data.Dot(local,dir);depth=Data.Min(10000,-dot+Math.Sqrt(dot*dot+r.Radius*r.Radius-local.LengthSquared()));kind=JobKind.Survey;
}
else if(r.Generation=="Ring")
{
int x,y;RingPoint((2*r.Ring-1)*(2*r.Ring-1)+index,out x,out y);
entry+=right*x*StepX+r.Up*y*StepY;var search=Region(r.Parent);
add=Math.Abs(x)<=r.Columns&&Math.Abs(y)<=r.Rows&&(search==null||Inside(search,entry));depth=BoundDepth(search,entry,dir,depth);add=add&&depth>=1;
}
else throw Data.Invalid(L.FleetStoredRegionInvalid);
if(add&&Add(r,kind,entry,dir,depth)!=null)r.Added++;
if(r.Cursor>=r.Total)
{
generation.Dequeue();var emptyRing=r.Generation=="Ring"&&r.Added==0;r.Generation="";
if(emptyRing) {r.Status="Sampling complete: boundary";QueueAdvance(r.Parent);}
else QueueAdvance(r.Id);
}
}
}
bool Inside(MiningRegion r,Vector3D point)
{
var d=point-r.Center;
if(r.Gravity) {d-=r.Direction*Data.Dot(d,r.Direction); return d.LengthSquared()<=r.Radius*r.Radius;}
return d.LengthSquared()<=r.Radius*r.Radius;
}
double BoundDepth(MiningRegion r,Vector3D entry,Vector3D direction,double depth)
{
if(r==null||r.Gravity) return depth;
var d=entry-r.Center;double along=Data.Dot(d,direction),disc=along*along+r.Radius*r.Radius-d.LengthSquared();
return Data.Min(depth,Data.Max(0,-along+Math.Sqrt(Data.Max(0,disc))));
}
public void Report(Job j,JobReport report)
{
Dirty(j);
j.ConfirmedOres=MergeOres(j.ConfirmedOres,report.Ore);
if(j.EntityId==0&&report.EntityId!=0){j.EntityId=report.EntityId;IndexSurface(j);}
var existing=Region(j.RegionId);
if(report.Ore.Length>0&&existing!=null&&existing.Patch) {existing.Ore=MergeOres(existing.Ore,j.ConfirmedOres);Dirty(existing);}
if(Finished(j)) return;
if (j.Kind == JobKind.Survey && report.Scan != null) j.Scan = report.Scan;
j.Progress=Data.Max(j.Progress,Data.Clamp(report.Progress,0,j.Depth));
// A terminal report carries accumulated ores, so a reordered/dropped Discovery
// message cannot erase a confirmed find. Processing is idempotent per hole.
if(report.Outcome=="Complete"&&report.Ore.Length>0)
Report(j,new JobReport {JobId=j.Id,Outcome="Discovery",Ore=report.Ore,Progress=report.Progress,Position=report.Position,Direction=report.Direction,Up=report.Up,EntityId=report.EntityId});
if(report.Outcome=="Discovery")
{
var ore=PreferredOre(report.Ore); if(ore.Length==0) return;
var first=discoveries.Add(j.Id);
if(existing!=null && existing.Patch) {existing.RingFound=true;Dirty(existing);return;}
if(!first) return;
if(j.Kind!=JobKind.Probe || existing==null) return;
var patch=new MiningRegion {Id=Id("deposit"),Parent=existing.Id,Center=j.Entry,Direction=j.Direction,Up=j.Up,Patch=true,RingFound=true,Ore=j.ConfirmedOres,
Width=N("Survey","ExpansionWidth",60,1,100),Height=N("Survey","ExpansionHeight",60,1,100),Depth=N("Survey","MaxDepth",100,1,100),Radius=existing.Radius,Gravity=existing.Gravity,EntityId=j.EntityId};
AddRegion(patch); j.RegionId=patch.Id; j.TargetOre=ore; return;
}
if(report.Outcome=="Paused") return;
if(report.Outcome=="SurveyHit" && j.Kind==JobKind.Survey)
{
var r=Region(j.RegionId);
var known=KnownSurface(report);
if(r!=null&&!known&&Inside(r,report.Position)&&report.EntityId!=0&&report.Direction.LengthSquared()>0.1)
{
var direction=Data.Unit(report.Direction,Data.Forward);var depth=BoundDepth(r,report.Position,direction,r.Depth);
if(depth>=1) Add(r,JobKind.Probe,report.Position,direction,depth,report.EntityId);
}
}
j.Outcome=report.Outcome;
if(Finished(j)) {PendingCount--;SetOwner(j,"");QueueAdvance(j.RegionId);}
}
void QueueAdvance(string id) {if(id.Length>0&&advanceQueued.Add(id)) advanceQueue.Enqueue(id);}
public void Advance()
{
GenerateStep();
if(!p.HasBudget(0.55))return;
if(advancing==null)
{
if(advanceQueue.Count==0)return;
var id=advanceQueue.Dequeue();advanceQueued.Remove(id);advancing=Region(id);advanceCursor=0;advanceJobCount=Jobs.Count;advanceRegionCount=Regions.Count;advancePending=advanceBlocked=false;
if(advancing==null||advancing.Status!="Pending"||advancing.Generation.Length>0) {advancing=null;return;}
}
// Appended jobs must not move the boundary under a cursor already scanning
// child regions. Restart the summary before considering a region complete.
if(advanceJobCount!=Jobs.Count||advanceRegionCount!=Regions.Count) {advanceCursor=0;advanceJobCount=Jobs.Count;advanceRegionCount=Regions.Count;advancePending=advanceBlocked=false;}
var r=advancing;int work=0;
while(advanceCursor<Jobs.Count+Regions.Count&&work++<64&&p.HasBudget(0.55))
{
int index=advanceCursor++;
if(index<Jobs.Count)
{
Job j=Jobs[index];if(j.RegionId==r.Id) {if(!Finished(j))advancePending=true;if(j.Outcome=="Blocked"||j.Outcome=="InvalidSample")advanceBlocked=true;}
}
else if(r.Search)
{
var child=Regions[index-Jobs.Count];if(child.Parent==r.Id) {if(child.Status=="Pending")advancePending=true;if(child.Status.IndexOf("incomplete",Data.IgnoreCase)>=0)advanceBlocked=true;}
}
}
if(advanceCursor<Jobs.Count+Regions.Count)return;
advancing=null;Dirty(r);
if(r.Search) {if(!advancePending)r.Status=advanceBlocked?"Sampling incomplete: blocked observations":"Sampling complete (not proof of no ore)";return;}
if(advancePending)return;
if(!r.Patch) {r.Status=advanceBlocked?"Range incomplete: blocked":"Specified range complete";return;}
if(advanceBlocked) {r.Status="Sampling incomplete: blocked or invalid sample";QueueAdvance(r.Parent);return;}
if(!r.RingFound)r.EmptyRings++;else r.EmptyRings=0;
if(r.EmptyRings>=2) {r.Status="Sampling complete: two empty rings";QueueAdvance(r.Parent);return;}
r.RingFound=false;r.Ring++;
r.Columns=(int)Math.Floor(r.Width/(2*StepX));r.Rows=(int)Math.Floor(r.Height/(2*StepY));
if(r.Ring>Data.Max(r.Columns,r.Rows)) {r.Status="Sampling complete: range limit";QueueAdvance(r.Parent);return;}
r.Total=8*r.Ring;r.Cursor=r.Added=0;
if(Jobs.Count+reserved+r.Total>Limit) {r.Status="Sampling incomplete: job budget limit";QueueAdvance(r.Parent);return;}
r.Generation="Ring";Generate(r);
}
public void Save(MyIni ini)
{
for(int n=0;n<Jobs.Count;n++) Jobs[n].Write(ini,"job"+n);
for(int n=0;n<Regions.Count;n++) Regions[n].Write(ini,"region"+n);
WriteCheckpointHeader(ini);
}
void WriteCheckpointHeader(MyIni ini,bool includeDiscoveries=true)
{
ini.Set("planner","Serial",serial);ini.Set("planner","Jobs",Jobs.Count);ini.Set("planner","Regions",Regions.Count);
if(includeDiscoveries)ini.Set("planner","Discoveries",string.Join(";",new List<string>(discoveries)));
// This small final ownership table overrides owners copied in earlier frames.
var ownership=includeDiscoveries?(IEnumerable<Job>)Jobs:OwnedJobs.Values;
int owners=0;foreach(Job j in ownership) if(j.Owner.Length>0&&!Finished(j)) {ini.Set("planner","OwnerJob"+owners,j.Id);ini.Set("planner","OwnerId"+owners,j.Owner);owners++;}
ini.Set("planner","Owners",owners);
}
public bool Checkpoint(MyIni ini)
{
if(checkpointTarget!=ini)
{
checkpointTarget=ini;checkpointJobs=Jobs.Count;checkpointRegions=Regions.Count;checkpointJobCursor=checkpointRegionCursor=0;checkpointDirtyJobs.Clear();checkpointDirtyRegions.Clear();checkpointRegionIndexes.Clear();
}
// Lists are append-only: extend the tail without restarting already copied work.
checkpointJobs=Jobs.Count;checkpointRegions=Regions.Count;
int work=0;
while(checkpointJobCursor<checkpointJobs&&work++<4&&p.HasBudget(0.55)) {Jobs[checkpointJobCursor].Write(ini,"job"+checkpointJobCursor);checkpointJobCursor++;}
if(checkpointJobCursor<checkpointJobs) return false;
work=0;
while(checkpointRegionCursor<checkpointRegions&&work++<4&&p.HasBudget(0.55)) {checkpointRegionIndexes[Regions[checkpointRegionCursor]]=checkpointRegionCursor;Regions[checkpointRegionCursor].Write(ini,"region"+checkpointRegionCursor);checkpointRegionCursor++;}
if(checkpointRegionCursor<checkpointRegions) return false;
if(checkpointDirtyJobs.Count>0)
{
var dirty=new List<Job>(checkpointDirtyJobs);int count=Data.Min(4,dirty.Count);
for(int n=0;n<count&&p.HasBudget(0.55);n++) {int index=dirty[n].HoleIndex;if(index>=0&&index<Jobs.Count&&Jobs[index]==dirty[n])dirty[n].Write(ini,"job"+index);else throw Data.Invalid(L.FleetCheckpointJobIndexMismatch);checkpointDirtyJobs.Remove(dirty[n]);}
return false;
}
if(checkpointDirtyRegions.Count>0)
{
var dirty=new List<MiningRegion>(checkpointDirtyRegions);int count=Data.Min(4,dirty.Count);
for(int n=0;n<count;n++) {int index;if(checkpointRegionIndexes.TryGetValue(dirty[n],out index))dirty[n].Write(ini,"region"+index);checkpointDirtyRegions.Remove(dirty[n]);}
return false;
}
WriteCheckpointHeader(ini);checkpointTarget=null;return true;
}
bool recordSaving;
readonly List<string> savedJobs=new List<string>(),savedRegions=new List<string>();
int recordJobCursor,recordRegionCursor;
MyIni restoreHeader;
public static string StorageRecord(MyIni ini,string kind)
{
ini.Set("record","Kind",kind);var value=ini.ToString();if(value.Length>32768)throw Data.Invalid(L.FleetStorageRecordInvalid);return value.Length.ToString(Data.Culture)+":"+value;
}
string JobRecord(Job j)
{
var ini=new MyIni();j.Write(ini,"job");ini.Set("record","Discovered",discoveries.Contains(j.Id));return StorageRecord(ini,"Job");
}
string RegionRecord(MiningRegion r) {var ini=new MyIni();r.Write(ini,"region");return StorageRecord(ini,"Region");}
public bool CheckpointRecords(out List<string> records)
{
records=null;
if(!recordSaving)
{
recordSaving=true;recordJobCursor=recordRegionCursor=0;savedJobs.Clear();savedRegions.Clear();checkpointDirtyJobs.Clear();checkpointDirtyRegions.Clear();checkpointRegionIndexes.Clear();
}
int work=0;
while(recordJobCursor<Jobs.Count&&work++<4&&p.HasBudget(0.55)) {savedJobs.Add(JobRecord(Jobs[recordJobCursor++]));}
if(recordJobCursor<Jobs.Count)return false;
work=0;
while(recordRegionCursor<Regions.Count&&work++<4&&p.HasBudget(0.55)) {var r=Regions[recordRegionCursor];checkpointRegionIndexes[r]=recordRegionCursor++;savedRegions.Add(RegionRecord(r));}
if(recordRegionCursor<Regions.Count)return false;
if(checkpointDirtyJobs.Count>0)
{
var dirty=new List<Job>(checkpointDirtyJobs);work=0;
foreach(Job j in dirty) {if(work++>=4||!p.HasBudget(0.55))break;savedJobs[j.HoleIndex]=JobRecord(j);checkpointDirtyJobs.Remove(j);}return false;
}
if(checkpointDirtyRegions.Count>0)
{
var dirty=new List<MiningRegion>(checkpointDirtyRegions);work=0;
foreach(var r in dirty) {if(work++>=4||!p.HasBudget(0.55))break;savedRegions[checkpointRegionIndexes[r]]=RegionRecord(r);checkpointDirtyRegions.Remove(r);}return false;
}
if(!p.HasBudget(0.35))return false;
var header=new MyIni();WriteCheckpointHeader(header,false);records=new List<string>();records.Add(StorageRecord(header,"Planner"));records.AddRange(savedJobs);records.AddRange(savedRegions);recordSaving=false;return true;
}
public void BeginRecordLoad(MyIni header)
{
var storedSerial=Data.ReadLong(header,"planner","Serial");int jobs=(int)Data.ReadLong(header,"planner","Jobs"),regions=(int)Data.ReadLong(header,"planner","Regions");
if(storedSerial<0||storedSerial>1000000000||jobs<0||jobs>4096||regions<0||regions>4096)throw Data.Invalid(L.FleetPlannerCountsInvalid);
serial=(int)storedSerial;restoreHeader=header;ClearLoadedTasks();
}
void ClearLoadedTasks()
{
Jobs.Clear();Regions.Clear();jobsById.Clear();regionsById.Clear();surfaceCells.Clear();OwnedJobs.Clear();PendingCount=0;generation.Clear();reserved=0;advancing=null;discoveries.Clear();advanceQueue.Clear();advanceQueued.Clear();
}
void RestoreJob(Job j)
{
Jobs.Add(j);jobsById.Add(j.Id,j);IndexSurface(j);
if(!Finished(j)){PendingCount++;if(j.Owner.Length>0)OwnedJobs[j.Owner]=j;}
}
void RestoreRegion(MiningRegion r)
{
if(r.Id.Length==0||r.Width<0||r.Height<0||r.Depth<0||r.Radius<0||r.Ring<0||r.Ring>1000)throw Data.Invalid(L.FleetStoredRegionInvalid);
AddRegion(r);RestoreGeneration(r);if(r.Status=="Pending")QueueAdvance(r.Id);
}
public void LoadRecord(MyIni ini,string kind)
{
if(restoreHeader==null)throw Data.Invalid(L.FleetStoredJobInvalid);
if(kind=="Job")
{
Job j=Job.Read(ini,"job");if(jobsById.ContainsKey(j.Id)||j.HoleIndex!=Jobs.Count||Jobs.Count>=Data.ReadLong(restoreHeader,"planner","Jobs"))throw Data.Invalid(L.FleetStoredJobInvalid);
j.Owner="";RestoreJob(j);if(Data.Flag(ini,"record","Discovered"))discoveries.Add(j.Id);
}
else
{
var r=MiningRegion.Read(ini,"region");if(regionsById.ContainsKey(r.Id)||Regions.Count>=Data.ReadLong(restoreHeader,"planner","Regions"))throw Data.Invalid(L.FleetStoredRegionInvalid);
RestoreRegion(r);
}
}
public void FinishRecordLoad()
{
if(restoreHeader==null||Jobs.Count!=Data.ReadLong(restoreHeader,"planner","Jobs")||Regions.Count!=Data.ReadLong(restoreHeader,"planner","Regions")||Jobs.Count+reserved>4096)throw Data.Invalid(L.FleetPlannerCountsInvalid);
LoadOwners(restoreHeader);
restoreHeader=null;
}
void LoadOwners(MyIni ini)
{
var ids=new HashSet<string>();int owners=(int)Data.ReadLong(ini,"planner","Owners");if(owners<0||owners>16)throw Data.Invalid(L.FleetCheckpointOwnerCountInvalid);
for(int n=0;n<owners;n++) {var owner=Data.Text(ini,"planner","OwnerId"+n);Job j=Find(Data.Text(ini,"planner","OwnerJob"+n));if(owner.Length==0||!ids.Add(owner)||j==null||Finished(j))throw Data.Invalid(L.FleetCheckpointOwnerInvalid);SetOwner(j,owner);}
}
void RestoreGeneration(MiningRegion r)
{
if(r.Generation.Length==0)return;
if((r.Generation!="Manual"&&r.Generation!="Ground"&&r.Generation!="Space"&&r.Generation!="Ring")||r.Cursor<0||r.Total<1||r.Total>4096||r.Cursor>=r.Total||r.Columns<0||r.Columns>4096||r.Rows<0||r.Rows>4096||r.Spacing<0||r.BaseClearance<0||r.ScanDistance<0)throw Data.Invalid(L.FleetStoredRegionInvalid);
if((r.Generation=="Manual"&&(r.Columns<1||r.Rows<1||(long)r.Columns*r.Rows!=r.Total))||(r.Generation=="Space"&&(r.Columns<12||r.Columns>128||r.Total!=7*r.Columns)))throw Data.Invalid(L.FleetStoredRegionInvalid);
Generate(r);
}
public void Load(MyIni ini)
{
var storedSerial=Data.ReadLong(ini,"planner","Serial");if(storedSerial<0||storedSerial>1000000000)throw Data.Invalid(L.FleetPlannerSerialInvalid);serial=(int)storedSerial;
int count=(int)Data.ReadLong(ini,"planner","Jobs"),regions=(int)Data.ReadLong(ini,"planner","Regions");
if(count<0||count>4096||regions<0||regions>4096) throw Data.Invalid(L.FleetPlannerCountsInvalid);
ClearLoadedTasks();
var ids=new HashSet<string>();
for(int n=0;n<count;n++) {Job j=Job.Read(ini,"job"+n);if(!ids.Add(j.Id)||j.HoleIndex!=n)throw Data.Invalid(L.FleetStoredJobInvalid);RestoreJob(j);}
ids.Clear();
for(int n=0;n<regions;n++) {var r=MiningRegion.Read(ini,"region"+n);if(!ids.Add(r.Id))throw Data.Invalid(L.FleetStoredRegionInvalid);RestoreRegion(r);}
if(ini.ContainsKey("planner","Owners"))
{
foreach(Job j in Jobs)j.Owner="";OwnedJobs.Clear();LoadOwners(ini);
}
foreach(var key in Data.Text(ini,"planner","Discoveries").Split(';')) if(key.Length>0) discoveries.Add(key);
}
}
}
}