XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

AutoMiningScript

【SpaceEngineer】全自动挖矿脚本

公开
关注 0 Fork 0 Star 0
UTF-8
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",Data.Zero),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",0),ScanDistance=Data.ReadNumber(i,s,"ScanDistance",0),BaseClearance=Data.ReadNumber(i,s,"BaseClearance",0),
                    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=Math.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.
                Jobs.Clear();Regions.Clear();jobsById.Clear();regionsById.Clear();surfaceCells.Clear();OwnedJobs.Clear();
                PendingCount=reserved=0;generation.Clear();discoveries.Clear();advanceQueue.Clear();advanceQueued.Clear();
                advancing=null;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","Iron,Nickel,Cobalt").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=Ores, found=ore.Split(',');
                for(int n=0;n<ores.Length;n++) for(int f=0;f<found.Length;f++) if(string.Equals(ores[n].Trim(),found[f].Trim(),Data.IgnoreCase)) return ores.Length-n;
                return 0;
            }
            public string PreferredOre(string ore)
            {
                string[] ores=Ores,found=ore.Split(',');
                for(int n=0;n<ores.Length;n++) for(int f=0;f<found.Length;f++) if(string.Equals(ores[n].Trim(),found[f].Trim(),Data.IgnoreCase)) return ores[n].Trim();
                return "";
            }
            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=Math.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=Math.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=Math.Max(1,1+(int)Math.Ceiling(Math.Max(0,width-FootprintWidth)/StepX));
                int ny=Math.Max(1,1+(int)Math.Ceiling(Math.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);
                if(string.Join("",Ores).Trim().Length==0) throw Data.Invalid(L.FleetTargetOresEmpty);
                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",Math.Min(10000,Math.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=Math.Max(0,r.Width-FootprintWidth),spanY=Math.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(Math.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?Math.Min(r.BaseClearance,r.Radius*0.2):r.Radius*0.35);
                        var local=entry-r.Center;var dot=Data.Dot(local,dir);depth=Math.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 Math.Min(depth,Math.Max(0,-along+Math.Sqrt(Math.Max(0,disc))));
            }
            public void Report(Job j,JobReport report)
            {
                if(Finished(j)) return;
                Dirty(j);
                if (j.Kind == JobKind.Survey && report.Scan != null) j.Scan = report.Scan;
                j.Progress=Math.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&&!discoveries.Contains(j.Id))
                    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);var existing=Region(j.RegionId);
                    if(existing!=null && existing.Patch) {existing.RingFound=true;Dirty(existing);if(OrePriority(ore)>OrePriority(existing.Ore)) existing.Ore=ore; 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=ore,
                        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>Math.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=Math.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=Math.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;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();
            }
            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="";Jobs.Add(j);jobsById.Add(j.Id,j);IndexSurface(j);if(!Finished(j))PendingCount++;if(Data.Flag(ini,"record","Discovered"))discoveries.Add(j.Id);
                }
                else
                {
                    var r=MiningRegion.Read(ini,"region");if(r.Id.Length==0||regionsById.ContainsKey(r.Id)||Regions.Count>=Data.ReadLong(restoreHeader,"planner","Regions")||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 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);
                var ids=new HashSet<string>();int owners=(int)Data.ReadLong(restoreHeader,"planner","Owners");if(owners<0||owners>16)throw Data.Invalid(L.FleetCheckpointOwnerCountInvalid);
                for(int n=0;n<owners;n++) {var owner=Data.Text(restoreHeader,"planner","OwnerId"+n);Job j=Find(Data.Text(restoreHeader,"planner","OwnerJob"+n));if(owner.Length==0||!ids.Add(owner)||j==null||Finished(j))throw Data.Invalid(L.FleetCheckpointOwnerInvalid);SetOwner(j,owner);}
                restoreHeader=null;
            }
            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);
                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();
                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);Jobs.Add(j);jobsById.Add(j.Id,j);IndexSurface(j);if(!Finished(j)){PendingCount++;if(j.Owner.Length>0)OwnedJobs[j.Owner]=j;}}
                ids.Clear();
                for(int n=0;n<regions;n++) {var r=MiningRegion.Read(ini,"region"+n);if(r.Id.Length==0||!ids.Add(r.Id)||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);}
                if(ini.ContainsKey("planner","Owners"))
                {
                    foreach(Job j in Jobs)j.Owner="";OwnedJobs.Clear();ids.Clear();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);}
                }
                foreach(var key in Data.Text(ini,"planner","Discoveries").Split(';')) if(key.Length>0) discoveries.Add(key);
            }
        }
    }
}