返回提交历史
Added
AutoMiningScript.Tests/BulkMiningTests.cs
+117
-0
Modified
AutoMiningScript.Tests/PlannerTests.cs
+26
-26
Modified
AutoMiningScript.Tests/SurveyNavigationTests.cs
+1
-1
Modified
AutoMiningScript.Tests/UnreachableSurveyTests.cs
+3
-3
Modified
AutoMiningScript/FleetController.cs
+4
-3
Modified
AutoMiningScript/Planning.cs
+67
-48
Modified
Localization/miner.en-US.json
+1
-1
Modified
Localization/miner.zh-CN.json
+1
-1
Modified
README.md
+14
-10
Modified
docs/PARAMETERS.zh-CN.md
+1
-1
Modified
docs/VALIDATION.md
+11
-0
SpaceEngineersModDev/AutoMiningScript
Plan bulk excavation on surface discovery and distribute work across the fleet
23812f6
代码差异
11 个文件
+246
-94
@@ -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
}
@@ -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()
@@ -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
@@ -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]
@@ -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
}
@@ -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)
@@ -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",
@@ -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": "缺少船舶控制器",
@@ -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
@@ -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` |
@@ -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)。