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

AutoMiningScript

【SpaceEngineer】全自动挖矿脚本

公开
关注 0 Fork 0 Star 0
返回提交历史

SpaceEngineersModDev/AutoMiningScript

Plan bulk excavation on surface discovery and distribute work across the fleet

23812f6
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

11 个文件 +246 -94
Added AutoMiningScript.Tests/BulkMiningTests.cs +117 -0
@@ -0,0 +1,117 @@
1 using System;
2 using System.Linq;
3 using VRage.Game.ModAPI.Ingame.Utilities;
4 using VRageMath;
5 using Xunit;
6 using P=AutoMiningScript.Program;
7
8 namespace AutoMiningScript.Tests
9 {
10 public class BulkMiningTests
11 {
12 static void Drain(P.TaskPlanner p) {for(int n=0;n<1024;n++)p.Advance();}
13 static P.Job Hit(P.TaskPlanner p,double radius=100)
14 {
15 p.Survey(Vector3D.Zero,Vector3D.Down,Vector3D.Forward,Vector3D.Up,radius);
16 p.Report(p.Jobs[0],new P.JobReport {Outcome="SurveyHit",Position=new Vector3D(0,-10,0),Direction=Vector3D.Down,EntityId=42});
17 return p.Jobs.Single(j=>j.Kind==P.JobKind.Probe);
18 }
19 [Theory]
20 [InlineData(12)] [InlineData(16)]
21 public void OneSurfaceHitCanEmployTheWholeReadyFleetWithoutOreConfirmation(int count)
22 {
23 var r=new FleetStateTests.Rig(count,true);foreach(var t in r.Miners)t.Radius=4.9;r.RegisterAll();
24 r.Planner.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,2000);
25 r.Planner.Report(r.Planner.Jobs[0],new P.JobReport {Outcome="SurveyHit",Position=Vector3D.Forward*500,Direction=Vector3D.Forward,EntityId=42});Drain(r.Planner);
26 foreach(var j in r.Planner.Jobs.Where(j=>j.Kind==P.JobKind.Survey).ToList())r.Planner.Report(j,new P.JobReport {Outcome="SurveyEmpty"});
27 r.Fleet.Command("start");
28 for(int n=0;n<200;n++){if(n%5==0)foreach(var t in r.Miners)r.Receive("TELEMETRY",t);r.Tick(1);}
29 var jobs=r.Planner.OwnedJobs.Values.ToList();Assert.Equal(count,jobs.Count);Assert.Equal(count,jobs.Select(j=>j.Owner).Distinct().Count());
30 Assert.All(jobs,j=>{Assert.Equal(P.JobKind.Probe,j.Kind);Assert.Equal(42,j.EntityId);Assert.Empty(j.ConfirmedOres);});
31 for(int i=0;i<jobs.Count;i++)for(int k=i+1;k<jobs.Count;k++)
32 Assert.True(P.Data.SegmentDistance(jobs[i].Entry-jobs[i].Direction*15,jobs[i].Entry+jobs[i].Direction*jobs[i].Depth,
33 jobs[k].Entry-jobs[k].Direction*15,jobs[k].Entry+jobs[k].Direction*jobs[k].Depth)>=11.8-1e-6);
34 }
35 [Fact]
36 public void BulkGenerationResumesExactlyAfterCheckpoint()
37 {
38 var p=new P.TaskPlanner(TestRig.Program());Hit(p);p.Advance();p.Advance();
39 var saved=new MyIni();p.Save(saved);var after=new P.TaskPlanner(TestRig.Program());after.Load(saved);Drain(p);Drain(after);
40 Assert.Equal(p.Jobs.Select(j=>j.Id),after.Jobs.Select(j=>j.Id));Assert.Equal(p.Jobs.Select(j=>j.Entry),after.Jobs.Select(j=>j.Entry));
41 Assert.Equal(p.Jobs.Count,p.Jobs.Select(j=>j.Id).Distinct().Count());Assert.Equal(0,after.PendingGeneration);
42 Assert.True(after.Regions.Single(r=>r.Patch).Bulk);
43 }
44 [Theory]
45 [InlineData(false)] [InlineData(true)]
46 public void FleetRecordStorageResumesNewGridOrUpgradesLegacyRingWithoutLosingActiveWork(bool legacy)
47 {
48 var before=new FleetStateTests.Rig(1,true);before.RegisterAll();var seed=Hit(before.Planner);
49 seed.Progress=12;before.Planner.SetOwner(seed,"miner-0");
50 if(legacy)
51 {
52 var patch=before.Planner.Regions.Single(r=>r.Patch);patch.Bulk=false;patch.Generation="Ring";
53 patch.Cursor=3;patch.Total=8;patch.Columns=patch.Rows=1;
54 }
55 else {before.Planner.Advance();before.Planner.Advance();}
56 var after=new FleetStateTests.Rig(1,true);after.Restore(before.Checkpoint());Drain(after.Planner);
57 var restored=after.Planner.Find(seed.Id);Assert.Equal(12,restored.Progress);Assert.Equal(seed.Entry,restored.Entry);
58 Assert.Equal("miner-0",restored.Owner);Assert.Equal(restored,after.Planner.OwnedJobs["miner-0"]);
59 Assert.True(after.Planner.Regions.Single(r=>r.Patch).Bulk);Assert.Equal(0,after.Planner.PendingGeneration);
60 var holes=after.Planner.Jobs.Where(j=>j.Kind==P.JobKind.Probe).ToList();Assert.Equal(145,holes.Count);
61 Assert.Equal(holes.Count,holes.Select(j=>j.Entry).Distinct().Count());
62 if(!legacy)
63 {
64 Drain(before.Planner);Assert.Equal(before.Planner.Jobs.Select(j=>j.Id),after.Planner.Jobs.Select(j=>j.Id));
65 Assert.Equal(before.Planner.Jobs.Select(j=>j.Entry),after.Planner.Jobs.Select(j=>j.Entry));
66 }
67 }
68 [Fact]
69 public void ExcavationNearSearchEdgeClipsBothEntriesAndBoreEndsToTheFrozenSphere()
70 {
71 var p=new P.TaskPlanner(TestRig.Program());p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,200);
72 p.Report(p.Jobs[0],new P.JobReport {Outcome="SurveyHit",Position=Vector3D.Forward*198,Direction=Vector3D.Forward,EntityId=42});Drain(p);
73 var holes=p.Jobs.Where(j=>j.Kind==P.JobKind.Probe).ToList();Assert.InRange(holes.Count,2,144);
74 Assert.All(holes,j=>
75 {
76 Assert.True(j.Entry.Length()<=200+1e-6);Assert.True((j.Entry+j.Direction*j.Depth).Length()<=200+1e-6);
77 Assert.InRange(j.Depth,1,2+1e-6);
78 });
79 }
80 [Fact]
81 public void LegacyIsolatedProbeExpandsWithoutChangingItsProgressOrOwnership()
82 {
83 var p=new P.TaskPlanner(TestRig.Program());p.Survey(Vector3D.Zero,Vector3D.Down,Vector3D.Forward,Vector3D.Up,100);Drain(p);
84 string search=p.Regions[0].Id;p.Manual(new Vector3D(0,-10,0),Vector3D.Down,Vector3D.Forward,6,6,40);
85 var seed=p.Jobs.Last();seed.Kind=P.JobKind.Probe;seed.EntityId=42;seed.RegionId=search;seed.Progress=12;p.SetOwner(seed,"old-miner");
86 var saved=new MyIni();p.Save(saved);var after=new P.TaskPlanner(TestRig.Program());after.Load(saved);Drain(after);
87 var restored=after.Find(seed.Id);Assert.Equal(12,restored.Progress);Assert.Equal(40,restored.Depth);Assert.Equal("old-miner",restored.Owner);
88 Assert.Equal(seed.Entry,restored.Entry);Assert.True(after.Regions.Single(r=>r.Patch).Bulk);
89 Assert.True(after.Jobs.Count(j=>j.Kind==P.JobKind.Probe)>100);
90 }
91 [Fact]
92 public void LegacyStoppedEmptyRingsUpgradeWithoutRepeatingExistingHoles()
93 {
94 var p=new P.TaskPlanner(TestRig.Program());var seed=Hit(p);p.Report(seed,new P.JobReport {Outcome="Complete",Progress=seed.Depth});
95 var saved=new MyIni();p.Save(saved);
96 saved.Set("region1","Bulk",false);saved.Set("region1","Generation","");saved.Set("region1","Status","Sampling complete: two empty rings");saved.Set("region1","Ring",2);saved.Set("region1","EmptyRings",2);
97 var after=new P.TaskPlanner(TestRig.Program());after.Load(saved);Drain(after);
98 Assert.Equal("Complete",after.Find(seed.Id).Outcome);Assert.Equal("Pending",after.Regions.Single(r=>r.Patch).Status);
99 var holes=after.Jobs.Where(j=>j.Kind==P.JobKind.Probe).ToList();Assert.True(holes.Count>100);Assert.Equal(holes.Count,holes.Select(j=>j.Entry).Distinct().Count());
100 }
101 [Fact]
102 public void JobBudgetProducesFinitePartialWorkAndHonestIncompleteStatus()
103 {
104 var p=new P.TaskPlanner(TestRig.Program("[Survey]\nMaxJobs=32\n"));Hit(p);Drain(p);
105 Assert.InRange(p.Jobs.Count,15,32);Assert.Equal(0,p.PendingGeneration);Assert.Equal("Pending",p.Regions.Single(r=>r.Patch).Status);
106 Assert.True(p.Jobs.Count(j=>j.Kind==P.JobKind.Probe)>1);
107 foreach(var j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe).ToList())p.Report(j,new P.JobReport {Outcome="Complete",Progress=j.Depth});Drain(p);
108 Assert.Contains("job budget",p.Regions.Single(r=>r.Patch).Status);
109 }
110 [Fact]
111 public void NewExcavationDepthUsesMaxDepthInsteadOfLegacyProbeDepth()
112 {
113 var p=new P.TaskPlanner(TestRig.Program("[Survey]\nMaxDepth=80\nProbeDepth=3\n"));Hit(p);Drain(p);
114 Assert.All(p.Jobs.Where(j=>j.Kind==P.JobKind.Probe),j=>Assert.Equal(80,j.Depth));
115 }
116 }
117 }
Modified AutoMiningScript.Tests/PlannerTests.cs +26 -26
@@ -26,7 +26,7 @@ namespace AutoMiningScript.Tests
26 26 static void Drain(P.TaskPlanner planner) {for(int n=0;n<1024;n++)planner.Advance();}
27 27 static P.Job SeedProbe(P.TaskPlanner planner)
28 28 {
29 planner.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);
29 planner.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,100);
30 30 P.Job survey=planner.Jobs[0];
31 31 planner.Report(survey,new P.JobReport {JobId=survey.Id,Outcome="SurveyHit",Position=new Vector3D(0,-10,0),Direction=Vector3D.Down,Up=Vector3D.Forward,EntityId=42});
32 32 return planner.Jobs.Single(j=>j.Kind==P.JobKind.Probe);
@@ -91,12 +91,12 @@ namespace AutoMiningScript.Tests
91 91 {
92 92 var p=Planner();P.Job probe=SeedProbe(p);
93 93 p.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);
94 P.Job survey=p.Jobs.Last();
94 P.Job survey=p.Jobs.First(j=>j.Kind==P.JobKind.Survey&&!P.TaskPlanner.Finished(j));int before=p.Jobs.Count(j=>j.Kind==P.JobKind.Probe);
95 95 p.Report(survey,new P.JobReport {Outcome="SurveyHit",Position=probe.Entry+new Vector3D(0.1,0,0),Direction=Vector3D.Down,Up=Vector3D.Forward,EntityId=42});
96 Assert.Single(p.Jobs,j=>j.Kind==P.JobKind.Probe);
97 p.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);survey=p.Jobs.Last();
96 Assert.Equal(before,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Single(p.Regions,r=>r.Patch);
97 p.Survey(Vector3D.Zero,Vector3D.Down*9.81,Vector3D.Forward,Vector3D.Up,20);survey=p.Jobs.First(j=>j.Kind==P.JobKind.Survey&&!P.TaskPlanner.Finished(j));before=p.Jobs.Count(j=>j.Kind==P.JobKind.Probe);
98 98 p.Report(survey,new P.JobReport {Outcome="SurveyHit",Position=probe.Entry,Direction=Vector3D.Down,Up=Vector3D.Forward,EntityId=43});
99 Assert.Equal(2,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
99 Assert.Equal(before+1,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Equal(2,p.Regions.Count(r=>r.Patch));
100 100 }
101 101 [Fact]
102 102 public void SurfaceOutsideFrozenSearchDoesNotGenerateProbe()
@@ -106,15 +106,16 @@ namespace AutoMiningScript.Tests
106 106 Assert.DoesNotContain(p.Jobs,j=>j.Kind==P.JobKind.Probe);
107 107 }
108 108 [Fact]
109 public void DiscoveryWaitsForRetreatCompletionBeforeSchedulingNeighborRing()
109 public void WholeExcavationIsAvailableBeforeDiscoveryOrFirstRetreat()
110 110 {
111 var p=Planner();P.Job probe=SeedProbe(p);probe.Owner="miner-1";
111 var p=Planner();P.Job probe=SeedProbe(p);p.SetOwner(probe,"miner-1");Drain(p);
112 Assert.Equal(145,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Empty(p.Regions.Single(r=>r.Patch).Ore);
112 113 p.Report(probe,new P.JobReport {Outcome="Discovery",Ore="Cobalt,Iron",Progress=5});Drain(p);
113 Assert.Single(p.Jobs,j=>j.Kind==P.JobKind.Probe);Assert.Equal("miner-1",probe.Owner);
114 Assert.Equal(145,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Equal("miner-1",probe.Owner);
114 115 Assert.Equal("Cobalt,Iron",p.Regions.Single(r=>r.Patch).Ore);
115 116 Assert.Equal("Iron",p.PreferredOre(p.Regions.Single(r=>r.Patch).Ore));
116 117 p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
117 Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Equal("",probe.Owner);
118 Assert.Equal(145,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Equal("",probe.Owner);
118 119 Assert.All(p.Jobs.Where(j=>j.Kind==P.JobKind.Probe),j=>Assert.Equal(42,j.EntityId));
119 120 }
120 121 [Fact]
@@ -122,31 +123,29 @@ namespace AutoMiningScript.Tests
122 123 {
123 124 var p=Planner();P.Job probe=SeedProbe(p);
124 125 p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
125 Assert.Single(p.Regions,r=>r.Patch);Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
126 Assert.Single(p.Regions,r=>r.Patch);Assert.Equal(145,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
126 127 p.Report(probe,new P.JobReport {Outcome="Discovery",Ore="Iron",Progress=5});
127 128 p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
128 Assert.Single(p.Regions,r=>r.Patch);Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
129 Assert.Single(p.Regions,r=>r.Patch);Assert.Equal(145,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));
129 130 }
130 131 [Fact]
131 public void TwoCompletedEmptyRingsStopExpansion()
132 public void EmptyHolesDoNotStopTheRemainingExcavation()
132 133 {
133 134 var p=Planner("[Survey]\nExpansionWidth=60\nExpansionHeight=60\n");P.Job probe=SeedProbe(p);
134 // Widen the frozen search so both complete expansion rings fit.
135 p.Regions[0].Radius=100;
136 p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
137 Assert.Equal(8,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)));
138 foreach(P.Job j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)).ToList()) p.Report(j,new P.JobReport {Outcome="Complete",Progress=j.Depth});Drain(p);
139 Assert.Equal(16,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)));
135 p.Report(probe,new P.JobReport {Outcome="Complete",Progress=probe.Depth});Drain(p);
136 Assert.Equal(144,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)));
137 foreach(P.Job j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)).Take(24).ToList()) p.Report(j,new P.JobReport {Outcome="Complete",Progress=j.Depth});Drain(p);
138 Assert.Equal(120,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)));Assert.Equal("Pending",p.Regions.Single(r=>r.Patch).Status);
140 139 foreach(P.Job j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)).ToList()) p.Report(j,new P.JobReport {Outcome="Complete",Progress=j.Depth});Drain(p);
141 Assert.Equal(25,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.DoesNotContain(p.Jobs,j=>!P.TaskPlanner.Finished(j));
142 Assert.Contains("two empty rings",p.Regions.Single(r=>r.Patch).Status);
140 Assert.Equal(145,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.DoesNotContain(p.Jobs,j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j));
141 Assert.Equal("Specified range complete",p.Regions.Single(r=>r.Patch).Status);
143 142 }
144 143 [Fact]
145 public void InvalidSamplesNeverCountAsConfirmedEmptyRings()
144 public void InvalidSamplesNeverCountAsCompletedExcavation()
146 145 {
147 146 var p=Planner();P.Job probe=SeedProbe(p);p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Iron",Progress=probe.Depth});Drain(p);
148 147 foreach(P.Job j in p.Jobs.Where(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j)).ToList()) p.Report(j,new P.JobReport {Outcome="InvalidSample",Progress=j.Depth});Drain(p);
149 var patch=p.Regions.Single(r=>r.Patch);Assert.Equal(0,patch.EmptyRings);Assert.Contains("incomplete",patch.Status);
148 var patch=p.Regions.Single(r=>r.Patch);Assert.True(patch.Bulk);Assert.Contains("incomplete",patch.Status);
150 149 }
151 150 [Fact]
152 151 public void PausedReportPreservesOwnershipAndMonotonicProgress()
@@ -229,10 +228,11 @@ namespace AutoMiningScript.Tests
229 228 Drain(p);Assert.Equal("Pending",p.Regions[0].Status);
230 229 }
231 230 [Fact]
232 public void UnknownOreDoesNotCreateTargetDeposit()
231 public void NonTargetOreDoesNotExcludeTheRestOfTheAsteroid()
233 232 {
234 233 var p=Planner("[Mining]\nTargetOres=Iron,Nickel,Cobalt\n");P.Job probe=SeedProbe(p);p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Stone,Ice",Progress=probe.Depth});Drain(p);
235 Assert.DoesNotContain(p.Regions,r=>r.Patch);Assert.Equal(0,p.OrePriority("Stone"));Assert.True(p.OrePriority("Iron")>p.OrePriority("Cobalt"));
234 Assert.Equal("Stone,Ice",Assert.Single(p.Regions,r=>r.Patch).Ore);Assert.Equal(0,p.OrePriority("Stone"));Assert.True(p.OrePriority("Iron")>p.OrePriority("Cobalt"));
235 Assert.All(p.Jobs.Where(j=>j.Kind==P.JobKind.Probe),j=>Assert.Equal(10,p.JobPriority(j)));
236 236 Assert.Equal("Stone,Ice",probe.ConfirmedOres);
237 237 }
238 238 [Theory]
@@ -244,7 +244,7 @@ namespace AutoMiningScript.Tests
244 244 var p=Planner(config);var probe=SeedProbe(p);
245 245 p.Report(probe,new P.JobReport {Outcome="Complete",Ore="Stone,Ice,ModOre",Progress=probe.Depth});Drain(p);
246 246 var patch=Assert.Single(p.Regions,r=>r.Patch);Assert.Equal("Stone,Ice,ModOre",patch.Ore);
247 Assert.Equal(9,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Equal(0,p.OrePriority("Stone"));
247 Assert.Equal(145,p.Jobs.Count(j=>j.Kind==P.JobKind.Probe));Assert.Equal(0,p.OrePriority("Stone"));
248 248 }
249 249 [Fact]
250 250 public void TargetOrderDoesNotSetPriorityAndPriorityCannotBypassTargets()
@@ -274,7 +274,7 @@ namespace AutoMiningScript.Tests
274 274 var pending=restored.Jobs.First(j=>j.Kind==P.JobKind.Probe&&!P.TaskPlanner.Finished(j));
275 275 Assert.Equal(11,restored.JobPriority(pending));
276 276 var excluded=Planner("[Mining]\nTargetOres=Gold\n");excluded.Load(saved);
277 Assert.Equal(-1,excluded.JobPriority(excluded.Find(pending.Id)));
277 Assert.Equal(10,excluded.JobPriority(excluded.Find(pending.Id)));
278 278 }
279 279 [Fact]
280 280 public void LateTerminalOreReportUpdatesPatchWithoutReopeningFinishedHole()
Modified AutoMiningScript.Tests/SurveyNavigationTests.cs +1 -1
@@ -136,7 +136,7 @@ namespace AutoMiningScript.Tests
136 136 VRage.Game.MyRelationsBetweenPlayerAndBlock.NoOwnership,new BoundingBoxD(surface-new Vector3D(10),surface+new Vector3D(10)),1);
137 137 P.JobReport report=r.Finish();Assert.Equal("SurveyHit",report.Outcome);planner.Report(r.Job,report);
138 138 P.Job probe=Assert.Single(planner.Jobs,j=>j.Kind==P.JobKind.Probe);
139 Assert.Equal(surface,probe.Entry);Assert.Equal(42,probe.EntityId);Assert.Equal(40,probe.Depth);
139 Assert.Equal(surface,probe.Entry);Assert.Equal(42,probe.EntityId);Assert.Equal(100,probe.Depth);
140 140 Assert.True(probe.Priority>r.Job.Priority);Assert.NotEqual(Vector3D.Zero,probe.Direction);
141 141 }
142 142
Modified AutoMiningScript.Tests/UnreachableSurveyTests.cs +3 -3
@@ -65,9 +65,9 @@ namespace AutoMiningScript.Tests
65 65 // A real surface report remains useful even if another miner could not approach.
66 66 p.Report(active,new P.JobReport {Outcome="SurveyHit",EntryUnreachable=true,Position=active.Entry+Vector3D.Forward*10,Direction=Vector3D.Forward,Up=Vector3D.Up,EntityId=42});
67 67 Drain(p);Assert.Equal("SurveyHit",active.Outcome);Assert.False(active.EntryUnreachable);
68 var probe=Assert.Single(p.Jobs,j=>j.Kind==P.JobKind.Probe);
69 Assert.True(p.JobPriority(probe)>0);Assert.False(P.TaskPlanner.Finished(probe));
70 Assert.Equal(253,p.PendingCount);Assert.Empty(p.OwnedJobs);
68 var holes=p.Jobs.Where(j=>j.Kind==P.JobKind.Probe).ToList();Assert.Equal(145,holes.Count);
69 Assert.All(holes,j=>{Assert.True(p.JobPriority(j)>0);Assert.False(P.TaskPlanner.Finished(j));});
70 Assert.Equal(252+holes.Count,p.PendingCount);Assert.Empty(p.OwnedJobs);
71 71 }
72 72
73 73 [Fact]
Modified AutoMiningScript/FleetController.cs +4 -3
@@ -106,7 +106,7 @@ namespace AutoMiningScript
106 106 Job assignmentBest;
107 107 string assignmentRegion = "";
108 108 bool assignmentPending;
109 double assignmentScore = double.MinValue, assignmentClock;
109 double assignmentScore = double.MinValue, assignmentClock, assignmentRadius;
110 110 bool running;
111 111 double poseClock, scheduleClock, unloadClock, scanClock;
112 112 DockFrame baseFrame;
@@ -741,16 +741,17 @@ namespace AutoMiningScript
741 741 if (planner.OwnedJobs.Count >= 16 || clearanceHolds.Count + planner.OwnedJobs.Count >= 64) { problem = L.FleetUnknownOccupancyLimit; return; }
742 742 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;
743 743 assignmentMiner = t.Id; assignmentJobCursor = 0; assignmentBest = null; assignmentScore = double.MinValue;
744 assignmentRadius=t.Radius;foreach(var miner in miners.Values)if(P.Now-miner.ReceivedAt<=5)assignmentRadius=Data.Max(assignmentRadius,miner.Radius);
744 745 assignmentRegion = CanContinue(t)?SearchRoot(planner.Find(t.NextAfter)):""; assignmentPending=false;
745 746 }
746 747 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; }
747 748 int examined = 0;
748 while (assignmentJobCursor < planner.Jobs.Count && examined++ < 32 && P.HasBudget(0.55))
749 while (assignmentJobCursor < planner.Jobs.Count && examined++ < 64 && P.HasBudget(0.55))
749 750 {
750 751 Job j = planner.Jobs[assignmentJobCursor++]; if (TaskPlanner.Finished(j) || (assignmentRegion.Length>0 && (j.Kind==JobKind.Manual || SearchRoot(j)!=assignmentRegion))) continue;
751 752 var priority=planner.JobPriority(j);if(priority<0)continue;
752 753 assignmentPending=true;if(j.Owner.Length>0)continue;
753 var score = priority * 100000 - Data.Distance(t.Position, j.Entry) - j.HoleIndex * 0.001;
754 var score = priority * 100000 - planner.WorkPhase(j,assignmentRadius)*10000 - Data.Distance(t.Position, j.Entry) - j.HoleIndex * 0.001;
754 755 if (score <= assignmentScore || Conflict(j, Data.Max(j.Radius, t.Radius),t.Id)) continue;
755 756 assignmentScore = score; assignmentBest = j;
756 757 }
Modified AutoMiningScript/Planning.cs +67 -48
@@ -15,8 +15,7 @@ namespace AutoMiningScript
15 15 public Vector3D Center, Direction = Data.Forward, Up = Data.Up;
16 16 public double Radius, Width, Height, Depth;
17 17 public long EntityId;
18 public bool Gravity, Search, Patch, RingFound;
19 public int Ring, EmptyRings;
18 public bool Gravity, Search, Patch, Bulk;
20 19 // A persisted cursor replaces command-time loops over hundreds of holes.
21 20 public string Generation="";
22 21 public int Cursor, Total, Columns, Rows, Added;
@@ -32,14 +31,14 @@ namespace AutoMiningScript
32 31 Data.PutVector(i,s,"Center",Center); Data.PutVector(i,s,"Direction",Direction); Data.PutVector(i,s,"Up",Up);
33 32 i.Set(s,"Radius",Radius); i.Set(s,"Width",Width); i.Set(s,"Height",Height); i.Set(s,"Depth",Depth);
34 33 i.Set(s,"EntityId",EntityId);WriteGeneration(i,s);
35 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);
34 i.Set(s,"Gravity",Gravity); i.Set(s,"Search",Search); i.Set(s,"Patch",Patch); i.Set(s,"Bulk",Bulk);
36 35 }
37 36 public static MiningRegion Read(MyIni i,string s)
38 37 => 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"),
39 38 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),
40 39 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"),
41 40 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"),
42 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") };
41 Gravity=Data.Flag(i,s,"Gravity"),Search=Data.Flag(i,s,"Search"),Patch=Data.Flag(i,s,"Patch"),Bulk=Data.Flag(i,s,"Bulk") };
43 42 }
44 43
45 44 public class TaskPlanner
@@ -60,7 +59,7 @@ namespace AutoMiningScript
60 59 readonly Dictionary<string,List<Job>> surfaceCells=new Dictionary<string,List<Job>>();
61 60 readonly Dictionary<string,MiningRegion> regionsById=new Dictionary<string,MiningRegion>();
62 61 readonly HashSet<string> unreachableEntries=new HashSet<string>();
63 int reserved,serial,advanceCursor,advanceJobCount,advanceRegionCount;
62 int reserved,serial,advanceCursor,advanceJobCount,advanceRegionCount,legacyCursor;
64 63 MiningRegion advancing;
65 64 bool advancePending,advanceBlocked;
66 65 public bool Generating => generation.Count>0;
@@ -121,7 +120,26 @@ namespace AutoMiningScript
121 120 if(SkipUnreachable(j))return -1;
122 121 if(j.Kind==JobKind.Manual)return 100;if(j.Kind==JobKind.Survey)return 0;
123 122 var r=Region(j.RegionId);var ore=r!=null&&r.Patch?r.Ore:j.TargetOre;
124 return ore.Length>0&&PreferredOre(ore).Length==0?-1:10+OrePriority(ore);
123 // A sampled hole cannot establish the absence of a rare ore elsewhere.
124 return 10+OrePriority(ore);
125 }
126 public double WorkPhase(Job j,double radius)
127 {
128 var r=Region(j.RegionId);if(r==null||!r.Bulk)return 0;
129 // Work on one spaced lattice first, then fill its gaps. Greedy nearest-hole
130 // selection can otherwise strand ready ships between overlapping reservations.
131 var offset=j.Entry-r.Center;double phase=0,scale=1,gap=2*Data.Max(radius,j.Radius)+2;
132 for(int axis=0;axis<2;axis++)
133 {
134 int count=axis==0?r.Columns:r.Rows;if(count<2)continue;
135 var span=axis==0?r.Width-FootprintWidth:r.Height-FootprintHeight;
136 if(span<=0)return 1;
137 var step=span/(count-1);var position=(Data.Dot(offset,axis==0?Data.Cross(r.Direction,r.Up):r.Up)+span/2)/step;
138 int index=(int)Math.Round(position),stride=Data.Min(count,Data.Max(1,(int)Math.Ceiling(gap/step)));
139 if(index<0||index>=count||Math.Abs(position-index)>.01)return 1;
140 phase+=(index%stride)*scale;scale*=stride;
141 }
142 return phase/scale;
125 143 }
126 144 void IndexUnreachable(Job j)
127 145 {
@@ -149,13 +167,13 @@ namespace AutoMiningScript
149 167 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;
150 168 if(!surfaceCells.TryGetValue(key,out bucket))surfaceCells[key]=bucket=new List<Job>();bucket.Add(j);
151 169 }
152 bool KnownSurface(JobReport report)
170 bool KnownSurface(JobReport report,bool exact=false)
153 171 {
154 172 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);
155 173 for(int dx=-1;dx<=1;dx++)for(int dy=-1;dy<=1;dy++)for(int dz=-1;dz<=1;dz++)
156 174 {
157 175 List<Job> bucket;if(!surfaceCells.TryGetValue(SurfaceKey(report.EntityId,x+dx,y+dy,z+dz),out bucket))continue;
158 foreach(Job j in bucket)if(Data.DistanceSquared(j.Entry,report.Position)<cell*cell)return true;
176 foreach(Job j in bucket)if(Data.DistanceSquared(j.Entry,report.Position)<(exact?.01:cell*cell))return true;
159 177 }
160 178 return false;
161 179 }
@@ -183,7 +201,7 @@ namespace AutoMiningScript
183 201 var planet=gravity.LengthSquared()>0.01;
184 202 var radius=requestedRadius>0?requestedRadius:N("Survey","Radius",planet?300:2000,20,20000);
185 203 if(!Data.Finite(radius)||radius<20||radius>20000) throw Data.Invalid(L.FleetSurveyRadius);
186 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)};
204 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","MaxDepth",100,1,100),BaseClearance=N("Survey","BaseClearance",80,10,2000)};
187 205 if(planet)
188 206 {
189 207 r.Spacing=N("Survey","Spacing",60,5,2000);int cells=(int)Math.Floor(radius/r.Spacing);
@@ -218,11 +236,17 @@ namespace AutoMiningScript
218 236 {
219 237 var r=generation.Peek();int index=r.Cursor++;reserved--;Dirty(r);
220 238 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;
221 if(r.Generation=="Manual")
239 if(r.Generation=="Manual" || r.Generation=="Mine")
222 240 {
223 241 int y=index/r.Columns,column=index%r.Columns,x=y%2==0?column:r.Columns-column-1;
224 242 double spanX=Data.Max(0,r.Width-FootprintWidth),spanY=Data.Max(0,r.Height-FootprintHeight);
225 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;
243 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));
244 if(r.Generation=="Manual")kind=JobKind.Manual;
245 else
246 {
247 var search=Region(r.Parent);depth=BoundDepth(search,entry,dir,depth);
248 add=(search==null||Inside(search,entry)) && depth>=1 && !KnownSurface(new JobReport {EntityId=r.EntityId,Position=entry},true);
249 }
226 250 }
227 251 else if(r.Generation=="Ground")
228 252 {
@@ -238,19 +262,11 @@ namespace AutoMiningScript
238 262 entry+=offset*(point==0?Data.Min(r.BaseClearance,r.Radius*0.2):r.Radius*0.35);
239 263 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;
240 264 }
241 else if(r.Generation=="Ring")
242 {
243 int x,y;RingPoint((2*r.Ring-1)*(2*r.Ring-1)+index,out x,out y);
244 entry+=right*x*StepX+r.Up*y*StepY;var search=Region(r.Parent);
245 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;
246 }
247 265 else throw Data.Invalid(L.FleetStoredRegionInvalid);
248 266 if(add&&Add(r,kind,entry,dir,depth)!=null)r.Added++;
249 267 if(r.Cursor>=r.Total)
250 268 {
251 generation.Dequeue();var emptyRing=r.Generation=="Ring"&&r.Added==0;r.Generation="";
252 if(emptyRing) {r.Status="Sampling complete: boundary";QueueAdvance(r.Parent);}
253 else QueueAdvance(r.Id);
269 generation.Dequeue();r.Generation="";QueueAdvance(r.Id);
254 270 }
255 271 }
256 272 }
@@ -266,6 +282,23 @@ namespace AutoMiningScript
266 282 var d=entry-r.Center;double along=Data.Dot(d,direction),disc=along*along+r.Radius*r.Radius-d.LengthSquared();
267 283 return Data.Min(depth,Data.Max(0,-along+Math.Sqrt(Data.Max(0,disc))));
268 284 }
285 void BeginBulk(MiningRegion r)
286 {
287 r.Bulk=true;
288 r.Columns=Data.Max(1,1+(int)Math.Ceiling(Data.Max(0,r.Width-FootprintWidth)/StepX));
289 r.Rows=Data.Max(1,1+(int)Math.Ceiling(Data.Max(0,r.Height-FootprintHeight)/StepY));
290 r.Total=Data.Max(0,Data.Min(r.Columns*r.Rows,Limit-Jobs.Count-reserved));r.Cursor=r.Added=0;
291 r.Status=r.Total==0?"Range incomplete: job budget limit":"Pending";
292 if(r.Total>0){r.Generation="Mine";Generate(r);}else QueueAdvance(r.Parent);
293 Dirty(r);
294 }
295 void Expand(Job seed)
296 {
297 var search=Region(seed.RegionId);if(search==null||!search.Search||seed.EntityId==0)return;
298 var patch=new MiningRegion {Id=Id("deposit"),Parent=search.Id,Center=seed.Entry,Direction=seed.Direction,Up=seed.Up,Patch=true,Ore=seed.ConfirmedOres,
299 Width=N("Survey","ExpansionWidth",60,1,100),Height=N("Survey","ExpansionHeight",60,1,100),Depth=N("Survey","MaxDepth",100,1,100),Radius=search.Radius,Gravity=search.Gravity,EntityId=seed.EntityId};
300 AddRegion(patch);seed.RegionId=patch.Id;Dirty(seed);BeginBulk(patch);
301 }
269 302 public void Report(Job j,JobReport report)
270 303 {
271 304 Dirty(j);
@@ -276,20 +309,10 @@ namespace AutoMiningScript
276 309 if(Finished(j)) return;
277 310 if (j.Kind == JobKind.Survey && report.Scan != null) j.Scan = report.Scan;
278 311 j.Progress=Data.Max(j.Progress,Data.Clamp(report.Progress,0,j.Depth));
279 // A terminal report carries accumulated ores, so a reordered/dropped Discovery
280 // message cannot erase a confirmed find. Processing is idempotent per hole.
281 if(report.Outcome=="Complete"&&report.Ore.Length>0)
282 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});
312 if(report.Ore.Length>0)discoveries.Add(j.Id);
283 313 if(report.Outcome=="Discovery")
284 314 {
285 var ore=PreferredOre(report.Ore); if(ore.Length==0) return;
286 var first=discoveries.Add(j.Id);
287 if(existing!=null && existing.Patch) {existing.RingFound=true;Dirty(existing);return;}
288 if(!first) return;
289 if(j.Kind!=JobKind.Probe || existing==null) return;
290 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,
291 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};
292 AddRegion(patch); j.RegionId=patch.Id; j.TargetOre=ore; return;
315 if(j.Kind==JobKind.Probe)Expand(j);return;
293 316 }
294 317 if(report.Outcome=="Paused") return;
295 318 if(report.Outcome=="SurveyHit" && j.Kind==JobKind.Survey)
@@ -298,8 +321,8 @@ namespace AutoMiningScript
298 321 var known=KnownSurface(report);
299 322 if(r!=null&&!known&&Inside(r,report.Position)&&report.EntityId!=0&&report.Direction.LengthSquared()>0.1)
300 323 {
301 var direction=Data.Unit(report.Direction,Data.Forward);var depth=BoundDepth(r,report.Position,direction,r.Depth);
302 if(depth>=1) Add(r,JobKind.Probe,report.Position,direction,depth,report.EntityId);
324 var direction=Data.Unit(report.Direction,Data.Forward);var depth=BoundDepth(r,report.Position,direction,N("Survey","MaxDepth",100,1,100));
325 if(depth>=1) {var seed=Add(r,JobKind.Probe,report.Position,direction,depth,report.EntityId);if(seed!=null)Expand(seed);}
303 326 }
304 327 }
305 328 j.Outcome=report.Outcome;
@@ -310,6 +333,8 @@ namespace AutoMiningScript
310 333 void QueueAdvance(string id) {if(id.Length>0&&advanceQueued.Add(id)) advanceQueue.Enqueue(id);}
311 334 public void Advance()
312 335 {
336 // Upgrade saved isolated probes without clearing active holes or their owners.
337 for(int n=0;legacyCursor<Jobs.Count && n++<4 && p.HasBudget(.55);) {var j=Jobs[legacyCursor++];if(j.Kind==JobKind.Probe)Expand(j);}
313 338 GenerateStep();
314 339 if(!p.HasBudget(0.55))return;
315 340 if(advancing==null)
@@ -338,16 +363,7 @@ namespace AutoMiningScript
338 363 advancing=null;Dirty(r);
339 364 if(r.Search) {if(!advancePending)r.Status=advanceBlocked?"Sampling incomplete: blocked observations":"Sampling complete (not proof of no ore)";return;}
340 365 if(advancePending)return;
341 if(!r.Patch) {r.Status=advanceBlocked?"Range incomplete: blocked":"Specified range complete";return;}
342 if(advanceBlocked) {r.Status="Sampling incomplete: blocked or invalid sample";QueueAdvance(r.Parent);return;}
343 if(!r.RingFound)r.EmptyRings++;else r.EmptyRings=0;
344 if(r.EmptyRings>=2) {r.Status="Sampling complete: two empty rings";QueueAdvance(r.Parent);return;}
345 r.RingFound=false;r.Ring++;
346 r.Columns=(int)Math.Floor(r.Width/(2*StepX));r.Rows=(int)Math.Floor(r.Height/(2*StepY));
347 if(r.Ring>Data.Max(r.Columns,r.Rows)) {r.Status="Sampling complete: range limit";QueueAdvance(r.Parent);return;}
348 r.Total=8*r.Ring;r.Cursor=r.Added=0;
349 if(Jobs.Count+reserved+r.Total>Limit) {r.Status="Sampling incomplete: job budget limit";QueueAdvance(r.Parent);return;}
350 r.Generation="Ring";Generate(r);
366 r.Status=r.Bulk&&r.Total<r.Columns*r.Rows?"Range incomplete: job budget limit":advanceBlocked?"Range incomplete: blocked":"Specified range complete";QueueAdvance(r.Parent);
351 367 }
352 368 public void Save(MyIni ini)
353 369 {
@@ -439,7 +455,7 @@ namespace AutoMiningScript
439 455 }
440 456 void ClearLoadedTasks()
441 457 {
442 Jobs.Clear();Regions.Clear();jobsById.Clear();regionsById.Clear();surfaceCells.Clear();unreachableEntries.Clear();OwnedJobs.Clear();PendingCount=0;generation.Clear();reserved=0;advancing=null;discoveries.Clear();advanceQueue.Clear();advanceQueued.Clear();
458 Jobs.Clear();Regions.Clear();jobsById.Clear();regionsById.Clear();surfaceCells.Clear();unreachableEntries.Clear();OwnedJobs.Clear();PendingCount=0;generation.Clear();reserved=legacyCursor=0;advancing=null;discoveries.Clear();advanceQueue.Clear();advanceQueued.Clear();
443 459 }
444 460 void RestoreJob(Job j)
445 461 {
@@ -448,8 +464,10 @@ namespace AutoMiningScript
448 464 }
449 465 void RestoreRegion(MiningRegion r)
450 466 {
451 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);
452 AddRegion(r);RestoreGeneration(r);if(r.Status=="Pending")QueueAdvance(r.Id);
467 if(r.Id.Length==0||r.Width<0||r.Height<0||r.Depth<0||r.Radius<0||(r.Patch&&(r.Width>100||r.Height>100||r.Depth>100)))throw Data.Invalid(L.FleetStoredRegionInvalid);
468 AddRegion(r);
469 if(r.Patch&&!r.Bulk){r.Generation="";BeginBulk(r);}else RestoreGeneration(r);
470 if(r.Status=="Pending")QueueAdvance(r.Id);
453 471 }
454 472 public void LoadRecord(MyIni ini,string kind)
455 473 {
@@ -479,8 +497,9 @@ namespace AutoMiningScript
479 497 void RestoreGeneration(MiningRegion r)
480 498 {
481 499 if(r.Generation.Length==0)return;
482 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);
500 if((r.Generation!="Manual"&&r.Generation!="Ground"&&r.Generation!="Space"&&r.Generation!="Mine")||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);
483 501 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);
502 if(r.Generation=="Mine" && (!r.Bulk||!r.Patch||r.Columns<1||r.Rows<1||r.Total>(long)r.Columns*r.Rows))throw Data.Invalid(L.FleetStoredRegionInvalid);
484 503 Generate(r);
485 504 }
486 505 public void Load(MyIni ini)
Modified Localization/miner.en-US.json +1 -1
@@ -2,7 +2,7 @@
2 2 "MinerFindingRoute": "Scanning bypass",
3 3 "MinerSurveyEntryStalled": "Entry blocked; await reassignment",
4 4 "MinerAutomaticReturn": "Init; docking",
5 "MinerWaitNextTask": "Await regional task; probes first",
5 "MinerWaitNextTask": "Await regional task; mining first",
6 6 "MinerSearchCompleteReturn": "Region done; returning",
7 7 "MinerTrafficYield": "Yield to nearby {0}",
8 8 "MinerMissingController": "No ship controller",
Modified Localization/miner.zh-CN.json +1 -1
@@ -2,7 +2,7 @@
2 2 "MinerFindingRoute": "正在扫描可行绕路",
3 3 "MinerSurveyEntryStalled": "探索入口持续无法接近,已报告受阻并等待续派",
4 4 "MinerAutomaticReturn": "矿机初始化完成,自动返航接港",
5 "MinerWaitNextTask": "本项作业完成,等待本矿区续派(优先试钻)",
5 "MinerWaitNextTask": "本项作业完成,等待本矿区续派(优先开采)",
6 6 "MinerSearchCompleteReturn": "本矿区暂无待执行任务,返航待命",
7 7 "MinerTrafficYield": "近距离避让:{0}",
8 8 "MinerMissingController": "缺少船舶控制器",
Modified README.md +14 -10
@@ -1,6 +1,6 @@
1 1 # XFE AMS 2.0
2 2
3 XFE AMS 是 Space Engineers 原版编程块采矿舰队系统:自主推进器与陀螺飞控、移动母舰靠泊、按实际库存循环卸货、按总储能计算电量、有限区域探索试钻、手动范围采掘,以及 LCD Sprite 仪表盘。一次构建同时生成简体中文和英文两套脚本,每套包含四个角色;编程块自带屏幕默认显示图形化配置与状态。
3 XFE AMS 是 Space Engineers 原版编程块采矿舰队系统:自主推进器与陀螺飞控、移动母舰靠泊、按实际库存循环卸货、按总储能计算电量、有限区域探索与多机并行开采、手动范围采掘,以及 LCD Sprite 仪表盘。一次构建同时生成简体中文和英文两套脚本,每套包含四个角色;编程块自带屏幕默认显示图形化配置与状态。
4 4
5 5 矿机分别可以在大气/重力环境和太空作业;单次任务不负责穿越大气层。远程控制可作为位置、速度和姿态测量方块,飞行不使用其自动驾驶航点。系统面向单一刚性网格矿机,设计上限为 16 台。
6 6
@@ -38,7 +38,7 @@ XFE AMS 是 Space Engineers 原版编程块采矿舰队系统:自主推进器
38 38 - 至少一块电池、足够容量的货舱、连接器、完整的传送系统。使用氢推进时需要储氢罐;库存中的冰不被当作保证可用的氢燃料。
39 39 - 能覆盖作业范围的通信设备。建议添加单独安全看门狗编程块。
40 40
41 不支持子网格推进器、转子/活塞钻具或子网格连接器。不可用“同构造”扫描来代替矿机所属网格,因为连接器会把母舰与矿机临时连成同一构造。运输载荷仅统计本刚性网格的货仓、钻头和连接器;反应堆、制气机等功能方块的供料库存另行保留,全部库存质量仍计入矿机称重。母舰库存和质量不计入。
41 不支持子网格推进器、转子/活塞钻具或子网格连接器。不可用“同构造”扫描来代替矿机所属网格,因为连接器会把母舰与矿机临时连成同一构造。运输载荷统计本刚性网格的货仓、钻头、连接器和分拣器;反应堆、制气机等功能方块的供料库存另行保留,全部库存质量仍计入矿机称重。母舰库存和质量不计入。
42 42
43 43 母舰需要一个船舶控制器、与“泊位方块组”配置一致的连接器组、与“基地货仓组”一致的接收货舱组。中文版新模板默认名称为“采矿泊位”“基地货仓”;旧部署可继续使用 `AMS Docks`、`AMS Cargo`。组内受管方块必须在主控的刚性网格上。每个泊位的外侧预留完整的直线进出港通道,避免装饰、太阳能板或其他停泊船侵入;母舰可以移动,但只能在运动窗口满足条件时进出港。
44 44
@@ -176,7 +176,7 @@ XFE AMS 是 Space Engineers 原版编程块采矿舰队系统:自主推进器
176 176
177 177 范围被分成有重叠的平行孔,采用固定蛇形顺序。手动任务会完成规定范围和深度,遇到无目标矿段也继续;安全故障仍会使任务中止或暂停。实际开挖可能略超出几何边界,取决于钻头实际作用体积,不能用于贴近重要设施的精密切削。
178 178
179 ### 自动探索、试钻与指定矿种优先
179 ### 自动探索、并行开采与矿种记录
180 180
181 181 ```text
182 182 探索
@@ -188,21 +188,25 @@ XFE AMS 是 Space Engineers 原版编程块采矿舰队系统:自主推进器
188 188
189 189 航行避障与探索扫描都利用已安装的前后、左右、上下各方向镜头。目标方向已有工作镜头覆盖时保留船体姿态;镜头正在充能时等待或使用同方向其他镜头,不因缺电转船。只有目标射线缺少有效视场时,才按实际镜头安装朝向转动探测,并检查该姿态的悬停与制动能力。扫描阶段以已接受的观察位置停驻,不再要求朝侧面挪回更精确的坐标。射线终点仍由冻结的探索任务决定,实际命中位置与方向回传主控。导航避障射线命中的行星或小行星表面也会交给探矿逻辑:若小行星挡在观察点前方,可直接报告实际命中并申请试钻,不必先穿过小行星到点。主控仍核对冻结区域边界并去重,摄像机列表仍仅在初始化或明确重扫时更新。
190 190
191 扫描新发现小行星后,收到发现报告与完成遥测即可优先安排下一次试钻派发;发现者资源不足时由其他可用矿机接手。其它矿机保留已有扫描任务,正在钻孔的矿机不会被抢占。
191 扫描命中表面后,主控立即建立开采区并分帧生成整片孔位,不再等待第一台矿机发现矿物或退出试钻孔。默认范围为 60 × 60 m、深度 100 m,并受原搜索边界限制;默认覆盖配置在未被边界裁剪时生成 144 个网格孔位及原始命中点。多台可用矿机可以同时领取同一区域的不同孔位。已有扫描和钻孔任务继续执行,完成后优先续派开采任务。
192 192
193 **自动探索不再每扫一条射线就返航。** 完成扫描或退出已完成的自动试钻孔后,矿机显示“等待本矿区续派”,主控优先分配同一搜索区的新试钻点,其次安排附近未完成的观察方向。相同观察点的不同扫描方向可以原地连续执行。其它矿机正在占用所需空间时等待,不能把暂时无可用任务算成整个区域完成。返航条件仍包括满载、低电/低氢、动力或通信问题、返航指令,以及本区域已无待执行任务;不自动转去另一个搜索区。手动范围采掘仍沿原流程完成规定孔道。
193 选孔按舰队船体半径把网格分成相互隔开的批次,先分配分散孔位,再填补间隙,避免最近点优先把可用区域挤碎。最终分配仍检查孔道间距、活动矿机和未确认退出的占用;区域过小、能源不足或船体尚未退出时会等待,不强行往同一孔道塞入矿机。单舰队最多 16 台,受管母舰泊位也须足够。
194
195 **自动探索不再每扫一条射线就返航。** 完成扫描或退出已完成的开采孔后,矿机显示“等待本矿区续派”,主控优先分配同一搜索区的开采孔位,其次安排附近未完成的观察方向。相同观察点的不同扫描方向可以原地连续执行。其它矿机正在占用所需空间时等待,不能把暂时无可用任务算成整个区域完成。返航条件仍包括满载、低电/低氢、动力或通信问题、返航指令,以及本区域已无待执行任务;不自动转去另一个搜索区。手动范围采掘仍沿原流程完成规定孔道。
194 196
195 197 空中续派会重新核算当前位置到新任务、退钻和返回移动母舰的能源;不套用停靠时的5%离港载荷和90%充电锁存门槛。预算不足时保留新领取任务并返航补给。结果会重发并随存档保留,续派绑定上一任务编号;晚到任务不会撤销暂停、返航或人工接管。更新时同时替换主控和所有矿机脚本,恢复上线后在主控运行 `继续`,可保留已有探索任务,无需重新创建矿区或逐机标定。
196 198
197 摄像机能判断实体及表面命中,**不能透视矿种**。系统钻进时每 5 m 比较货仓、钻头和连接器中的矿物库存增量,记录全部检出矿物,同一采样可确认多种矿物。`目标矿种` 是自动矿点的采集范围,默认留空,表示全部采集(包括石头);显式填写列表时只安排其中的已确认矿点。`优先矿种` 单独决定已确认矿点的调度顺序,默认 `铁,钴,镍,硅`,列表靠前者优先;其余允许采集的矿物作为普通矿点。优先列表不会扩大目标范围。已有自定义数据不会自动重写,旧部署若要启用全矿种采集,需要将原 `目标矿种` 的值清空。
199 摄像机能判断实体及表面命中,**不能透视矿种**。系统钻进时每 5 m 比较货仓、钻头、连接器和分拣器中的矿物库存增量,记录全部实际检出矿物,同一采样可确认多种矿物。记录用于矿图和后续派工优先级,不作为继续挖掘的门槛;当前孔没矿,或只钻到石头,不代表整颗小行星没有目标矿物。`目标矿种` 默认留空,表示全部矿物(包括石头);显式列表限定哪些发现参与优先判断,但不会取消尚未挖掘的孔。`优先矿种` 默认 `铁,钴,镍,硅`,列表靠前者优先,且仍须在目标范围内。已有自定义数据不会被重写。
200
201 库存内部转运不会被算成新发现。采样时受管制气/加工/抛货会停用;如果仍存在库存损失或无法隔离,会提示采样无效并将当前孔标为受阻,不会推断“这里没有矿”。其他未开采孔仍可分配。
198 202
199 库存内部转运不会被算成新发现。采样时受管制气/加工/抛货会停用;如果仍存在库存损失或无法隔离,会提示采样无效并将未证实的试钻标为受阻,不会推断“这里没有矿”。
203 自动开采统一使用 `[探索] 最大深度`,默认 100 m;旧 `试钻深度` / `ProbeDepth` 保留配置兼容,但不再限制新任务。`扩挖宽度`、`扩挖高度` 和原搜索边界继续有效。不再因连续若干孔或两圈未发现目标矿物而停止。矿机按载荷和能源往返补给,继续未完成孔位,直到指定范围完成或明确报告受阻、预算不足;不会无限扩大区域。
200 204
201 默认试钻深度为大气 60 m、太空 40 m,`[探索] 试钻深度` 可显式设置。发现区的扩挖受 `扩挖宽度`、`扩挖高度`、`最大深度` 和原搜索边界共同限制。连续两圈相邻孔没有目标矿增量后停止扩张。搜索完成后返航等待,不无限增加范围或深度。
205 加载旧任务时,主控会逐步扩展孤立试钻点,并把旧环带区域迁移为整片网格,保留已完成孔、正在执行的孔及其进度和归属。因旧“两圈没矿”规则结束的区域可继续开采;迁移后的网格和生成游标会持久保存,重载不会反复创建相同任务。无需清空任务或重新标定。
202 206
203 207 `[探索] 母舰安全间距=80` 将大气观察点沿反重力方向抬高 80 m;太空首个观察点也避开母舰中心,且保持在搜索球内。搜索区域仍以创建时的世界坐标固定。按母舰体积和附近地形增大净空;不把母舰内部当作可到达的观察位置。入孔前执行中心和四周共五点检查,`[采矿] 入口高差上限=5` 限制新孔入口的地表起伏;已有孔道发现其他船体占用时拒绝钻进。
204 208
205 请区分结果:`SurveyEmpty` 表示该次扫描没有表面命中;试钻未检出只表示已采样部分没有目标矿增量;“采样完成”不等于已经确定地下无矿;手动任务的“指定范围采掘完成”才表示其计划钻孔全部完成。受阻或无效采样的区域不能当作正常完成。
209 请区分结果:`SurveyEmpty` 表示该次扫描没有表面命中;钻采未检出只表示已采样部分没有目标矿增量;“采样完成”不等于已经确定地下无矿;开采区的“指定范围采掘完成”表示其计划钻孔全部完成。受阻、无效采样或任务预算不足的区域不能当作正常完成。
206 210
207 211 ## 周转、能源与移动母舰
208 212
@@ -432,7 +436,7 @@ XFE AMS 是 Space Engineers 原版编程块采矿舰队系统:自主推进器
432 436 | 到接口附近又向外飞,或30多米就以0.1m/s挪动 | 更新矿机脚本并从主控运行 `返航 矿机编号`。有效分配下直接接近接口;若显示近距让行,完成避让后继续。已标定的 `最终接近速度` 只用于最后1米,最后3米仍确认姿态稳定;正常轴向贴合免扫描,无需重新手动标定 |
433 437 | 对接反复退出 | 母舰加速/旋转、遥测过期、泊位阻塞、连接器几何/钻尖标定错误 |
434 438 | 已被连接器吸住,仍显示进港对接 | 更新矿机脚本;新版在连接器报告可连接时直接锁定,成功后立即上报连接状态,随后按实际需求显示卸货、充能或待命,不再被姿态、速度或稳定计时拦住 |
435 | 自动探索未检出 | 可见表面扫描不代表地下矿床;检查试钻深度、采样有效性与目标矿种配置 |
439 | 自动探索未检出 | 可见表面扫描不代表地下矿床;新策略继续开采剩余孔位,可检查最大深度、区域范围和采样有效性 |
436 440 | 旧矿机失联后任务不派出 | 保守保留未知占用;先找到失联矿机并安全回收,不能仅等待超时 |
437 441 | 改名后大屏保留旧矿机 | 身份取自 `[系统] 编号`;新身份上线、旧条目显示离线后,在主控运行 `清理离线矿机`。只清名单,旧任务和未知通道占用仍保留 |
438 442
Modified docs/PARAMETERS.zh-CN.md +1 -1
@@ -69,7 +69,7 @@
69 69 | `入口净空距离` | `StandOff` |
70 70 | `旧版载荷返航阈值` | `CargoReturn` |
71 71 | `搜索半径` | `Radius` |
72 | `试钻深度` | `ProbeDepth` |
72 | `试钻深度`(旧配置兼容;新自动开采用“最大深度”) | `ProbeDepth` |
73 73 | `母舰安全间距` | `BaseClearance` |
74 74 | `采样间距` | `Spacing` |
75 75 | `扫描距离` | `ScanDistance` |
Modified docs/VALIDATION.md +11 -0
@@ -4,6 +4,17 @@
4 4
5 5 ## 开发机检查
6 6
7 ### 2026-10-10 并行大范围开采与原存档更新
8
9 修复自动探索先生成一个试钻任务、检出目标矿后才扩挖且连续两圈无矿就停止的派工瓶颈。现在表面命中即建立有限开采区,按分帧游标生成整个网格;默认 60 × 60 m、最大深度 100 m,未受边界裁剪时为 144 个网格孔和原始命中孔。矿物确认只补充矿图和派工优先级,没有检出或仅检出非目标矿都不会取消剩余孔。孔位按船体间距分批优先选取,保留实际孔道占用检查;派工每轮最多检查 64 项并保留指令预算保护,缩短大量历史扫描任务下的分配等待。
10
11 - 实际主控调度模型中,12 台和 16 台可用矿机在未确认任何矿物时均取得不同开采孔位;半径 4.9 m 的模型中,所有已分配孔道两两最小间距不低于 11.8 m。这验证任务分配和空间预留,不代表游戏内同时钻进已实测通过。
12 - 普通与分帧记录存档覆盖新网格生成中途恢复、旧孤立试钻迁移、旧环带生成迁移和“两圈无矿”完成区恢复;保留既有孔位、任务编号、进度、归属及完成结果,避免重复生成已存在孔。
13 - 回归覆盖无矿仍继续、非目标矿不排除未挖区、任务预算不足的明确结果、最大深度替代旧试钻深度,以及搜索球边缘的入口和钻孔终点裁剪。新生成器的受阻判定和能源往返流程继续沿用原实现。
14 - 完整中英文构建及八份实际导出编译、角色检查通过,均不超过 100000 字符。补齐存档与边界回归后,中英文各 **893/893** 项测试通过;记录为 `obj/bulk-mining-build.log`、`TestResults/bulk-final-zh-CN.trx`、`TestResults/bulk-final-en-US.trx`。中文矿机 97141 字符,SHA-256 `9b130c141a8ce9d2fc22234fb5bd93d87f0bd41f7eb1c5b36a233ac7c1f9e917`;中文主控 98106 字符,SHA-256 `60c5420dbbd97d8e3eb30922f18b9ddc902dc83a708b1d7558f713e22be9b5ef`。英文矿机 99542、主控 99264 字符。
15
16 通过 AgentBridge 通用离线部署接口更新原存档“飚船啦兄弟萌”的 1 个主控和 12 台矿机。部署后逐项核对全部源码摘要、Storage 与启用状态,156 个编程块中的其余 143 个源码保持不变。备份位于 `%LOCALAPPDATA%\XFE\SpaceEngineersAgent\backups\20261010T101500803-24c04ae1e5b844d3b92f2b1c654639bc`,预检、写入与核验记录位于 `artifacts/agent-debug/world-install-20261010T101447Z-6f4354c0/`。本轮未启动游戏,存档将在下次加载时编译新脚本;尚未完成此版本的游戏内采矿循环验证。
17
7 18 ### 2026-09-16 原存档部署
8 19
9 20 按用户要求,通过 AgentBridge 新增的通用离线管理 CLI 将中文最终导出写入原存档“飚船啦兄弟萌”:1 个主控、12 台矿机;156 个编程块中其余 143 个源码未变。写入前完整备份活动存档文件,逐个核对新源码摘要,保留 Storage、CustomData、启用状态、世界配置和其他设备,失效旧二进制 sector 缓存。此次没有加载原存档,因此不将离线写入当作该存档的游戏内编译或完整循环验收。具体证据、备份及后续限制见 [部署记录](agent-debug/ORIGINAL_DEPLOYMENT_20260916.md)。