返回提交历史
Modified
AutoMiningScript.Tests/DockingNavigationTests.cs
+212
-9
Added
AutoMiningScript.Tests/FleetHullTrafficTests.cs
+143
-0
Modified
AutoMiningScript.Tests/FleetTrafficTests.cs
+1
-1
Added
AutoMiningScript.Tests/UnreachableSurveyTests.cs
+133
-0
Modified
AutoMiningScript/Core.cs
+13
-2
Modified
AutoMiningScript/FleetController.cs
+213
-120
Modified
AutoMiningScript/Flight.cs
+12
-0
Modified
AutoMiningScript/MinerController.cs
+63
-15
Modified
AutoMiningScript/Planning.cs
+21
-3
Modified
Localization/automatic.en-US.json
+3
-3
Modified
Localization/camera.en-US.json
+15
-15
Modified
Localization/common.en-US.json
+27
-27
Modified
Localization/display.en-US.json
+15
-15
Modified
Localization/docking.en-US.json
+11
-10
Modified
Localization/docking.zh-CN.json
+2
-1
Modified
Localization/fleet.en-US.json
+46
-46
Modified
Localization/fleetoperations.en-US.json
+5
-5
Modified
Localization/miner.en-US.json
+134
-134
Modified
docs/VALIDATION.md
+15
-0
SpaceEngineersModDev/AutoMiningScript
修复实机探索续派、船体避让与对接捕获
ddde3ad
代码差异
19 个文件
+1084
-406
@@ -30,6 +30,7 @@ namespace AutoMiningScript.Tests
30
30
public double Mass=1000,ContactSpacing=1;
31
31
public readonly List<P.FlightState> States=new List<P.FlightState>();
32
32
public Func<Vector3D,MyDetectedEntityInfo> Hit=p=>default(MyDetectedEntityInfo);
33
public Func<bool> Capture;
33
34
public DockRig(double finalSpeed=.2)
34
35
{
35
36
R.PrepareDeparture("valid-lease");R.Connector.Set("Status",MyShipConnectorStatus.Unconnected);
@@ -87,11 +88,11 @@ namespace AutoMiningScript.Tests
87
88
public Vector3D PortPosition {get{return R.Position+Vector3D.TransformNormal(R.Hardware.ConnectorLocal,Orientation);}}
88
89
public MatrixD PortPose {get{return Mount*Orientation;}}
89
90
public double Gap {get{return Vector3D.Dot(PortPosition-TargetPort,Dock.Forward);}}
90
bool CanConnect {get{return Vector3D.Distance(PortPosition,TargetPort)<.15 && Vector3D.Dot(PortPose.Forward,-Dock.Forward)>.9998 && Vector3D.Dot(PortPose.Up,Dock.Up)>.9998;}}
91
bool CanConnect {get{return (Capture!=null?Capture():Vector3D.Distance(PortPosition,TargetPort)<.15) && Vector3D.Dot(PortPose.Forward,-Dock.Forward)>.9998 && Vector3D.Dot(PortPose.Up,Dock.Up)>.9998;}}
91
92
public void Tick(double dt=.05,bool integrate=false)
92
93
{
93
94
R.Program.Now+=dt;Dock.Position+=Dock.Velocity*dt;Dock.Center+=Dock.Velocity*dt;Dock.ReceivedAt=R.Program.Now;
94
string method=R.StateNow==P.FlightState.Holding?"Waiting":R.StateNow==P.FlightState.Returning?"ReturnHome":R.StateNow==P.FlightState.DockAlign?"DockAlign":R.StateNow==P.FlightState.DockApproach?"DockApproach":null;
95
string method=R.StateNow==P.FlightState.Holding?"Waiting":R.StateNow==P.FlightState.Returning?"ReturnHome":R.StateNow==P.FlightState.DockAlign?"DockAlign":R.StateNow==P.FlightState.DockApproach?"DockApproach":R.StateNow==P.FlightState.DockRetreat?"DockRetreat":null;
95
96
if(method==null)return;
96
97
Call(R.Miner,method,dt);States.Add(R.StateNow);
97
98
if(!integrate || Connected)return;
@@ -115,6 +116,127 @@ namespace AutoMiningScript.Tests
115
116
}
116
117
}
117
118
119
static MyDetectedEntityInfo SphereHit(DockRig r,Vector3D center,double radius,Vector3D endpoint)
120
{
121
var delta=endpoint-r.R.Position;double length=delta.Length();if(length<.001)return default(MyDetectedEntityInfo);
122
Vector3D direction=delta/length,offset=r.R.Position-center;double along=Vector3D.Dot(offset,direction);
123
double discriminant=along*along-offset.LengthSquared()+radius*radius;
124
if(discriminant<0)return default(MyDetectedEntityInfo);
125
double distance=-along-Math.Sqrt(discriminant);if(distance<0 || distance>length)return default(MyDetectedEntityInfo);
126
return new MyDetectedEntityInfo(96,"Stationary fault miner",MyDetectedEntityType.SmallGrid,r.R.Position+direction*distance,
127
MatrixD.Identity,Vector3.Zero,VRage.Game.MyRelationsBetweenPlayerAndBlock.NoOwnership,
128
new BoundingBoxD(center-new Vector3D(radius),center+new Vector3D(radius)),1);
129
}
130
131
[Fact]
132
public void OccupiedOuterGuideSelectsACameraCheckedAxisEntryAndCompletesDocking()
133
{
134
var r=new DockRig();r.Dock.ApproachDistance=100;r.R.Hardware.Radius=4.63146;
135
r.R.Hardware.BodyHalfSize=new Vector3D(2.25,1.75,2.25);
136
r.R.Hardware.BodyMin=-r.R.Hardware.BodyHalfSize;r.R.Hardware.BodyMax=r.R.Hardware.BodyHalfSize;
137
foreach(var direction in new[]{Vector3D.Right,Vector3D.Left,Vector3D.Up,Vector3D.Down})r.AddCamera(direction);
138
var obstacle=r.Contact+r.Dock.Forward*100;double radius=2.25;
139
r.R.Position=r.Contact+r.Dock.Forward*93+Vector3D.Right*11;
140
r.Hit=end=>SphereHit(r,obstacle,radius,end);
141
double nearest=double.MaxValue;bool alternate=false;
142
for(int n=0;n<3600 && !r.Connected && r.R.StateNow!=P.FlightState.DockRetreat;n++)
143
{
144
r.Tick(.05,true);nearest=Math.Min(nearest,Vector3D.Distance(r.R.Position,obstacle));
145
alternate|=Read<double>(r.R.Miner,"dockJoinDepth")>0 && Read<double>(r.R.Miner,"dockJoinDepth")<99;
146
}
147
Assert.True(alternate,"The occupied nominal entry was never replaced.");
148
Assert.True(r.Rays>0);Assert.True(r.Connected,r.Reason);Assert.Equal(P.FlightState.Servicing,r.R.StateNow);
149
Assert.True(nearest>=radius+r.R.Hardware.Radius,"Hull clearance: "+(nearest-radius));
150
Assert.Equal(1,r.ConnectCalls);
151
}
152
153
[Fact]
154
public void FreeGuideKeepsItsGeometryAndAChosenAlternativeStaysStableUntilCapture()
155
{
156
var r=new DockRig();r.Dock.ApproachDistance=30;r.R.Position=r.Contact+r.Dock.Forward*23+Vector3D.Right*11;
157
var nominal=r.Contact+r.Dock.Forward*30;r.Tick();Assert.Equal(nominal,r.Flight.RouteTarget);
158
r.Hit=end=>SphereHit(r,nominal,2,end);
159
for(int n=0;n<160 && Read<double>(r.R.Miner,"dockJoinDepth")==30;n++)r.Tick(.05,true);
160
double selected=Read<double>(r.R.Miner,"dockJoinDepth");Assert.InRange(selected,5,29.99);
161
// New nominal clearance does not cancel a still-advancing alternate route.
162
r.Hit=end=>default(MyDetectedEntityInfo);
163
for(int n=0;n<8 && r.R.StateNow==P.FlightState.DockAlign;n++)
164
{r.Tick(.05,true);Assert.Equal(selected,Read<double>(r.R.Miner,"dockJoinDepth"));}
165
r.Finish();Assert.True(r.Rays>0);
166
}
167
168
[Fact]
169
public void UnobservedAlternateEntriesNeverAuthorizeMovementAndHaveABoundedSearch()
170
{
171
var r=new DockRig();r.Dock.ApproachDistance=100;r.CamerasReady=false;
172
r.R.Position=r.Contact+r.Dock.Forward*93+Vector3D.Right*11;var start=r.R.Position;
173
for(int n=0;n<300 && r.R.StateNow==P.FlightState.DockAlign;n++)r.Tick(.5,true);
174
Assert.Equal(P.FlightState.Holding,r.R.StateNow);Assert.InRange(Read<int>(r.R.Miner,"dockJoinAttempt"),1,7);
175
Assert.Equal(0,r.Rays);Assert.Equal(0,r.ConnectCalls);Assert.InRange(Vector3D.Distance(start,r.R.Position),0,.001);
176
Assert.Contains(r.R.Sent,p=>p.Kind=="LANE_RELEASE");
177
}
178
179
[Fact]
180
public void TrafficYieldsGetShortGraceButCannotRenewTheDockingProgressWatchdogForever()
181
{
182
var r=new DockRig();r.CamerasReady=false;r.R.Position=r.Contact+r.Dock.Forward*30+Vector3D.Right*10;
183
Write(r.R.Miner,"progressAt",r.R.Program.Now-59.5);
184
for(int n=0;n<500 && r.R.StateNow==P.FlightState.DockAlign;n++)
185
{
186
r.R.Program.Now+=.05;r.Dock.ReceivedAt=r.R.Program.Now;r.Flight.TrafficWait=true;r.Flight.TrafficAt=r.R.Program.Now;
187
Call(r.R.Miner,"YieldAtPort",.05);
188
if(n==119)Assert.Equal(P.FlightState.DockAlign,r.R.StateNow);
189
}
190
Assert.Equal(P.FlightState.Holding,r.R.StateNow);Assert.Contains(r.R.Sent,p=>p.Kind=="LANE_RELEASE");
191
Assert.Equal(0,r.ConnectCalls);
192
}
193
194
static void TrafficSurvey(DockRig r)
195
{
196
Write(r.R.Miner,"job",new P.Job {Id="observation",Kind=P.JobKind.Survey,Entry=r.R.Position+Vector3D.Forward*100,Direction=Vector3D.Forward,Up=Vector3D.Up});
197
r.R.State(P.FlightState.Transit);r.Flight.TrafficWait=true;
198
}
199
[Fact]
200
public void SustainedTrafficCannotBypassTheSurveyEntryDeadlineOrLeaveItIdle()
201
{
202
var r=new DockRig();TrafficSurvey(r);double start=r.R.Program.Now;
203
for(int n=0;n<1200 && r.R.StateNow==P.FlightState.Transit;n++)
204
{r.R.Program.Now+=.05;r.Flight.TrafficAt=r.R.Program.Now;Call(r.R.Miner,"YieldAtPort",.05);}
205
var packet=Assert.Single(r.R.Sent,p=>p.Kind=="RESULT");var report=P.JobReport.FromIni(packet.Body);
206
Assert.Equal("Blocked",report.Outcome);Assert.True(report.EntryUnreachable);
207
Assert.InRange(r.R.Program.Now-start,55,56);Assert.Equal(P.FlightState.Survey,r.R.StateNow);
208
Assert.Equal("observation",Read<P.JobReport>(r.R.Miner,"nextReport").JobId);Assert.True(Read<bool>(r.R.Miner,"continuing"));
209
}
210
[Fact]
211
public void ActualSurveyProgressDuringTrafficKeepsItsDeadlineAlive()
212
{
213
var r=new DockRig();TrafficSurvey(r);
214
for(int n=0;n<1200;n++)
215
{
216
r.R.Program.Now+=.05;if(n%40==0)r.R.Position+=Vector3D.Forward*2.5;
217
r.Flight.TrafficAt=r.R.Program.Now;Call(r.R.Miner,"YieldAtPort",.05);
218
}
219
Assert.Equal(P.FlightState.Transit,r.R.StateNow);Assert.DoesNotContain(r.R.Sent,p=>p.Kind=="RESULT");
220
}
221
[Fact]
222
public void CameraSurfaceFoundWhileYieldingTakesPriorityOverAnExpiredSurveyDeadline()
223
{
224
var r=new DockRig();TrafficSurvey(r);Write(r.R.Miner,"lastProgress",100d);Write(r.R.Miner,"progressAt",r.R.Program.Now-60);
225
r.Flight.SurfaceHit=new P.JobReport {Outcome="SurveyHit",EntityId=42,Position=r.R.Position+Vector3D.Forward*10,Direction=Vector3D.Forward};
226
r.Flight.TrafficAt=r.R.Program.Now;Call(r.R.Miner,"YieldAtPort",.05);
227
var report=P.JobReport.FromIni(Assert.Single(r.R.Sent,p=>p.Kind=="RESULT").Body);
228
Assert.Equal("SurveyHit",report.Outcome);Assert.False(report.EntryUnreachable);Assert.Equal(42,report.EntityId);
229
}
230
[Fact]
231
public void HullTelemetryUsesControllerRelativeBoundsAndControllerPointVelocity()
232
{
233
var r=new DockRig();r.R.Position=new Vector3D(10,20,30);r.R.Controller.Set("CenterOfMass",new Vector3D(8,20,30));
234
r.Velocity=new Vector3D(1,2,3);r.AngularVelocity=new Vector3D(0,0,.5);
235
var snapshot=(P.Telemetry)r.R.Miner.GetType().GetMethod("Snapshot",Private).Invoke(r.R.Miner,null);
236
Assert.True(snapshot.HasBodyBounds);Assert.Equal(r.R.Hardware.BodyMin,snapshot.BodyMin);Assert.Equal(r.R.Hardware.BodyMax,snapshot.BodyMax);
237
Assert.Equal(new Vector3D(1,3,3),snapshot.Velocity);Assert.Equal(r.AngularVelocity,snapshot.AngularVelocity);Assert.Equal(2,snapshot.RotationRadius);
238
}
239
118
240
[Theory]
119
241
[InlineData(0d)]
120
242
[InlineData(1d)]
@@ -284,6 +406,82 @@ namespace AutoMiningScript.Tests
284
406
Assert.True(r.Connected,r.Gap+" / "+r.Reason);Assert.Equal(1,r.ConnectCalls);Assert.Equal(0,r.Rays);
285
407
}
286
408
409
static DockRig MeasuredCaptureRig(bool side=false)
410
{
411
var r=new DockRig(.5);r.Mass=10999.4;r.ContactSpacing=2.226945938;
412
if(side)r.Mount=MatrixD.CreateWorld(Vector3D.Zero,Vector3D.Right,Vector3D.Up);
413
r.MountPort(r.Mount,side?Vector3D.Right*1.75:Vector3D.Backward*1.75,r.Desired,Vector3D.Zero);
414
r.Orientation=r.Desired;r.R.Position=r.Contact+r.Dock.Forward*.00075;
415
r.R.State(P.FlightState.DockApproach);Write(r.R.Miner,"stableSince",r.R.Program.Now-3);
416
return r;
417
}
418
419
[Theory]
420
[InlineData(false)]
421
[InlineData(true)]
422
public void SavedContactOutsideTheObservedCapturePointKeepsClosingInsteadOfReversing(bool side)
423
{
424
var r=MeasuredCaptureRig(side);var nominal=r.Contact;
425
// The real return stopped at centre spacing 2.226945938. Another observed
426
// return first reported Connected at 1.993233; use distinct geometry so the
427
// test cannot declare capture merely because the old navigation target arrived.
428
r.Capture=()=>Vector3D.Distance(r.PortPosition,r.Dock.Position)<=1.993233;
429
for(int n=0;n<300 && !r.Connected && r.R.StateNow==P.FlightState.DockApproach;n++)
430
{
431
r.Tick(1.0/60,true);
432
if(!r.Connected)
433
{
434
Assert.InRange(r.Flight.CommandSpeed,.5,.501);
435
Assert.InRange(Vector3D.Dot(r.Velocity,r.Dock.Forward),-.501,0);
436
Assert.True(Vector3D.Dot(r.Flight.RouteTarget-r.R.Position,r.Dock.Forward)<-.24);
437
}
438
}
439
Assert.True(r.Connected,r.Gap+" / "+r.Reason);Assert.Equal(1,r.ConnectCalls);
440
Assert.InRange(Vector3D.Dot(r.R.Position-nominal,r.Dock.Forward),-.35,-.23);
441
Assert.Equal(P.FlightState.Servicing,r.R.StateNow);
442
}
443
444
[Fact]
445
public void MissingMagneticCaptureBrakesAtTheBoundAndThenMovesOutward()
446
{
447
var r=MeasuredCaptureRig();r.Capture=()=>false;double previousVelocity=0;
448
for(int n=0;n<300 && r.R.StateNow==P.FlightState.DockApproach;n++)
449
{previousVelocity=Vector3D.Dot(r.Velocity,r.Dock.Forward);r.Tick(1.0/60,true);}
450
Assert.Equal(P.FlightState.DockRetreat,r.R.StateNow);Assert.Equal(P.L.MinerDockCaptureLimit,r.Reason);
451
Assert.True(Vector3D.Dot(r.Velocity,r.Dock.Forward)>previousVelocity,"The limit must brake immediately.");
452
var retreatDepth=r.Gap;var minimum=retreatDepth;Assert.True(retreatDepth<-.35);
453
for(int n=0;n<180;n++){r.Tick(1.0/60,true);minimum=Math.Min(minimum,r.Gap);}
454
Assert.True(r.Gap>retreatDepth+.5,"The craft kept chasing the inward capture target.");
455
Assert.True(minimum>retreatDepth-.2,"Retreat did not arrest the remaining inward motion.");
456
Assert.True(Vector3D.Dot(r.Velocity,r.Dock.Forward)>.25);Assert.Equal(0,r.ConnectCalls);
457
}
458
459
[Fact]
460
public void RealMagneticCaptureStillLocksBeforeTheInwardLimitCheck()
461
{
462
var r=MeasuredCaptureRig();r.R.Position=r.Contact-r.Dock.Forward*.36;r.Magnetized=true;
463
r.Tick();Assert.True(r.Connected);Assert.Equal(P.FlightState.Servicing,r.R.StateNow);
464
Assert.Equal(1,r.ConnectCalls);Assert.DoesNotContain(P.FlightState.DockRetreat,r.States);
465
}
466
467
[Theory]
468
[InlineData(.4,true)]
469
[InlineData(.05,false)]
470
public void FortyFrameMagneticPollingEitherCapturesOrTriggersBoundedRetreat(double phase,bool expectedCapture)
471
{
472
var r=MeasuredCaptureRig();double next=r.R.Program.Now+phase;bool available=false;
473
r.Capture=()=>{
474
if(r.R.Program.Now>=next)
475
{next+=40.0/60;available=Vector3D.Distance(r.PortPosition,r.Dock.Position)<=1.993233;}
476
return available;
477
};
478
for(int n=0;n<300 && !r.Connected && r.R.StateNow==P.FlightState.DockApproach;n++)r.Tick(1.0/60,true);
479
Assert.Equal(expectedCapture,r.Connected);
480
Assert.Equal(expectedCapture?P.FlightState.Servicing:P.FlightState.DockRetreat,r.R.StateNow);
481
Assert.Equal(expectedCapture?1:0,r.ConnectCalls);
482
if(!expectedCapture)Assert.Equal(P.L.MinerDockCaptureLimit,r.Reason);
483
}
484
287
485
[Fact]
288
486
public void StraightApproachKeepsMovingAcrossTheFormerSixtyAndThirtyMetreStops()
289
487
{
@@ -388,16 +586,21 @@ namespace AutoMiningScript.Tests
388
586
}
389
587
390
588
[Fact]
391
public void ObstacleOnMotherStillBlocksTravelToTheEntryPoint()
589
public void ObstacleOnMotherStillBlocksUnverifiedEntryWithDetoursEnabled()
392
590
{
393
591
var r=new DockRig();r.R.Position=r.Contact+Vector3D.Forward*30+Vector3D.Right*10;
394
592
r.AddCamera(Vector3D.Left);
395
r.Hit=p=>new MyDetectedEntityInfo(50,"mother wall",MyDetectedEntityType.LargeGrid,r.R.Position+Vector3D.Normalize(p-r.R.Position)*1.1,
396
MatrixD.Identity,Vector3.Zero,VRage.Game.MyRelationsBetweenPlayerAndBlock.NoOwnership,new BoundingBoxD(new Vector3D(-1),new Vector3D(1)),1);
397
var start=r.R.Position;
398
for(int n=0;n<40 && !r.Flight.Blocked;n++)r.Tick(.05,true);
399
Assert.True(r.Flight.Blocked);Assert.InRange(Vector3D.Distance(start,r.R.Position),0,.1);Assert.False(Read<bool>(r.Flight,"hasDetour"));
400
Assert.Contains(P.L.MinerObstacleBraking,r.Reason);Assert.Equal(0,r.ConnectCalls);
593
var wall=new BoundingBoxD(new Vector3D(7,-1000,-1000),new Vector3D(8,1000,1000));
594
r.Hit=end=>{
595
var ray=end-r.R.Position;double length=ray.Length();var direction=Vector3D.Normalize(ray);
596
var distance=wall.Intersects(new RayD(r.R.Position,direction));
597
if(!distance.HasValue || distance.Value>length)return default(MyDetectedEntityInfo);
598
return new MyDetectedEntityInfo(50,"mother wall",MyDetectedEntityType.LargeGrid,r.R.Position+direction*distance.Value,
599
MatrixD.Identity,Vector3.Zero,VRage.Game.MyRelationsBetweenPlayerAndBlock.NoOwnership,wall,1);
600
};
601
double clearance=double.MaxValue;
602
for(int n=0;n<160;n++){r.Tick(.05,true);clearance=Math.Min(clearance,r.R.Position.X-wall.Max.X-r.R.Hardware.BodyHalfSize.X);}
603
Assert.True(r.Rays>0);Assert.True(clearance>=.2,"Crossed the observed wall: "+clearance);Assert.Equal(0,r.ConnectCalls);
401
604
}
402
605
403
606
[Fact]
@@ -0,0 +1,143 @@
1
using System;
2
using System.Linq;
3
using System.Reflection;
4
using VRageMath;
5
using Xunit;
6
using P=AutoMiningScript.Program;
7
8
namespace AutoMiningScript.Tests
9
{
10
public partial class FleetTrafficTests
11
{
12
static void Body(P.Telemetry t)
13
{
14
t.HasBodyBounds=true;t.BodyMin=new Vector3D(-2.25,-1.75,-2.25);t.BodyMax=new Vector3D(2.25,1.25,2.25);
15
t.Radius=4.63146;t.ThrustMargin=2.6183;t.Forward=Vector3D.Forward;t.Up=Vector3D.Up;
16
}
17
static FleetStateTests.Rig Adjacent(double side=4.90460)
18
{
19
var r=Arrivals(2);var a=r.Miners[0];var b=r.Miners[1];Body(a);Body(b);
20
a.State=P.FlightState.DockApproach;b.State=P.FlightState.Paused;
21
a.Position=Vector3D.Zero;a.Velocity=Vector3D.Forward*4.67;
22
b.Position=Vector3D.Right*side+Vector3D.Forward*14+Vector3D.Up*(-.03369);b.Velocity=Vector3D.Zero;
23
a.ReceivedAt=b.ReceivedAt=r.Program.Now;return r;
24
}
25
static bool HullRisk(FleetStateTests.Rig r)
26
{
27
return (bool)typeof(P.FleetController).GetMethod("NearTraffic",BindingFlags.Instance|BindingFlags.NonPublic,null,
28
new[]{typeof(P.Telemetry),typeof(P.Telemetry)},null).Invoke(r.Fleet,new object[]{r.Miners[0],r.Miners[1]});
29
}
30
31
[Theory]
32
[InlineData(0)] [InlineData(.73)]
33
public void ParallelDockingBesideAStationaryMinerKeepsTheMeasuredFourTenthsMetreHullGap(double rotation)
34
{
35
var r=Adjacent();var frame=MatrixD.CreateFromYawPitchRoll(rotation,rotation*.4,-rotation*.3);
36
foreach(var t in r.Miners)
37
{t.Position=Vector3D.TransformNormal(t.Position,frame);t.Velocity=Vector3D.TransformNormal(t.Velocity,frame);t.Forward=frame.Forward;t.Up=frame.Up;}
38
Assert.False(HullRisk(r));
39
// Verify this fixture reaches the narrow phase, rather than passing because
40
// a distant crossing falls outside the braking/reaction horizon.
41
r.Miners[1].HasBodyBounds=false;Assert.True(HullRisk(r));r.Miners[1].HasBodyBounds=true;
42
foreach(var t in r.Miners)r.Receive("TELEMETRY",t);
43
var response=Traffic(r,0).Body;Assert.False(P.Data.Flag(response,"traffic","Avoid"));Assert.False(P.Data.Flag(response,"traffic","Wait"));
44
}
45
46
[Theory]
47
[InlineData(0,true)] [InlineData(4.65,true)] [InlineData(4.90460,false)]
48
public void SweptHullContactKeepsRealCollisionsAndAnExplicitTenCentimetresPerCraftPadding(double side,bool collision)
49
{var r=Adjacent(side);Assert.Equal(collision,HullRisk(r));}
50
51
[Fact]
52
public void SweptBoxesDetectACrossingThatOnlyOverlapsLaterAndIgnoreRemoteRouteClaims()
53
{
54
var r=Adjacent();var a=r.Miners[0];var b=r.Miners[1];
55
a.Position=Vector3D.Left*8;a.Velocity=Vector3D.Right*4;b.Position=Vector3D.Forward*8;b.Velocity=Vector3D.Backward*4;
56
a.HasRoute=b.HasRoute=false;a.RouteTarget=b.RouteTarget=Vector3D.Up*10000;
57
Assert.True(HullRisk(r));
58
b.Position+=Vector3D.Up*5;Assert.False(HullRisk(r));
59
}
60
61
[Fact]
62
public void RotationOfAnAdjacentHullRetainsCollisionProtectionEvenWithoutClosingTranslation()
63
{
64
var r=Adjacent();r.Miners[1].AngularVelocity=Vector3D.Forward*.12;Assert.True(HullRisk(r));
65
r.Miners[0].Velocity=Vector3D.Zero;r.Miners[1].Position=Vector3D.Right*4.9046;
66
Assert.True(HullRisk(r));
67
r.Miners[1].AngularVelocity=Vector3D.Forward*1e-5;Assert.False(HullRisk(r));
68
}
69
70
[Fact]
71
public void ControllerOrbitCanRotateALongHullIntoACraftOutsideItsInitialReferenceSphere()
72
{
73
var r=Adjacent();var a=r.Miners[0];var b=r.Miners[1];
74
a.BodyMin=new Vector3D(0,-.1,-.1);a.BodyMax=new Vector3D(10,.1,.1);a.Radius=11;
75
a.Position=Vector3D.Zero;a.Velocity=Vector3D.Down*10;a.AngularVelocity=Vector3D.Backward;a.RotationRadius=10;
76
b.Position=Vector3D.Right*19;b.BodyMin=new Vector3D(-.1);b.BodyMax=new Vector3D(.1);b.Radius=1;
77
a.ThrustMargin=b.ThrustMargin=2;
78
// COM=(10,0,0), omega=+Z: at pi seconds the original [0,10]
79
// hull occupies [10,20]. The controller's tangent misses that collision.
80
Assert.True(HullRisk(r));
81
}
82
83
[Fact]
84
public void AsymmetricBoundsUseTheirControllerRelativeCentres()
85
{
86
var r=Adjacent();var a=r.Miners[0];var b=r.Miners[1];
87
a.BodyMin=new Vector3D(-6,-1,-1);a.BodyMax=new Vector3D(0,1,1);
88
b.BodyMin=new Vector3D(0,-1,-1);b.BodyMax=new Vector3D(6,1,1);a.Radius=b.Radius=7;
89
Assert.False(HullRisk(r));
90
}
91
92
[Theory]
93
[InlineData("legacy")] [InlineData("reversed")] [InlineData("zero")] [InlineData("nan")]
94
[InlineData("oversize")] [InlineData("orientation")] [InlineData("angular")] [InlineData("stale")]
95
[InlineData("negativePivot")] [InlineData("nanPivot")]
96
public void UnreliableHullMetadataFallsBackToTheOriginalSphereProtection(string invalid)
97
{
98
var r=Adjacent();var b=r.Miners[1];
99
if(invalid=="legacy")b.HasBodyBounds=false;
100
if(invalid=="reversed")b.BodyMin=b.BodyMax+Vector3D.One;
101
if(invalid=="zero")b.BodyMax=b.BodyMin;
102
if(invalid=="nan")b.BodyMin.X=double.NaN;
103
if(invalid=="oversize")b.BodyMax.X=100;
104
if(invalid=="orientation")b.Up=b.Forward;
105
if(invalid=="angular")b.AngularVelocity.X=double.NaN;
106
if(invalid=="stale")b.ReceivedAt=r.Program.Now-3.01;
107
if(invalid=="negativePivot")b.RotationRadius=-1;
108
if(invalid=="nanPivot")b.RotationRadius=double.NaN;
109
Assert.True(HullRisk(r));
110
}
111
112
[Fact]
113
public void FreshSmallFloatingPointNoiseDoesNotReintroduceTheSphereFalsePositive()
114
{
115
var r=Adjacent();var b=r.Miners[1];b.BodyMin-=new Vector3D(1e-7);b.BodyMax+=new Vector3D(1e-7);
116
b.Forward+=Vector3D.Right*1e-7;b.Up+=Vector3D.Forward*1e-7;b.AngularVelocity=Vector3D.Up*1e-5;
117
b.ReceivedAt=r.Program.Now-.8;Assert.False(HullRisk(r));
118
}
119
120
[Fact]
121
public void OlderMovingTelemetryIncludesTranslationBeyondTheOneSecondCoarsePrediction()
122
{
123
var r=Adjacent();var a=r.Miners[0];var b=r.Miners[1];
124
a.Position=Vector3D.Zero;a.Velocity=Vector3D.Right*10;b.Position=Vector3D.Right*50;
125
a.ReceivedAt=b.ReceivedAt=r.Program.Now-2.9;a.ThrustMargin=b.ThrustMargin=1000;
126
Assert.True(HullRisk(r));
127
b.HasBodyBounds=false;Assert.False(HullRisk(r)); // Legacy path has only its old capped sphere prediction.
128
}
129
130
[Fact]
131
public void HullTelemetryRoundTripsAndMissingOrMalformedOptionalMetadataStaysLegacy()
132
{
133
var r=Adjacent();var a=r.Miners[0];a.AngularVelocity=new Vector3D(.01,.02,.03);var ini=a.ToIni();
134
var copy=P.Telemetry.FromIni(ini);Assert.True(copy.HasBodyBounds);Assert.Equal(a.BodyMin,copy.BodyMin);
135
Assert.Equal(a.BodyMax,copy.BodyMax);Assert.Equal(a.AngularVelocity,copy.AngularVelocity);
136
ini.Delete("telemetry","RotationRadius");Assert.False(P.Telemetry.FromIni(ini).HasBodyBounds);ini.Set("telemetry","RotationRadius",0);
137
ini.Delete("telemetry","AngularVelocity");Assert.False(P.Telemetry.FromIni(ini).HasBodyBounds);
138
ini.Set("telemetry","AngularVelocity","0,0,0");ini.Set("telemetry","BodyMin","NaN,0,0");
139
Assert.False(P.Telemetry.FromIni(ini).HasBodyBounds);
140
a.HasBodyBounds=false;Assert.False(P.Telemetry.FromIni(a.ToIni()).HasBodyBounds);
141
}
142
}
143
}
@@ -9,7 +9,7 @@ using P=AutoMiningScript.Program;
9
9
10
10
namespace AutoMiningScript.Tests
11
11
{
12
public class FleetTrafficTests
12
public partial class FleetTrafficTests
13
13
{
14
14
static FleetStateTests.Rig Arrivals(int count)
15
15
{
@@ -0,0 +1,133 @@
1
using System;
2
using System.Linq;
3
using Sandbox.ModAPI.Ingame;
4
using VRage.Game.ModAPI.Ingame.Utilities;
5
using VRageMath;
6
using Xunit;
7
using P=AutoMiningScript.Program;
8
9
namespace AutoMiningScript.Tests
10
{
11
public class UnreachableSurveyTests
12
{
13
static P.TaskPlanner Planner() => new P.TaskPlanner(TestRig.Program());
14
static void Drain(P.TaskPlanner p) {for(int n=0;n<1024;n++)p.Advance();}
15
static P.TaskPlanner Survey(bool complete=true)
16
{
17
var p=Planner();p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,250);
18
if(complete)Drain(p);return p;
19
}
20
static void Block(P.TaskPlanner p,P.Job j)
21
{
22
p.SetOwner(j,"miner-7");
23
p.Report(j,new P.JobReport {JobId=j.Id,Outcome="Blocked",EntryUnreachable=true,Message="machine flag, not a localized reason"});
24
}
25
26
[Fact]
27
public void FlagRoundTripsAndLegacyMessagesAndJobsDefaultToFalse()
28
{
29
var p=Survey();var job=p.Jobs[0];job.EntryUnreachable=true;
30
var body=job.ToIni();Assert.True(P.Job.FromIni(body).EntryUnreachable);
31
body.Delete("job","EntryUnreachable");Assert.False(P.Job.FromIni(body).EntryUnreachable);
32
var report=new P.JobReport {JobId=job.Id,Outcome="Blocked",EntryUnreachable=true};
33
body=report.ToIni();Assert.True(P.JobReport.FromIni(body).EntryUnreachable);
34
body.Delete("result","EntryUnreachable");Assert.False(P.JobReport.FromIni(body).EntryUnreachable);
35
}
36
37
[Fact]
38
public void UnreachableEntrySkipsItsFortyTwoRaysWithoutClaimingAnyScan()
39
{
40
var p=Survey();var source=p.Jobs[0];Block(p,source);
41
// The dispatcher cannot select a sibling before the bounded summary runs.
42
Assert.Equal(-1,p.JobPriority(p.Jobs[1]));
43
Drain(p);Assert.Equal(252,p.PendingCount);Assert.Empty(p.OwnedJobs);
44
Assert.All(p.Jobs.Where(j=>j.Entry==source.Entry),j=>{Assert.Equal("Blocked",j.Outcome);Assert.Null(j.Scan);});
45
Assert.All(p.Jobs.Where(j=>j.Entry!=source.Entry),j=>Assert.False(P.TaskPlanner.Finished(j)));
46
p.Report(source,new P.JobReport {Outcome="Blocked",EntryUnreachable=true});Drain(p);
47
Assert.Equal(252,p.PendingCount);
48
}
49
50
[Fact]
51
public void OrdinaryRayOrCameraFailureDoesNotBlockOtherDirections()
52
{
53
var p=Survey();var source=p.Jobs[0];
54
p.Report(source,new P.JobReport {Outcome="Blocked",Message=P.L.MinerSurveyEntryStalled});Drain(p);
55
Assert.Equal(293,p.PendingCount);Assert.False(source.EntryUnreachable);
56
Assert.Equal(0,p.JobPriority(p.Jobs[1]));Assert.False(P.TaskPlanner.Finished(p.Jobs[1]));
57
}
58
59
[Fact]
60
public void OtherOwnerCanStillDiscoverAndDrillAtTheBlockedObservation()
61
{
62
var p=Survey();var source=p.Jobs[0];var active=p.Jobs[1];p.SetOwner(active,"miner-2");
63
Block(p,source);Drain(p);Assert.Equal(active,p.OwnedJobs["miner-2"]);
64
Assert.False(P.TaskPlanner.Finished(active));Assert.Equal(253,p.PendingCount);
65
// A real surface report remains useful even if another miner could not approach.
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
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);
71
}
72
73
[Fact]
74
public void SameCoordinatesInAnotherRegionAndNearbyEntriesRemainAvailable()
75
{
76
var p=Survey();var source=p.Jobs[0];var nearby=p.Jobs[1];nearby.Entry+=Vector3D.Right*.001;
77
p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,250);Drain(p);
78
p.Manual(source.Entry,Vector3D.Forward,Vector3D.Up,6,6,10);Drain(p);
79
Block(p,source);Drain(p);
80
Assert.False(P.TaskPlanner.Finished(nearby));Assert.Equal(0,p.JobPriority(nearby));
81
Assert.All(p.Jobs.Where(j=>j.RegionId!=source.RegionId),j=>Assert.False(P.TaskPlanner.Finished(j)));
82
}
83
84
[Fact]
85
public void EarlyBlockSurvivesReloadAndAlsoSkipsRaysGeneratedLater()
86
{
87
var p=Survey(false);Assert.InRange(p.Jobs.Count,1,8);var source=p.Jobs[0];Block(p,source);
88
var save=new MyIni();p.Save(save);var restored=Planner();restored.Load(save);
89
Drain(restored);Assert.Equal(294,restored.Jobs.Count);Assert.Equal(252,restored.PendingCount);
90
Assert.True(restored.Find(source.Id).EntryUnreachable);
91
Assert.All(restored.Jobs.Where(j=>j.Entry==source.Entry),j=>{Assert.Equal("Blocked",j.Outcome);Assert.Null(j.Scan);});
92
}
93
94
[Fact]
95
public void CheckpointAlreadyInProgressRecordsSkippedSiblingsAndSourceFlag()
96
{
97
var p=Survey();var save=new MyIni();Assert.False(p.Checkpoint(save));
98
Block(p,p.Jobs[0]);Drain(p);bool complete=false;
99
for(int n=0;n<1024&&!complete;n++)complete=p.Checkpoint(save);
100
Assert.True(complete);var restored=Planner();restored.Load(save);Drain(restored);
101
Assert.Equal(252,restored.PendingCount);Assert.True(restored.Jobs[0].EntryUnreachable);
102
Assert.All(restored.Jobs.Take(42),j=>Assert.Equal("Blocked",j.Outcome));
103
}
104
105
[Fact]
106
public void WholeBlockedSearchIsIncompleteAndNeverCreatesCoverage()
107
{
108
var p=Survey();foreach(var group in p.Jobs.GroupBy(j=>j.Entry))Block(p,group.First());
109
Drain(p);Assert.Equal(0,p.PendingCount);Assert.Equal("Sampling incomplete: blocked observations",p.Regions[0].Status);
110
Assert.All(p.Jobs,j=>{Assert.Equal("Blocked",j.Outcome);Assert.Null(j.Scan);});
111
p.ClearTasks();p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,250);Drain(p);
112
Assert.Equal(294,p.PendingCount);Assert.All(p.Jobs,j=>Assert.Equal(0,p.JobPriority(j)));
113
}
114
115
[Fact]
116
public void OccupiedSiblingIsSkippedOnlyAfterItsOwnerReleasesIt()
117
{
118
var p=Survey();var active=p.Jobs[1];p.SetOwner(active,"miner-2");Block(p,p.Jobs[0]);Drain(p);
119
Assert.Equal(253,p.PendingCount);p.SetOwner(active,"");
120
Assert.Equal(-1,p.JobPriority(active));Assert.Equal(252,p.PendingCount);Assert.Empty(p.OwnedJobs);
121
}
122
123
[Fact]
124
public void SkippingYieldsToBudgetButAssignmentStillCannotTakeBlockedEntry()
125
{
126
var program=TestRig.Program();var runtime=new Stub<IMyGridProgramRuntimeInfo>().Set("MaxInstructionCount",50000).Set("CurrentInstructionCount",0);
127
TestRig.SetBase(program,"Runtime",runtime.Value);var p=new P.TaskPlanner(program);
128
p.Survey(Vector3D.Zero,Vector3D.Zero,Vector3D.Forward,Vector3D.Up,250);Drain(p);Block(p,p.Jobs[0]);
129
runtime.Set("CurrentInstructionCount",30000);p.Advance();Assert.Equal(293,p.PendingCount);
130
runtime.Set("CurrentInstructionCount",0);Assert.Equal(-1,p.JobPriority(p.Jobs[1]));Drain(p);Assert.Equal(252,p.PendingCount);
131
}
132
}
133
}
@@ -159,6 +159,7 @@ namespace AutoMiningScript
159
159
public class Job
160
160
{
161
161
public ScanRecord Scan;
162
public bool EntryUnreachable;
162
163
public string Id = "", RegionId = "", TargetOre = "", ConfirmedOres = "", Owner = "", Outcome = "", LastOwner = "";
163
164
public JobKind Kind;
164
165
public Vector3D Entry, Direction = Data.Forward, Up = Data.Up;
@@ -173,6 +174,7 @@ namespace AutoMiningScript
173
174
{
174
175
if (Scan != null) Scan.Write(i, s);
175
176
i.Set(s, "ConfirmedOres", ConfirmedOres);
177
i.Set(s, "EntryUnreachable", EntryUnreachable);
176
178
i.Set(s, "LastOwner", LastOwner);
177
179
i.Set(s, "Id", Id); i.Set(s, "RegionId", RegionId); i.Set(s, "Kind", (int)Kind); Data.PutVector(i,s, "Entry",Entry); Data.PutVector(i,s, "Direction",Direction); Data.PutVector(i,s, "Up",Up);
178
180
i.Set(s, "Depth", Depth); i.Set(s, "Radius", Radius); i.Set(s, "Progress", Progress); i.Set(s, "HoleIndex", HoleIndex); i.Set(s, "EntityId", EntityId); i.Set(s, "Priority", Priority); i.Set(s, "TargetOre", TargetOre); i.Set(s, "Owner", Owner); i.Set(s, "Outcome", Outcome);
@@ -183,6 +185,7 @@ namespace AutoMiningScript
183
185
var j = new Job(); j.Id = Data.Text(i,s, "Id"); j.RegionId = Data.Text(i,s, "RegionId");
184
186
j.Scan = ScanRecord.Read(i, s);
185
187
j.ConfirmedOres = Data.Text(i, s, "ConfirmedOres");
188
j.EntryUnreachable = Data.Flag(i, s, "EntryUnreachable");
186
189
j.LastOwner = Data.Text(i,s, "LastOwner");
187
190
int kind = (int)Data.ReadLong(i, s, "Kind"); if (kind < 0 || kind > 2) throw Data.Invalid(L.CommonJobKindInvalid); j.Kind = (JobKind)kind;
188
191
j.Entry = Data.ReadVector(i, s, "Entry"); j.Direction = Data.ReadVector(i, s, "Direction", Data.Forward); j.Up = Data.ReadVector(i, s, "Up", Data.Up);
@@ -201,6 +204,9 @@ namespace AutoMiningScript
201
204
public bool HasRoute;
202
205
public FlightState State;
203
206
public Vector3D Position, Velocity, Forward = Data.Forward, Up = Data.Up;
207
public bool HasBodyBounds;
208
public Vector3D BodyMin,BodyMax,AngularVelocity;
209
public double RotationRadius;
204
210
public double Battery, Hydrogen = 1, Cargo, OreAmount, StoredMWh, CapacityMWh, NetMW, EnduranceSeconds, Depth, TargetDepth, DistanceHome, ThrustMargin, Gravity, Radius = 5, ReceivedAt;
205
211
public double BrakeResponse = .625;
206
212
public bool Connected;
@@ -210,6 +216,8 @@ namespace AutoMiningScript
210
216
{
211
217
var i = new MyIni(); var s = "telemetry";
212
218
i.Set(s, "NextAfter", NextAfter); Data.PutVector(i,s, "RouteTarget",RouteTarget); i.Set(s, "HasRoute", HasRoute);
219
i.Set(s,"HasBodyBounds",HasBodyBounds);
220
if(HasBodyBounds){Data.PutVector(i,s,"BodyMin",BodyMin);Data.PutVector(i,s,"BodyMax",BodyMax);Data.PutVector(i,s,"AngularVelocity",AngularVelocity);i.Set(s,"RotationRadius",RotationRadius);}
213
221
i.Set(s, "CancelledThrough", CancelledThrough);
214
222
i.Set(s, "WaitingSlot", WaitingSlot);
215
223
i.Set(s, "ServiceFlags", ServiceFlags);
@@ -222,6 +230,8 @@ namespace AutoMiningScript
222
230
{
223
231
var s = "telemetry"; var t = new Telemetry(); t.Id = Data.Text(i,s, "Id"); int state = (int)Data.ReadLong(i, s, "State"); if (state < 0 || state > (int)FlightState.Fault) throw Data.Invalid(L.CommonFlightStateInvalid); t.State = (FlightState)state;
224
232
t.NextAfter = Data.Text(i,s, "NextAfter"); t.RouteTarget = Data.ReadVector(i, s, "RouteTarget", t.Position); t.HasRoute = Data.Flag(i,s, "HasRoute");
233
t.HasBodyBounds=Data.Flag(i,s,"HasBodyBounds") && Data.TryVector(Data.Text(i,s,"BodyMin"),out t.BodyMin) &&
234
Data.TryVector(Data.Text(i,s,"BodyMax"),out t.BodyMax) && Data.TryVector(Data.Text(i,s,"AngularVelocity"),out t.AngularVelocity) && Data.TryNumber(Data.Text(i,s,"RotationRadius"),out t.RotationRadius);
225
235
t.CancelledThrough = Data.ReadIndex(i, s, "CancelledThrough", 1000000000);
226
236
t.WaitingSlot = (int)Data.ReadIndex(i, s, "WaitingSlot", 1000000);
227
237
t.ServiceFlags = (int)Data.ReadIndex(i, s, "ServiceFlags", 3);
@@ -302,12 +312,13 @@ namespace AutoMiningScript
302
312
public class JobReport
303
313
{
304
314
public ScanRecord Scan;
315
public bool EntryUnreachable;
305
316
public string JobId = "", Outcome = "", Ore = "", Message = "";
306
317
public double Progress;
307
318
public Vector3D Position, Direction = Data.Forward, Up = Data.Up;
308
319
public long EntityId;
309
public MyIni ToIni() { var i = new MyIni(); var s = "result"; if(Scan!=null)Scan.Write(i,s); i.Set(s, "JobId", JobId); i.Set(s, "Outcome", Outcome); i.Set(s, "Ore", Ore); i.Set(s, "Message", Message); i.Set(s, "Progress", Progress); Data.PutVector(i,s, "Position",Position); Data.PutVector(i,s, "Direction",Direction); Data.PutVector(i,s, "Up",Up); i.Set(s, "EntityId", EntityId); return i; }
310
public static JobReport FromIni(MyIni i) { var s = "result"; return new JobReport { Scan=ScanRecord.Read(i,s), JobId = Data.Text(i,s, "JobId"), Outcome = Data.Text(i,s, "Outcome"), Ore = Data.Text(i,s, "Ore"), Message = Data.Text(i,s, "Message"), Progress = Data.ReadNumber(i, s, "Progress"), Position = Data.ReadVector(i, s, "Position"), Direction = Data.ReadVector(i, s, "Direction", Data.Forward), Up = Data.ReadVector(i, s, "Up", Data.Up), EntityId = Data.ReadLong(i, s, "EntityId") }; }
320
public MyIni ToIni() { var i = new MyIni(); var s = "result"; if(Scan!=null)Scan.Write(i,s); i.Set(s,"EntryUnreachable",EntryUnreachable); i.Set(s, "JobId", JobId); i.Set(s, "Outcome", Outcome); i.Set(s, "Ore", Ore); i.Set(s, "Message", Message); i.Set(s, "Progress", Progress); Data.PutVector(i,s, "Position",Position); Data.PutVector(i,s, "Direction",Direction); Data.PutVector(i,s, "Up",Up); i.Set(s, "EntityId", EntityId); return i; }
321
public static JobReport FromIni(MyIni i) { var s = "result"; return new JobReport { Scan=ScanRecord.Read(i,s), EntryUnreachable=Data.Flag(i,s,"EntryUnreachable"), JobId = Data.Text(i,s, "JobId"), Outcome = Data.Text(i,s, "Outcome"), Ore = Data.Text(i,s, "Ore"), Message = Data.Text(i,s, "Message"), Progress = Data.ReadNumber(i, s, "Progress"), Position = Data.ReadVector(i, s, "Position"), Direction = Data.ReadVector(i, s, "Direction", Data.Forward), Up = Data.ReadVector(i, s, "Up", Data.Up), EntityId = Data.ReadLong(i, s, "EntityId") }; }
311
322
}
312
323
}
313
324
}
@@ -12,6 +12,49 @@ namespace AutoMiningScript
12
12
{
13
13
public class FleetController : RoleLogic
14
14
{
15
// Reuse exact wire/save keys to keep the Fleet-only PB export below its size limit.
16
static readonly string
17
FleetSection = "fleet",
18
DepartRequestKind = "DEPART_REQUEST",
19
RemovalSection = "remove",
20
LaneClearanceKey = "LaneClearance",
21
AddressKey = "Address",
22
MarkCommand = "mark",
23
MarkerReceiptKey = "MarkerReceipt",
24
NextWaitingSlotKey = "NextWaitingSlot",
25
ClearanceCountKey = "ClearanceCount",
26
ClearanceDataKey = "ClearanceData",
27
StorageSection = "storage",
28
DockRequestKind = "DOCK_REQUEST",
29
TokenSerialKey = "TokenSerial",
30
FleetConfigSection = "Fleet",
31
MarkerCountKey = "MarkerCount",
32
ResumeCommand = "resume",
33
AuxiliaryRecordsKey = "AuxRecords",
34
CommandSection = "command",
35
CommandKind = "COMMAND",
36
LaneOwnerKey = "LaneOwner",
37
ClearanceOwnerKey = "ClearanceOwner",
38
DockConfigSection = "Dock",
39
ReturnCommand = "return",
40
DiagnoseCommand = "diagnose",
41
DepartGrantKind = "DEPART_GRANT",
42
ClearanceJobKey = "ClearanceJob",
43
MinerKey = "Miner",
44
TrafficSection = "traffic",
45
ReturnAllCommand = "return-all",
46
MinerCountKey = "MinerCount",
47
ProtectedOwnerAddressKey = "ProtectedOwnerAddress",
48
DockGrantKind = "DOCK_GRANT",
49
LaneApproachDistanceKey = "LaneApproachDistance",
50
RecordSection = "record",
51
DockGroupKey = "DockGroup",
52
ProtectedOwnerCountKey = "ProtectedOwnerCount",
53
TextKey = "Text",
54
DefaultDockGroup = "AMS Docks",
55
CancellationCountKey = "CancellationCount",
56
CancellationOwnerKey = "CancellationOwner",
57
OperatorStopCountKey = "OperatorStopCount";
15
58
class Request { public string Owner, Kind, Problem=""; public double At; }
16
59
class PortYield { public string Peer; public Vector3D Point, Velocity, PeerVelocity; }
17
60
readonly Random trafficRandom=new Random();
@@ -80,13 +123,13 @@ namespace AutoMiningScript
80
123
foreach (var t in miners.Values) if (P.Now - t.ReceivedAt <= OfflineSeconds) online++; else unknown++;
81
124
return L.F(L.FleetDiagnostics, running ? L.FleetRunning : L.FleetPaused, online, miners.Count, unknown, docks.Count, requests.Count,
82
125
LaneSummary(), pending, planner.Jobs.Count) +
83
(planner.Generating ? L.F(L.FleetPlanningProgress, planner.PendingGeneration) + "\n" : "") + (pendingCancellation.Count > 0 ? L.F(L.FleetAwaitCancellation, pendingCancellation.Count) + "\n" : "") + (controller == null ? L.FleetControllerMissing + "\n" : "") + (docks.Count==0 ? MissingGroup("DockGroup","AMS Docks",L.FleetBerthsMissing)+"\n" : "") + (cargo.Count == 0 ? MissingGroup("CargoGroup","AMS Cargo",L.FleetCargoMissing) + "\n" : "") + problem;
126
(planner.Generating ? L.F(L.FleetPlanningProgress, planner.PendingGeneration) + "\n" : "") + (pendingCancellation.Count > 0 ? L.F(L.FleetAwaitCancellation, pendingCancellation.Count) + "\n" : "") + (controller == null ? L.FleetControllerMissing + "\n" : "") + (docks.Count==0 ? MissingGroup(DockGroupKey,DefaultDockGroup,L.FleetBerthsMissing)+"\n" : "") + (cargo.Count == 0 ? MissingGroup("CargoGroup","AMS Cargo",L.FleetCargoMissing) + "\n" : "") + problem;
84
127
}
85
128
}
86
129
void Scan()
87
130
{
88
131
docks.Clear(); cargo.Clear(); connectedBlocks.Clear();
89
var group = FindGroup("DockGroup", "AMS Docks");
132
var group = FindGroup(DockGroupKey, DefaultDockGroup);
90
133
if (group != null) group.GetBlocksOfType(docks, b => b.CubeGrid == P.Me.CubeGrid);
91
134
docks.Sort((a,b)=>a.EntityId.CompareTo(b.EntityId));
92
135
var order=new List<IMyShipConnector>(docks);
@@ -96,7 +139,7 @@ namespace AutoMiningScript
96
139
if (cg != null) cg.GetBlocks(cargo, b => b.CubeGrid == P.Me.CubeGrid && b.HasInventory);
97
140
var controls = Data.CreateList<IMyShipController>();
98
141
P.GridTerminalSystem.GetBlocksOfType(controls, b => b.CubeGrid == P.Me.CubeGrid && b.IsFunctional);
99
var name = P.Config.Text("Fleet", "Controller", ""); controller = null;
142
var name = P.Config.Text(FleetConfigSection, "Controller", ""); controller = null;
100
143
foreach (var c in controls) if (name.Length == 0 ? controller == null || c.IsMainCockpit : c.CustomName == name) controller = c;
101
144
}
102
145
// Stable shuffle: connector roll, enumeration order and reloads do not move entry points.
@@ -105,10 +148,10 @@ namespace AutoMiningScript
105
148
unchecked {var n=(ulong)id;n=(n^(n>>30))*0xbf58476d1ce4e5b9UL;n=(n^(n>>27))*0x94d049bb133111ebUL;return n^(n>>31);}
106
149
}
107
150
double EntryDistance(long connector)
108
{double offset;approachOffsets.TryGetValue(connector,out offset);return P.Config.Number("Dock","ApproachDistance",30,10,300)+offset;}
151
{double offset;approachOffsets.TryGetValue(connector,out offset);return P.Config.Number(DockConfigSection,"ApproachDistance",30,10,300)+offset;}
109
152
IMyBlockGroup FindGroup(string key,string standard)
110
153
{
111
var name=P.Config.Text("Fleet",key,standard);
154
var name=P.Config.Text(FleetConfigSection,key,standard);
112
155
var group=P.GridTerminalSystem.GetBlockGroupWithName(name);
113
156
if(group!=null)return group;
114
157
var local=Settings.DefaultName(standard);
@@ -118,7 +161,7 @@ namespace AutoMiningScript
118
161
return null;
119
162
}
120
163
string MissingGroup(string key,string standard,string message)
121
=> message+": "+P.Config.Text("Fleet",key,standard);
164
=> message+": "+P.Config.Text(FleetConfigSection,key,standard);
122
165
public override void Tick(double dt)
123
166
{
124
167
P.Bus.AllowRebind=CanRebind;
@@ -143,7 +186,7 @@ namespace AutoMiningScript
143
186
if (controller != null) { var motion = controller.GetShipVelocities(); velocity = motion.LinearVelocity; angular = motion.AngularVelocity; }
144
187
var basis = controller == null ? P.Me.WorldMatrix : controller.WorldMatrix;
145
188
baseFrame = new DockFrame { BaseId = P.Config.Id, Center = center, Position = center, Forward = basis.Forward, Up = basis.Up, Velocity = velocity, AngularVelocity = angular, ReceivedAt = P.Now };
146
baseFrame.Problem=docks.Count==0?MissingGroup("DockGroup","AMS Docks",L.FleetBerthsMissing):controller==null?L.FleetControllerMissing:"";
189
baseFrame.Problem=docks.Count==0?MissingGroup(DockGroupKey,DefaultDockGroup,L.FleetBerthsMissing):controller==null?L.FleetControllerMissing:"";
147
190
frames.Clear();
148
191
foreach (var dock in docks)
149
192
{
@@ -174,12 +217,12 @@ namespace AutoMiningScript
174
217
DockFrame frame; if (!frames.TryGetValue(connector, out frame)) return null;
175
218
var copy = new DockFrame { BaseId = frame.BaseId, ConnectorId = frame.ConnectorId, GridId = frame.GridId, Position = frame.Position, Forward = frame.Forward, Up = frame.Up, Center = frame.Center, Velocity = frame.Velocity, AngularVelocity = frame.AngularVelocity, Acceleration = frame.Acceleration, ReceivedAt = P.Now, Connected = frame.Connected, Owner = owner, Token = token };
176
219
Telemetry waiting; int slot = miners.TryGetValue(owner, out waiting) && waiting.WaitingSlot >= 0 ? waiting.WaitingSlot : 0;
177
double spacing = P.Config.Number("Dock", "WaitingSpacing", 20, 10, 100), distance = P.Config.Number("Dock", "WaitingDistance", 80, 40, 500);
220
double spacing = P.Config.Number(DockConfigSection, "WaitingSpacing", 20, 10, 100), distance = P.Config.Number(DockConfigSection, "WaitingDistance", 80, 40, 500);
178
221
Lane lane;bool allocated=lanes.TryGetValue(owner,out lane) && lane.Dock==connector;
179
222
copy.ApproachDistance=allocated?lane.ApproachDistance:EntryDistance(connector);
180
223
copy.WaitingLocal = new Vector3D(0,0,-Data.Max(distance,copy.ApproachDistance+spacing)-(slot/Data.Max(1,docks.Count))*spacing);
181
224
Request request;if(token.Length==0 && requests.TryGetValue(owner,out request))copy.Problem=request.Problem;
182
copy.LaneClearance=allocated?lane.Clearance:P.Config.Number("Dock","LaneClearance",60,20,300);return copy;
225
copy.LaneClearance=allocated?lane.Clearance:P.Config.Number(DockConfigSection,LaneClearanceKey,60,20,300);return copy;
183
226
}
184
227
void SendPoses()
185
228
{
@@ -213,7 +256,7 @@ namespace AutoMiningScript
213
256
{int order;if(!trafficOrder.TryGetValue(id,out order))trafficOrder[id]=order=trafficRandom.Next();return order;}
214
257
void TrafficMessage(Telemetry t,bool wait,bool avoid,Vector3D point,string peer)
215
258
{
216
var body=new MyIni();body.Set("traffic","Wait",wait);body.Set("traffic","Avoid",avoid);Data.PutVector(body,"traffic","Point",point);body.Set("traffic","Peer",peer);
259
var body=new MyIni();body.Set(TrafficSection,"Wait",wait);body.Set(TrafficSection,"Avoid",avoid);Data.PutVector(body,TrafficSection,"Point",point);body.Set(TrafficSection,"Peer",peer);
217
260
P.Bus.Send("TRAFFIC",t.Id,body,false,t.Address);
218
261
}
219
262
bool NearTraffic(Telemetry t,Telemetry other)
@@ -223,17 +266,67 @@ namespace AutoMiningScript
223
266
if(other.Id==t.Id || ConnectedDock(other)!=null)return false;
224
267
var offset=other.Position+other.Velocity*Data.Clamp(P.Now-other.ReceivedAt,0,1)-t.Position-t.Velocity*Data.Clamp(P.Now-t.ReceivedAt,0,1);
225
268
var relative=otherVelocity-velocity;double speed=Data.Length(relative);
226
if(speed<.1 || Data.Dot(offset,relative)>=0)return false;
227
double when=-Data.Dot(offset,relative)/(speed*speed),gap=Data.Max(1,t.Radius)+Data.Max(1,other.Radius)+1;
228
double miss=Data.LengthSquared(offset+relative*when);if(miss>gap*gap)return false;
229
// Time to first envelope contact, not a clamped closest-point guess.
230
double contact=when-Math.Sqrt(Data.Max(0,gap*gap-miss))/speed;
231
269
double margin=t.ThrustMargin>0 && other.ThrustMargin>0?Data.Min(t.ThrustMargin,other.ThrustMargin):Data.Max(t.ThrustMargin,other.ThrustMargin);
232
270
// Proportional braking has distance v/gain even with unlimited
233
271
// thrust. Add actuator saturation and measured message latency;
234
272
// only relative velocity contributes, including on a moving base.
235
return contact<=.5+Data.Clamp(P.Now-Data.Min(t.ReceivedAt,other.ReceivedAt),0,1)+
273
double horizon=.5+Data.Clamp(P.Now-Data.Min(t.ReceivedAt,other.ReceivedAt),0,1)+
236
274
Data.Max(t.BrakeResponse,other.BrakeResponse)+(margin>0?speed/(2*Data.Max(.05,margin*.65)):0);
275
double gap=Data.Max(1,t.Radius)+Data.Max(1,other.Radius)+1;bool risk=false;
276
if(speed>=.1 && Data.Dot(offset,relative)<0)
277
{
278
double when=-Data.Dot(offset,relative)/(speed*speed),miss=Data.LengthSquared(offset+relative*when);
279
if(miss<=gap*gap)risk=when-Math.Sqrt(Data.Max(0,gap*gap-miss))/speed<=horizon;
280
}
281
// Old or invalid geometry keeps the original sphere protection. Reliable
282
// bounds refine it; neither a parked peer nor a planned route is an exemption.
283
if(!TrafficHull(t) || !TrafficHull(other))return risk;
284
if(!risk)
285
{
286
if(Data.LengthSquared(t.AngularVelocity)+Data.LengthSquared(other.AngularVelocity)==0 && P.Now-Data.Min(t.ReceivedAt,other.ReceivedAt)<=1)return false;
287
gap+=HullPadding(t,horizon)+HullPadding(other,horizon);
288
double closest=speed>.001?Data.Clamp(-Data.Dot(offset,relative)/(speed*speed),0,horizon):0;
289
if(Data.LengthSquared(offset+relative*closest)>gap*gap)return false;
290
}
291
return HullContact(t,other,offset,relative,horizon);
292
}
293
bool TrafficHull(Telemetry t)
294
{
295
var size=t.BodyMax-t.BodyMin;
296
return t.HasBodyBounds && P.Now-t.ReceivedAt>=0 && P.Now-t.ReceivedAt<=3 &&
297
Data.Finite(t.BodyMin) && Data.Finite(t.BodyMax) && Data.Finite(t.AngularVelocity) && Data.Finite(t.RotationRadius) && t.RotationRadius>=0 && Data.Finite(t.Forward) && Data.Finite(t.Up) &&
298
size.X>.05 && size.Y>.05 && size.Z>.05 && Data.Length(Vector3D.Max(Vector3D.Abs(t.BodyMin),Vector3D.Abs(t.BodyMax)))<=t.Radius+.01 &&
299
Math.Abs(Data.LengthSquared(t.Forward)-1)<.02 && Math.Abs(Data.LengthSquared(t.Up)-1)<.02 && Math.Abs(Data.Dot(t.Forward,t.Up))<.01;
300
}
301
double HullPadding(Telemetry t,double horizon)
302
{
303
double age=Data.Max(0,P.Now-t.ReceivedAt),angle=Data.Length(t.AngularVelocity)*(horizon+age);
304
// 10 cm per hull, plus a bound on every corner's measured rotation
305
// about the controller. Old translation beyond the 1 s extrapolation
306
// is also uncertainty, rather than evidence that an adjacent lane is clear.
307
// The controller itself orbits the centre of mass: bound the difference
308
// between that arc and extrapolating its measured tangent velocity.
309
return .1+t.Radius*Data.Min(2,angle)+t.RotationRadius*Data.Min(angle*angle*.5,angle+Data.Min(2,angle))+Data.Length(t.Velocity)*Data.Max(0,age-1);
310
}
311
bool HullContact(Telemetry a,Telemetry b,Vector3D offset,Vector3D relative,double horizon)
312
{
313
var am=Data.Frame(Data.Zero,a.Forward,a.Up);var bm=Data.Frame(Data.Zero,b.Forward,b.Up);
314
var axesA=new[]{am.Right,am.Up,am.Backward};var axesB=new[]{bm.Right,bm.Up,bm.Backward};
315
var halfA=(a.BodyMax-a.BodyMin)*.5;var halfB=(b.BodyMax-b.BodyMin)*.5;
316
offset+=Data.TransformNormal((b.BodyMin+b.BodyMax)*.5,bm)-Data.TransformNormal((a.BodyMin+a.BodyMax)*.5,am);
317
double enter=0,exit=horizon,padding=HullPadding(a,horizon)+HullPadding(b,horizon);
318
for(int i=0;i<15;i++)
319
{
320
var axis=i<3?axesA[i]:i<6?axesB[i-3]:Data.Cross(axesA[(i-6)/3],axesB[(i-6)%3]);
321
double length=Data.Length(axis);if(length<1e-6)continue;axis/=length;
322
double reach=padding;
323
for(int j=0;j<3;j++)reach+=Math.Abs(Data.Dot(axis,axesA[j]))*halfA.GetDim(j)+Math.Abs(Data.Dot(axis,axesB[j]))*halfB.GetDim(j);
324
double at=Data.Dot(offset,axis),rate=Data.Dot(relative,axis);
325
if(Math.Abs(rate)<1e-8){if(Math.Abs(at)>reach)return false;continue;}
326
double first=(-reach-at)/rate,last=(reach-at)/rate;
327
enter=Data.Max(enter,Data.Min(first,last));exit=Data.Min(exit,Data.Max(first,last));if(enter>exit)return false;
328
}
329
return true;
237
330
}
238
331
Vector3D YieldSide(Telemetry loser,Telemetry winner)
239
332
{
@@ -304,7 +397,7 @@ namespace AutoMiningScript
304
397
}
305
398
bool CanRebind(Packet packet)
306
399
{
307
if(!packet.IsRegistration || packet.Kind=="DEPART_REQUEST")return false;
400
if(!packet.IsRegistration || packet.Kind==DepartRequestKind)return false;
308
401
Telemetry old;if(!miners.TryGetValue(packet.From,out old) || protectedOwners.ContainsKey(packet.From))return false;
309
402
var current=Telemetry.FromIni(packet.Body);
310
403
if(current.Id!=packet.From || current.ConnectorId==0)return false;
@@ -327,7 +420,7 @@ namespace AutoMiningScript
327
420
P.Bus.CancelPending(null,t.Id);minerSessions.Remove(t.Id);poseSent.Remove(t.Id);trafficSent.Remove(t.Id);portYields.Remove(t.Id);
328
421
Lane prior;if(lanes.TryGetValue(t.Id,out prior)){prior.Token="";prior.Renew=true;prior.Consent=false;}
329
422
}
330
if (!miners.ContainsKey(t.Id) && miners.Count >= P.Config.Integer("Fleet", "MaxMiners", 16, 1, 16)) { problem = L.FleetRegistrationLimit; return false; }
423
if (!miners.ContainsKey(t.Id) && miners.Count >= P.Config.Integer(FleetConfigSection, "MaxMiners", 16, 1, 16)) { problem = L.FleetRegistrationLimit; return false; }
331
424
// The base owns slot allocation. Client-provided values must never move
332
425
// another ship's waiting point or recycle an abandoned flight corridor.
333
426
if (old != null && old.WaitingSlot >= 0) t.WaitingSlot = old.WaitingSlot;
@@ -364,13 +457,13 @@ namespace AutoMiningScript
364
457
Telemetry t; if (!miners.TryGetValue(packet.From, out t) || t.Address != packet.Source) return;
365
458
if (packet.Kind == "DIAGNOSTICS")
366
459
{
367
var text = Data.Text(packet.Body,"diagnostics", "Text");
460
var text = Data.Text(packet.Body,"diagnostics", TextKey);
368
461
problem = t.Id + "\n" + (text.Length > 2400 ? text.Substring(0, 2400) : text); P.Log(problem); return;
369
462
}
370
463
if(requesting)
371
464
{
372
465
UpdateFrames(baseFrame==null?.1:Data.Max(.001,P.Now-baseFrame.ReceivedAt));poseClock=0;
373
if (packet.Kind == "DEPART_REQUEST" && (!running || operatorStops.Contains(t.Id) || pendingCancellation.ContainsKey(t.Id))) return;
466
if (packet.Kind == DepartRequestKind && (!running || operatorStops.Contains(t.Id) || pendingCancellation.ContainsKey(t.Id))) return;
374
467
Request request; if (!requests.TryGetValue(packet.From, out request)) requests[packet.From] = new Request { Owner = packet.From, Kind = packet.Kind, At = P.Now };
375
468
else request.Kind = packet.Kind;
376
469
Lane lane;
@@ -378,7 +471,7 @@ namespace AutoMiningScript
378
471
{
379
472
// A cancelled departure may need the very same reserved corridor
380
473
// to return. Re-issue it to its current owner as an inbound lease.
381
if (packet.Kind != lane.Kind) { P.Bus.CancelPending(lane.Kind=="DEPART_REQUEST"?"DEPART_GRANT":"DOCK_GRANT",t.Id);lane.Renew = true; lane.Token = ""; }
474
if (packet.Kind != lane.Kind) { P.Bus.CancelPending(lane.Kind==DepartRequestKind?DepartGrantKind:DockGrantKind,t.Id);lane.Renew = true; lane.Token = ""; }
382
475
if (lane.Renew) { lane.Consent = true; lane.Kind = packet.Kind; }
383
476
else SendGrant(t);
384
477
}
@@ -388,7 +481,7 @@ namespace AutoMiningScript
388
481
{
389
482
Lane lane;
390
483
if (!lanes.TryGetValue(packet.From,out lane) || Data.Text(packet.Body,"lease", "Token") != lane.Token || lane.Token.Length==0) return;
391
var clear = ConnectedDock(t)!=null || (lane.Kind=="DEPART_REQUEST" ? FarFromPort(t) : SafeStopped(t,lane));
484
var clear = ConnectedDock(t)!=null || (lane.Kind==DepartRequestKind ? FarFromPort(t) : SafeStopped(t,lane));
392
485
if (clear) ClearLane(t.Id); else problem = L.F(L.FleetLaneReleaseDeferred, t.Id);
393
486
}
394
487
else if (packet.Kind == "RESULT")
@@ -416,8 +509,8 @@ namespace AutoMiningScript
416
509
var receipt = packet.From + "/" + packet.Id; if (markerReceiptSet.Contains(receipt)) return;
417
510
if (packet.Kind == "MARK")
418
511
{
419
Vector3D entry = Data.ReadVector(packet.Body, "mark", "Entry"), direction = Data.ReadVector(packet.Body, "mark", "Direction"), up = Data.ReadVector(packet.Body, "mark", "Up", Data.Up);
420
P.Log(planner.Manual(entry, direction, up, Data.ReadNumber(packet.Body, "mark", "Width"), Data.ReadNumber(packet.Body, "mark", "Height"), Data.ReadNumber(packet.Body, "mark", "Depth")));
512
Vector3D entry = Data.ReadVector(packet.Body, MarkCommand, "Entry"), direction = Data.ReadVector(packet.Body, MarkCommand, "Direction"), up = Data.ReadVector(packet.Body, MarkCommand, "Up", Data.Up);
513
P.Log(planner.Manual(entry, direction, up, Data.ReadNumber(packet.Body, MarkCommand, "Width"), Data.ReadNumber(packet.Body, MarkCommand, "Height"), Data.ReadNumber(packet.Body, MarkCommand, "Depth")));
421
514
}
422
515
else
423
516
{
@@ -430,14 +523,14 @@ namespace AutoMiningScript
430
523
bool FarFromPort(Telemetry t)
431
524
{
432
525
if (P.Now - t.ReceivedAt > 3) return false;
433
var distance = P.Config.Number("Dock", "LaneClearance", 60, 20, 300) + Data.Max(t.Radius, 3);
526
var distance = P.Config.Number(DockConfigSection, LaneClearanceKey, 60, 20, 300) + Data.Max(t.Radius, 3);
434
527
foreach (var d in docks) if (Data.DistanceSquared(d.GetPosition(), t.Position) < distance * distance) return false;
435
528
return true;
436
529
}
437
530
void ClearLane(string owner) {requests.Remove(owner);lanes.Remove(owner);}
438
531
string LaneSummary()
439
532
{
440
var names=Data.CreateList<string>();foreach(var lane in lanes.Values)names.Add(lane.Owner+" "+(lane.Kind=="DEPART_REQUEST"?L.FleetDeparting:L.FleetDocking));
533
var names=Data.CreateList<string>();foreach(var lane in lanes.Values)names.Add(lane.Owner+" "+(lane.Kind==DepartRequestKind?L.FleetDeparting:L.FleetDocking));
441
534
return names.Count==0?L.FleetLaneFree:string.Join(" / ",names);
442
535
}
443
536
bool SafeStopped(Telemetry t,Lane lane)
@@ -467,8 +560,8 @@ namespace AutoMiningScript
467
560
foreach(var owner in owners)
468
561
{
469
562
var lane=lanes[owner];Telemetry t;if(!miners.TryGetValue(owner,out t))continue;
470
if((lane.Kind=="DOCK_REQUEST" && ConnectedDock(t)!=null) ||
471
(lane.Kind=="DEPART_REQUEST" && !t.Connected && DepartureComplete(t.State) && FarFromPort(t)) ||
563
if((lane.Kind==DockRequestKind && ConnectedDock(t)!=null) ||
564
(lane.Kind==DepartRequestKind && !t.Connected && DepartureComplete(t.State) && FarFromPort(t)) ||
472
565
(t.State==FlightState.Fault && clearanceHolds.ContainsKey("fault-"+t.Id) && SafeStopped(t,lane))) ClearLane(owner);
473
566
}
474
567
// A stopped, known ship protects its actual location. An abandoned
@@ -484,7 +577,7 @@ namespace AutoMiningScript
484
577
=> state == FlightState.Transit || state == FlightState.Survey || state == FlightState.Align ||
485
578
state == FlightState.Drilling || state == FlightState.Retreat;
486
579
bool CanReassign(Telemetry t,Lane lane)
487
=> lane.Kind=="DOCK_REQUEST" && lane.Renew && lane.Consent && !t.Connected && P.Now-t.ReceivedAt<=3 &&
580
=> lane.Kind==DockRequestKind && lane.Renew && lane.Consent && !t.Connected && P.Now-t.ReceivedAt<=3 &&
488
581
(t.State==FlightState.Returning || t.State==FlightState.Holding) &&
489
582
Data.Distance(t.Position,lane.Position)>Data.Max(lane.ApproachDistance,t.Radius*2)+t.Radius+2;
490
583
void ScheduleLane()
@@ -494,7 +587,7 @@ namespace AutoMiningScript
494
587
{
495
588
if(!P.HasBudget(.55))return;Telemetry held;
496
589
if(!lane.Renew || !lane.Consent || !miners.TryGetValue(lane.Owner,out held))continue;
497
if(CanReassign(held,lane)) {lanes.Remove(lane.Owner);P.Bus.CancelPending("DOCK_GRANT",lane.Owner);}
590
if(CanReassign(held,lane)) {lanes.Remove(lane.Owner);P.Bus.CancelPending(DockGrantKind,lane.Owner);}
498
591
else RenewLane(held,lane);
499
592
}
500
593
var queue=Data.CreateList<Request>();
@@ -510,7 +603,7 @@ namespace AutoMiningScript
510
603
if(lanes.Count>=16)return;
511
604
if(!P.HasBudget(.6)){q.Problem=L.CommonBudgetDeferred;return;}
512
605
var t=miners[q.Owner];Lane granted=null;
513
if(q.Kind=="DEPART_REQUEST")
606
if(q.Kind==DepartRequestKind)
514
607
{
515
608
if(!running || operatorStops.Contains(t.Id) || pendingCancellation.ContainsKey(t.Id) || !HasOwnedJob(t.Id))continue;
516
609
var berth=ConnectedDock(t);if(berth==null)continue;
@@ -536,17 +629,17 @@ namespace AutoMiningScript
536
629
q.Problem="";granted.Token=NewLaneToken();lanes.Add(t.Id,granted);SendGrant(t);
537
630
}
538
631
}
539
double RequestScore(Request q) => P.Now-q.At+(q.Kind=="DOCK_REQUEST"?1000+(1-Data.Min(miners[q.Owner].Battery,miners[q.Owner].Hydrogen))*1000:0);
632
double RequestScore(Request q) => P.Now-q.At+(q.Kind==DockRequestKind?1000+(1-Data.Min(miners[q.Owner].Battery,miners[q.Owner].Hydrogen))*1000:0);
540
633
string NewLaneToken() => P.Config.Id+"-"+P.Bus.Session+"-lane-"+(++tokenSerial);
541
634
void WriteLanes(MyIni ini,string section,IEnumerable<Lane> values)
542
635
{
543
ini.Set(section,"LaneOwner","");ini.Set(section,"LaneToken","");
636
ini.Set(section,LaneOwnerKey,"");ini.Set(section,"LaneToken","");
544
637
int n=0;
545
638
foreach(var lane in values)
546
639
{
547
640
var s=n==0?section:section+"Lane"+n;n++;
548
ini.Set(s,"LaneOwner",lane.Owner);ini.Set(s,"LaneKind",lane.Kind);ini.Set(s,"LaneDock",lane.Dock);ini.Set(s,"LaneToken",lane.Token);
549
Data.PutVector(ini,s,"LanePosition",lane.Position);ini.Set(s,"LaneClearance",lane.Clearance);ini.Set(s,"LaneApproachDistance",lane.ApproachDistance);
641
ini.Set(s,LaneOwnerKey,lane.Owner);ini.Set(s,"LaneKind",lane.Kind);ini.Set(s,"LaneDock",lane.Dock);ini.Set(s,"LaneToken",lane.Token);
642
Data.PutVector(ini,s,"LanePosition",lane.Position);ini.Set(s,LaneClearanceKey,lane.Clearance);ini.Set(s,LaneApproachDistanceKey,lane.ApproachDistance);
550
643
}
551
644
ini.Set(section,"LaneCount",n);
552
645
}
@@ -554,16 +647,16 @@ namespace AutoMiningScript
554
647
{
555
648
if(clear)lanes.Clear();
556
649
int count=StoredCount(ini,section,"LaneCount",16);
557
if(count==0 && Data.Text(ini,section,"LaneOwner").Length>0)count=1;
650
if(count==0 && Data.Text(ini,section,LaneOwnerKey).Length>0)count=1;
558
651
var seen=new HashSet<string>();
559
652
for(int n=0;n<count;n++)
560
653
{
561
654
var s=n==0?section:section+"Lane"+n;
562
var lane=new Lane {Owner=Data.Text(ini,s,"LaneOwner"),Kind=Data.Text(ini,s,"LaneKind"),Dock=Data.ReadLong(ini,s,"LaneDock"),Renew=true};
563
if(lane.Owner.Length==0 || !seen.Add(lane.Owner) || (section=="remove" && !restoringRemoved.ContainsKey(lane.Owner)) || (lane.Kind!="DOCK_REQUEST" && lane.Kind!="DEPART_REQUEST"))throw Data.Invalid(L.FleetStorageRecordInvalid);
655
var lane=new Lane {Owner=Data.Text(ini,s,LaneOwnerKey),Kind=Data.Text(ini,s,"LaneKind"),Dock=Data.ReadLong(ini,s,"LaneDock"),Renew=true};
656
if(lane.Owner.Length==0 || !seen.Add(lane.Owner) || (section==RemovalSection && !restoringRemoved.ContainsKey(lane.Owner)) || (lane.Kind!=DockRequestKind && lane.Kind!=DepartRequestKind))throw Data.Invalid(L.FleetStorageRecordInvalid);
564
657
lane.Position=Data.ReadVector(ini,s,"LanePosition");
565
lane.ApproachDistance=Data.Clamp(Data.ReadNumber(ini,s,"LaneApproachDistance",EntryDistance(lane.Dock)),10,10000);
566
lane.Clearance=Data.Clamp(Data.ReadNumber(ini,s,"LaneClearance",P.Config.Number("Dock","LaneClearance",60,20,300)),20,10000);
658
lane.ApproachDistance=Data.Clamp(Data.ReadNumber(ini,s,LaneApproachDistanceKey,EntryDistance(lane.Dock)),10,10000);
659
lane.Clearance=Data.Clamp(Data.ReadNumber(ini,s,LaneClearanceKey,P.Config.Number(DockConfigSection,LaneClearanceKey,60,20,300)),20,10000);
567
660
lanes[lane.Owner]=lane;
568
661
}
569
662
}
@@ -572,7 +665,7 @@ namespace AutoMiningScript
572
665
var approach=EntryDistance(connector);
573
666
double offset;approachOffsets.TryGetValue(connector,out offset);
574
667
return new Lane {Owner=t.Id,Kind=request.Kind,Dock=connector,Position=frames[connector].Position,ApproachDistance=approach,
575
Clearance=Data.Max(P.Config.Number("Dock","LaneClearance",60,20,300)+offset,Data.Max(approach,t.Radius*2)-t.Radius-2)};
668
Clearance=Data.Max(P.Config.Number(DockConfigSection,LaneClearanceKey,60,20,300)+offset,Data.Max(approach,t.Radius*2)-t.Radius-2)};
576
669
}
577
670
string PortProblem(long connector,string owner)
578
671
{
@@ -587,7 +680,7 @@ namespace AutoMiningScript
587
680
request.Problem=L.FleetNoFreeDock;
588
681
IMyShipConnector berth = null; foreach (var d in docks) if (d.EntityId == lane.Dock) berth = d;
589
682
if (berth == null || berth.Closed || !berth.IsFunctional) return;
590
if (lane.Kind == "DEPART_REQUEST") { if (ConnectedDock(t) != berth) return; }
683
if (lane.Kind == DepartRequestKind) { if (ConnectedDock(t) != berth) return; }
591
684
else if (berth.Status == MyShipConnectorStatus.Connected) return;
592
685
request.Problem=PortProblem(lane.Dock,t.Id);if(request.Problem.Length>0)return;
593
686
lane.Token = NewLaneToken(); lane.Renew = false; SendGrant(t);
@@ -597,7 +690,7 @@ namespace AutoMiningScript
597
690
{
598
691
Lane lane;if(!lanes.TryGetValue(t.Id,out lane) || lane.Renew || lane.Token.Length==0)return;
599
692
var pose = Pose(lane.Dock, t.Id, lane.Token); if (pose == null) return;
600
P.Bus.Send(lane.Kind == "DEPART_REQUEST" ? "DEPART_GRANT" : "DOCK_GRANT", t.Id, pose.ToIni(), true, t.Address);
693
P.Bus.Send(lane.Kind == DepartRequestKind ? DepartGrantKind : DockGrantKind, t.Id, pose.ToIni(), true, t.Address);
601
694
}
602
695
string SearchRoot(Job job)
603
696
{
@@ -717,21 +810,21 @@ namespace AutoMiningScript
717
810
if (target.Length >= 2 && target[0] == '"' && target[target.Length - 1] == '"') target = target.Substring(1, target.Length - 2);
718
811
if (verb == "clear-tasks" || verb == "clear-all") { ClearAllTasks(); return; }
719
812
if (verb == "clear-offline" || verb == "remove-offline") { ClearOffline(); return; }
720
if (verb == "init" || verb == "rescan" || verb == "diagnose" || verb == "calibrate")
813
if (verb == "init" || verb == "rescan" || verb == DiagnoseCommand || verb == "calibrate")
721
814
{
722
if (target.Length == 0 && verb != "calibrate") { Scan(); P.Log(Diagnostics); if (verb == "diagnose") return; }
815
if (target.Length == 0 && verb != "calibrate") { Scan(); P.Log(Diagnostics); if (verb == DiagnoseCommand) return; }
723
816
OperateMiners(verb == "init" ? "rescan" : verb, target); return;
724
817
}
725
if (verb == "mark") { var mark = Marking.Parse(P, command); P.Log(planner.Manual(Data.ReadVector(mark, "mark", "Entry"), Data.ReadVector(mark, "mark", "Direction"), Data.ReadVector(mark, "mark", "Up", Data.Up), Data.ReadNumber(mark, "mark", "Width"), Data.ReadNumber(mark, "mark", "Height"), Data.ReadNumber(mark, "mark", "Depth"))); return; }
818
if (verb == MarkCommand) { var mark = Marking.Parse(P, command); P.Log(planner.Manual(Data.ReadVector(mark, MarkCommand, "Entry"), Data.ReadVector(mark, MarkCommand, "Direction"), Data.ReadVector(mark, MarkCommand, "Up", Data.Up), Data.ReadNumber(mark, MarkCommand, "Width"), Data.ReadNumber(mark, MarkCommand, "Height"), Data.ReadNumber(mark, MarkCommand, "Depth"))); return; }
726
819
if (verb == "survey")
727
820
{
728
821
if (controller == null) throw Data.Invalid(L.FleetSurveyControllerRequired);
729
822
var radius = parts.Length > 1 ? Marking.Number(parts[1]) : 0;
730
823
P.Log(planner.Survey(controller.GetPosition(), controller.GetNaturalGravity(), controller.WorldMatrix.Forward, controller.WorldMatrix.Up, radius)); return;
731
824
}
732
if (verb == "start" || verb == "resume" || verb == "pause" || verb == "return" || verb == "release" || verb == "return-all")
825
if (verb == "start" || verb == ResumeCommand || verb == "pause" || verb == ReturnCommand || verb == "release" || verb == ReturnAllCommand)
733
826
{
734
OperateMiners(verb == "return-all" ? "return" : verb == "start" ? "resume" : verb, verb == "return-all" ? "all" : target); return;
827
OperateMiners(verb == ReturnAllCommand ? ReturnCommand : verb == "start" ? ResumeCommand : verb, verb == ReturnAllCommand ? "all" : target); return;
735
828
}
736
829
if (verb == "release-lease" && target.Length > 0)
737
830
{
@@ -744,13 +837,13 @@ namespace AutoMiningScript
744
837
{
745
838
var all = target.Length == 0 || target == "*" || target.Equals("all", Data.IgnoreCase);
746
839
Telemetry selected; if (!all && !miners.TryGetValue(target, out selected)) throw Data.Invalid(L.FleetUnknownMiner);
747
if (action == "resume")
840
if (action == ResumeCommand)
748
841
{
749
842
// Starting one miner while the fleet is stopped must not dispatch the others.
750
843
if (!all && !running) foreach (var t in miners.Values) if (t.Id != target) operatorStops.Add(t.Id);
751
844
running = true; if (all) operatorStops.Clear(); else operatorStops.Remove(target);
752
845
}
753
else if (action == "pause" || action == "return" || action == "release")
846
else if (action == "pause" || action == ReturnCommand || action == "release")
754
847
{
755
848
if (all) running = false;
756
849
foreach (var t in miners.Values) if (all || t.Id == target) operatorStops.Add(t.Id);
@@ -762,13 +855,13 @@ namespace AutoMiningScript
762
855
// Offline identities retain their safety reservation; a later reconnect
763
856
// must not execute a stale one-off service command unexpectedly.
764
857
if (P.Now - t.ReceivedAt > OfflineSeconds) continue;
765
var text = action == "resume" && pendingCancellation.ContainsKey(t.Id) && ConnectedDock(t) == null ? "return" : action;
766
var body = new MyIni(); body.Set("command", "Text", text); P.Bus.Send("COMMAND", t.Id, body, true, t.Address); count++;
767
if (action == "diagnose") report += t.Id + ": " + L.State(t) + " / " + t.Reason + "\n";
858
var text = action == ResumeCommand && pendingCancellation.ContainsKey(t.Id) && ConnectedDock(t) == null ? ReturnCommand : action;
859
var body = new MyIni(); body.Set(CommandSection, TextKey, text); P.Bus.Send(CommandKind, t.Id, body, true, t.Address); count++;
860
if (action == DiagnoseCommand) report += t.Id + ": " + L.State(t) + " / " + t.Reason + "\n";
768
861
}
769
if (action == "diagnose" && report.Length > 0) problem = report;
862
if (action == DiagnoseCommand && report.Length > 0) problem = report;
770
863
P.Log(L.F(L.FleetOperatorSent, count));
771
if (action == "resume" && pendingCancellation.Count > 0) P.Log(L.F(L.FleetAwaitCancellation, pendingCancellation.Count));
864
if (action == ResumeCommand && pendingCancellation.Count > 0) P.Log(L.F(L.FleetAwaitCancellation, pendingCancellation.Count));
772
865
}
773
866
double OfflineSeconds => P.Config.Number("Display", "OfflineSeconds", 5, 1, 60);
774
867
bool HasOccupancy(string id)
@@ -836,11 +929,11 @@ namespace AutoMiningScript
836
929
if (!string.IsNullOrEmpty(P.Storage))
837
930
{
838
931
if (!P.Storage.StartsWith(StoragePrefix, Data.Ordinal)) throw Data.Invalid(L.FleetStorageRecordInvalid);
839
var record = new MyIni(); record.Set("remove", "Count", removed.Count); record.Set("remove", "NextWaitingSlot", nextWaitingSlot); record.Set("remove", "TaskSerial", planner.TaskSerial);
932
var record = new MyIni(); record.Set(RemovalSection, "Count", removed.Count); record.Set(RemovalSection, NextWaitingSlotKey, nextWaitingSlot); record.Set(RemovalSection, "TaskSerial", planner.TaskSerial);
840
933
var ids = new HashSet<string>(); int n = 0;
841
foreach (var t in removed) { ids.Add(t.Id); record.Set("remove", "Id" + n, t.Id); record.Set("remove", "Address" + n, t.Address); record.Set("remove", "Cleared" + n, verifiedClear.Contains(t.Id)); n++; }
934
foreach (var t in removed) { ids.Add(t.Id); record.Set(RemovalSection, "Id" + n, t.Id); record.Set(RemovalSection, AddressKey + n, t.Address); record.Set(RemovalSection, "Cleared" + n, verifiedClear.Contains(t.Id)); n++; }
842
935
var retainedLanes=Data.CreateList<Lane>();foreach(var lane in lanes.Values)if(ids.Contains(lane.Owner) && !verifiedClear.Contains(lane.Owner))retainedLanes.Add(lane);
843
WriteLanes(record,"remove",retainedLanes);record.Set("remove","HasLane",retainedLanes.Count>0);record.Set("remove","TokenSerial",tokenSerial);
936
WriteLanes(record,RemovalSection,retainedLanes);record.Set(RemovalSection,"HasLane",retainedLanes.Count>0);record.Set(RemovalSection,TokenSerialKey,tokenSerial);
844
937
var chunks = Data.CreateList<string>(); chunks.Add(P.Storage); chunks.Add(TaskPlanner.StorageRecord(record, "RemoveMiners"));
845
938
var captured = new HashSet<string>();
846
939
foreach (var t in removed)
@@ -862,7 +955,7 @@ namespace AutoMiningScript
862
955
}
863
956
static void AppendRemovedHold(List<string> chunks, Job job, long address)
864
957
{
865
var record = job.ToIni(); record.Set("remove", "Address", address); chunks.Add(TaskPlanner.StorageRecord(record, "RemovedClearance"));
958
var record = job.ToIni(); record.Set(RemovalSection, AddressKey, address); chunks.Add(TaskPlanner.StorageRecord(record, "RemovedClearance"));
866
959
}
867
960
void ClearAllTasks()
868
961
{
@@ -876,13 +969,13 @@ namespace AutoMiningScript
876
969
foreach (var t in miners.Values) if (!pendingCancellation.ContainsKey(t.Id)) pendingCancellation[t.Id] = t.JobId;
877
970
planner.ClearTasks(); assignmentMiner = ""; assignmentBest = null; assignmentJobCursor = assignMinerCursor = 0; assignmentScore = double.MinValue;
878
971
occupied.Clear(); dispatchedAt.Clear(); cancellationSent.Clear(); storageMiners = null; storageMinerSnapshot = null; storageMinerCursor = 0;
879
var requestIds = new List<string>(requests.Keys); foreach (var id in requestIds) if (requests[id].Kind == "DEPART_REQUEST" && !lanes.ContainsKey(id)) requests.Remove(id);
880
P.Bus.CancelPending("JOB"); P.Bus.CancelPending("COMMAND"); P.Bus.CancelPending("DEPART_GRANT");
972
var requestIds = new List<string>(requests.Keys); foreach (var id in requestIds) if (requests[id].Kind == DepartRequestKind && !lanes.ContainsKey(id)) requests.Remove(id);
973
P.Bus.CancelPending("JOB"); P.Bus.CancelPending(CommandKind); P.Bus.CancelPending(DepartGrantKind);
881
974
// Commit a bounded, complete empty-plan snapshot before sending cancellation.
882
975
// World-save/recompile in this very frame can never restore the previous plan.
883
var fleetHeader = new MyIni(); SaveFleet(fleetHeader, false, false); fleetHeader.Set("storage", "Role", P.Config.Role); fleetHeader.Set("storage", "Id", P.Config.Id);
884
var chunks = Data.CreateList<string>(); chunks.Add(StoragePrefix); chunks.Add(TaskPlanner.StorageRecord(fleetHeader, "Fleet"));
885
foreach (var t in miners.Values) { var record = t.ToIni(); record.Set("record", "Address", t.Address); chunks.Add(TaskPlanner.StorageRecord(record, "Miner")); }
976
var fleetHeader = new MyIni(); SaveFleet(fleetHeader, false, false); fleetHeader.Set(StorageSection, "Role", P.Config.Role); fleetHeader.Set(StorageSection, "Id", P.Config.Id);
977
var chunks = Data.CreateList<string>(); chunks.Add(StoragePrefix); chunks.Add(TaskPlanner.StorageRecord(fleetHeader, FleetConfigSection));
978
foreach (var t in miners.Values) { var record = t.ToIni(); record.Set(RecordSection, AddressKey, t.Address); chunks.Add(TaskPlanner.StorageRecord(record, MinerKey)); }
886
979
AppendAuxiliaryRecords(chunks);
887
980
var emptyPlanner = new MyIni(); planner.Save(emptyPlanner); chunks.Add(TaskPlanner.StorageRecord(emptyPlanner, "Planner"));
888
981
P.Storage = string.Concat(chunks); CapturePersistedOccupancies();
@@ -893,8 +986,8 @@ namespace AutoMiningScript
893
986
{
894
987
if (cancelledThrough < 0 || !pendingCancellation.ContainsKey(t.Id) || !P.HasBudget(0.65)) return;
895
988
double last; if (!force && cancellationSent.TryGetValue(t.Id, out last) && P.Now - last < 2) return;
896
var body = new MyIni(); body.Set("command", "Text", "cancel-through " + cancelledThrough.ToString(Data.Culture));
897
P.Bus.Send("COMMAND", t.Id, body, true, t.Address); cancellationSent[t.Id] = P.Now;
989
var body = new MyIni(); body.Set(CommandSection, TextKey, "cancel-through " + cancelledThrough.ToString(Data.Culture));
990
P.Bus.Send(CommandKind, t.Id, body, true, t.Address); cancellationSent[t.Id] = P.Now;
898
991
}
899
992
void RetryCancellation()
900
993
{
@@ -907,7 +1000,7 @@ namespace AutoMiningScript
907
1000
if (previous.Length == 0 ? t.JobId.Length > 0 : t.JobId == previous) return;
908
1001
pendingCancellation.Remove(t.Id); cancellationSent.Remove(t.Id);
909
1002
ClearLane(t.Id);
910
if (running && !operatorStops.Contains(t.Id) && t.State != FlightState.Manual) { var body = new MyIni(); body.Set("command", "Text", "resume"); P.Bus.Send("COMMAND", t.Id, body, true, t.Address); }
1003
if (running && !operatorStops.Contains(t.Id) && t.State != FlightState.Manual) { var body = new MyIni(); body.Set(CommandSection, TextKey, ResumeCommand); P.Bus.Send(CommandKind, t.Id, body, true, t.Address); }
911
1004
if (pendingCancellation.Count == 0) P.Log(L.FleetCancellationComplete);
912
1005
}
913
1006
const string StoragePrefix = "XFEAMS-FLEET-3\n";
@@ -915,7 +1008,7 @@ namespace AutoMiningScript
915
1008
public static string ResetStorage(long epoch) => ResetPrefix+epoch.ToString(Data.Culture);
916
1009
void RecallOldEpoch(Packet packet)
917
1010
{
918
var body=new MyIni();body.Set("command","Text","return");body.Set("command","Reset",true);P.Bus.Send("COMMAND",packet.From,body,false,packet.Source);
1011
var body=new MyIni();body.Set(CommandSection,TextKey,ReturnCommand);body.Set(CommandSection,"Reset",true);P.Bus.Send(CommandKind,packet.From,body,false,packet.Source);
919
1012
}
920
1013
string restoreValue;
921
1014
int restoreCursor, restorePhase, restoreAuxiliaryHolds, restoreAuxiliaryMarkers;
@@ -950,27 +1043,27 @@ namespace AutoMiningScript
950
1043
int colon = restoreValue.IndexOf(':', restoreCursor), length;
951
1044
if (colon < restoreCursor || colon - restoreCursor > 8 || !int.TryParse(restoreValue.Substring(restoreCursor, colon - restoreCursor), out length) || length < 1 || length > 32768 || length > restoreValue.Length - colon - 1) throw Data.Invalid(L.FleetStorageRecordInvalid);
952
1045
var ini = new MyIni(); if (!ini.TryParse(restoreValue.Substring(colon + 1, length))) throw Data.Invalid(L.FleetStorageRecordInvalid);
953
restoreCursor = colon + 1 + length; var kind = Data.Text(ini,"record", "Kind");
954
if (kind == "Fleet" && restorePhase == 0)
1046
restoreCursor = colon + 1 + length; var kind = Data.Text(ini,RecordSection, "Kind");
1047
if (kind == FleetConfigSection && restorePhase == 0)
955
1048
{
956
if (Data.Text(ini,"storage", "Role") != P.Config.Role || Data.Text(ini,"storage", "Id") != P.Config.Id) throw Data.Invalid(L.FleetStorageIdentityInvalid);
1049
if (Data.Text(ini,StorageSection, "Role") != P.Config.Role || Data.Text(ini,StorageSection, "Id") != P.Config.Id) throw Data.Invalid(L.FleetStorageIdentityInvalid);
957
1050
restoreFleet = ini; miners.Clear(); restorePhase = 1;
958
1051
}
959
else if (kind == "Miner" && restorePhase == 1)
1052
else if (kind == MinerKey && restorePhase == 1)
960
1053
{
961
var t = Telemetry.FromIni(ini); if (miners.Count >= 16 || miners.ContainsKey(t.Id)) throw Data.Invalid(L.FleetStoredSizeInvalid); t.Address = Data.ReadLong(ini, "record", "Address"); t.ReceivedAt = -1e9; miners.Add(t.Id, t);
1054
var t = Telemetry.FromIni(ini); if (miners.Count >= 16 || miners.ContainsKey(t.Id)) throw Data.Invalid(L.FleetStoredSizeInvalid); t.Address = Data.ReadLong(ini, RecordSection, AddressKey); t.ReceivedAt = -1e9; miners.Add(t.Id, t);
962
1055
}
963
else if (kind == "Clearance" && restorePhase == 1 && Data.Flag(restoreFleet,"fleet", "AuxRecords"))
1056
else if (kind == "Clearance" && restorePhase == 1 && Data.Flag(restoreFleet,FleetSection, AuxiliaryRecordsKey))
964
1057
{
965
if (restoreAuxiliaryHolds >= 64 || restoreAuxiliaryHolds >= Data.ReadLong(restoreFleet, "fleet", "ClearanceCount")) throw Data.Invalid(L.FleetClearanceStorageInvalid);
1058
if (restoreAuxiliaryHolds >= 64 || restoreAuxiliaryHolds >= Data.ReadLong(restoreFleet, FleetSection, ClearanceCountKey)) throw Data.Invalid(L.FleetClearanceStorageInvalid);
966
1059
Job hold = Job.FromIni(ini); int n = restoreAuxiliaryHolds++;
967
restoreFleet.Set("fleet", "ClearanceOwner" + n, hold.Owner); restoreFleet.Set("fleet", "ClearanceJob" + n, hold.Id); restoreFleet.Set("fleet", "ClearanceData" + n, ini.ToString());
1060
restoreFleet.Set(FleetSection, ClearanceOwnerKey + n, hold.Owner); restoreFleet.Set(FleetSection, ClearanceJobKey + n, hold.Id); restoreFleet.Set(FleetSection, ClearanceDataKey + n, ini.ToString());
968
1061
}
969
else if (kind == "MarkerReceipt" && restorePhase == 1 && Data.Flag(restoreFleet,"fleet", "AuxRecords"))
1062
else if (kind == MarkerReceiptKey && restorePhase == 1 && Data.Flag(restoreFleet,FleetSection, AuxiliaryRecordsKey))
970
1063
{
971
1064
var receipt = Data.Text(ini,"receipt", "Value");
972
if (receipt.Length > 256 || restoreAuxiliaryMarkers >= 128 || restoreAuxiliaryMarkers >= Data.ReadLong(restoreFleet, "fleet", "MarkerCount")) throw Data.Invalid(L.FleetMarkerReceiptsInvalid);
973
restoreFleet.Set("fleet", "MarkerReceipt" + (restoreAuxiliaryMarkers++), receipt);
1065
if (receipt.Length > 256 || restoreAuxiliaryMarkers >= 128 || restoreAuxiliaryMarkers >= Data.ReadLong(restoreFleet, FleetSection, MarkerCountKey)) throw Data.Invalid(L.FleetMarkerReceiptsInvalid);
1066
restoreFleet.Set(FleetSection, MarkerReceiptKey + (restoreAuxiliaryMarkers++), receipt);
974
1067
}
975
1068
else if (kind == "Planner" && restorePhase == 1) { planner.BeginRecordLoad(ini); restorePhase = 2; }
976
1069
else if ((kind == "Job" || kind == "Region") && restorePhase == 2) planner.LoadRecord(ini, kind);
@@ -982,7 +1075,7 @@ namespace AutoMiningScript
982
1075
else if (kind == "RemovedClearance" && restorePhase == 3)
983
1076
{
984
1077
Job hold = Job.FromIni(ini); long address;
985
if (!restoringRemoved.TryGetValue(hold.Owner, out address) || address != Data.ReadLong(ini, "remove", "Address")) throw Data.Invalid(L.FleetClearanceStorageInvalid);
1078
if (!restoringRemoved.TryGetValue(hold.Owner, out address) || address != Data.ReadLong(ini, RemovalSection, AddressKey)) throw Data.Invalid(L.FleetClearanceStorageInvalid);
986
1079
if (!clearanceHolds.ContainsKey(hold.Id) && clearanceHolds.Count >= 64) throw Data.Invalid(L.FleetClearanceStorageInvalid);
987
1080
clearanceHolds[hold.Id] = hold; persistedOccupancies.Add(hold.Id); ProtectOwner(hold.Owner, address);
988
1081
}
@@ -995,26 +1088,26 @@ namespace AutoMiningScript
995
1088
}
996
1089
void FinishFleetRecordLoad()
997
1090
{
998
if (Data.Flag(restoreFleet,"fleet", "AuxRecords") && (restoreAuxiliaryHolds != Data.ReadLong(restoreFleet, "fleet", "ClearanceCount") || restoreAuxiliaryMarkers != Data.ReadLong(restoreFleet, "fleet", "MarkerCount"))) throw Data.Invalid(L.FleetStorageRecordInvalid);
1091
if (Data.Flag(restoreFleet,FleetSection, AuxiliaryRecordsKey) && (restoreAuxiliaryHolds != Data.ReadLong(restoreFleet, FleetSection, ClearanceCountKey) || restoreAuxiliaryMarkers != Data.ReadLong(restoreFleet, FleetSection, MarkerCountKey))) throw Data.Invalid(L.FleetStorageRecordInvalid);
999
1092
planner.FinishRecordLoad(); LoadFleet(restoreFleet, false); CapturePersistedOccupancies();
1000
1093
}
1001
1094
void RestoreRemovedMiners(MyIni ini)
1002
1095
{
1003
int count = StoredCount(ini, "remove", "Count", 16), slot = StoredCount(ini, "remove", "NextWaitingSlot", 1000000);
1004
planner.PreserveSerial(Data.ReadLong(ini, "remove", "TaskSerial")); nextWaitingSlot = Data.Max(nextWaitingSlot, slot); restoringRemoved.Clear();
1096
int count = StoredCount(ini, RemovalSection, "Count", 16), slot = StoredCount(ini, RemovalSection, NextWaitingSlotKey, 1000000);
1097
planner.PreserveSerial(Data.ReadLong(ini, RemovalSection, "TaskSerial")); nextWaitingSlot = Data.Max(nextWaitingSlot, slot); restoringRemoved.Clear();
1005
1098
for (int n = 0; n < count; n++)
1006
1099
{
1007
var id = Data.Text(ini,"remove", "Id" + n); var address = Data.ReadLong(ini, "remove", "Address" + n);
1100
var id = Data.Text(ini,RemovalSection, "Id" + n); var address = Data.ReadLong(ini, RemovalSection, AddressKey + n);
1008
1101
if (id.Length == 0 || restoringRemoved.ContainsKey(id)) throw Data.Invalid(L.FleetStoredSizeInvalid);
1009
1102
restoringRemoved[id] = address;
1010
1103
}
1011
if (Data.Flag(ini,"remove", "HasLane"))
1104
if (Data.Flag(ini,RemovalSection, "HasLane"))
1012
1105
{
1013
ReadLanes(ini,"remove",false);tokenSerial=Data.Max(tokenSerial,StoredCount(ini,"remove","TokenSerial",1000000000));
1106
ReadLanes(ini,RemovalSection,false);tokenSerial=Data.Max(tokenSerial,StoredCount(ini,RemovalSection,TokenSerialKey,1000000000));
1014
1107
}
1015
1108
for (int n = 0; n < count; n++)
1016
1109
{
1017
var id = Data.Text(ini,"remove", "Id" + n); if (Data.Flag(ini,"remove", "Cleared" + n)) ReleaseOwnerOccupancy(id);
1110
var id = Data.Text(ini,RemovalSection, "Id" + n); if (Data.Flag(ini,RemovalSection, "Cleared" + n)) ReleaseOwnerOccupancy(id);
1018
1111
ForgetMiner(id, restoringRemoved[id]);
1019
1112
}
1020
1113
}
@@ -1029,14 +1122,14 @@ namespace AutoMiningScript
1029
1122
int work = 0;
1030
1123
while (storageMinerCursor < storageMinerSnapshot.Count && work++ < 2 && P.HasBudget(0.5))
1031
1124
{
1032
var t = storageMinerSnapshot[storageMinerCursor++]; var ini = t.ToIni(); ini.Set("record", "Address", t.Address); storageMiners.Add(TaskPlanner.StorageRecord(ini, "Miner"));
1125
var t = storageMinerSnapshot[storageMinerCursor++]; var ini = t.ToIni(); ini.Set(RecordSection, AddressKey, t.Address); storageMiners.Add(TaskPlanner.StorageRecord(ini, MinerKey));
1033
1126
}
1034
1127
if (storageMinerCursor < storageMinerSnapshot.Count) return false;
1035
1128
List<string> records; if (!planner.CheckpointRecords(out records)) return false;
1036
1129
// Fleet metadata and ownership are captured on this same final frame. Telemetry
1037
1130
// is restored as stale; its age and connector claims never grant a flight lane.
1038
var header = new MyIni(); SaveFleet(header, false, false); header.Set("fleet", "MinerCount", storageMiners.Count); header.Set("storage", "Role", P.Config.Role); header.Set("storage", "Id", P.Config.Id);
1039
var chunks = Data.CreateList<string>(); chunks.Add(StoragePrefix); chunks.Add(TaskPlanner.StorageRecord(header, "Fleet")); chunks.AddRange(storageMiners); AppendAuxiliaryRecords(chunks); chunks.AddRange(records); value = string.Concat(chunks); storageMiners = null; storageMinerSnapshot = null; CapturePersistedOccupancies(); return true;
1131
var header = new MyIni(); SaveFleet(header, false, false); header.Set(FleetSection, MinerCountKey, storageMiners.Count); header.Set(StorageSection, "Role", P.Config.Role); header.Set(StorageSection, "Id", P.Config.Id);
1132
var chunks = Data.CreateList<string>(); chunks.Add(StoragePrefix); chunks.Add(TaskPlanner.StorageRecord(header, FleetConfigSection)); chunks.AddRange(storageMiners); AppendAuxiliaryRecords(chunks); chunks.AddRange(records); value = string.Concat(chunks); storageMiners = null; storageMinerSnapshot = null; CapturePersistedOccupancies(); return true;
1040
1133
}
1041
1134
public override void Save(MyIni ini)
1042
1135
{
@@ -1050,23 +1143,23 @@ namespace AutoMiningScript
1050
1143
void AppendAuxiliaryRecords(List<string> chunks)
1051
1144
{
1052
1145
foreach (var hold in clearanceHolds.Values) chunks.Add(TaskPlanner.StorageRecord(hold.ToIni(), "Clearance"));
1053
foreach (var receipt in markerReceipts) { var ini = new MyIni(); ini.Set("receipt", "Value", receipt); chunks.Add(TaskPlanner.StorageRecord(ini, "MarkerReceipt")); }
1146
foreach (var receipt in markerReceipts) { var ini = new MyIni(); ini.Set("receipt", "Value", receipt); chunks.Add(TaskPlanner.StorageRecord(ini, MarkerReceiptKey)); }
1054
1147
}
1055
1148
void SaveFleet(MyIni ini, bool includeMiners = true, bool includeAuxiliary = true)
1056
1149
{
1057
ini.Set("fleet","Epoch",P.Bus.Epoch);
1058
ini.Set("fleet", "Running", running); WriteLanes(ini,"fleet",lanes.Values); ini.Set("fleet", "TokenSerial", tokenSerial);
1059
ini.Set("fleet", "OperatorStopCount", operatorStops.Count); int stopIndex = 0; foreach (var id in operatorStops) ini.Set("fleet", "OperatorStop" + stopIndex++, id);
1060
ini.Set("fleet", "NextWaitingSlot", nextWaitingSlot); ini.Set("fleet", "ProtectedOwnerCount", protectedOwners.Count); int protectedIndex = 0;
1061
foreach (var owner in protectedOwners) { ini.Set("fleet", "ProtectedOwnerId" + protectedIndex, owner.Key); ini.Set("fleet", "ProtectedOwnerAddress" + protectedIndex, owner.Value); protectedIndex++; }
1062
ini.Set("fleet", "CancelledThrough", cancelledThrough); ini.Set("fleet", "CancellationCount", pendingCancellation.Count); int cancelIndex = 0;
1063
foreach (var cancellation in pendingCancellation) { ini.Set("fleet", "CancellationOwner" + cancelIndex, cancellation.Key); ini.Set("fleet", "CancellationJob" + cancelIndex, cancellation.Value); cancelIndex++; }
1064
ini.Set("fleet", "MinerCount", miners.Count); int n = 0;
1065
if (includeMiners) foreach (var t in miners.Values) { ini.Set("fleet", "Miner" + n, t.ToIni().ToString()); ini.Set("fleet", "Address" + n, t.Address); n++; }
1066
ini.Set("fleet", "AuxRecords", !includeAuxiliary);
1067
ini.Set("fleet", "ClearanceCount", clearanceHolds.Count); n = 0;
1068
if (includeAuxiliary) foreach (var hold in clearanceHolds.Values) { ini.Set("fleet", "ClearanceOwner" + n, hold.Owner); ini.Set("fleet", "ClearanceJob" + n, hold.Id); ini.Set("fleet", "ClearanceData" + n, hold.ToIni().ToString()); n++; }
1069
ini.Set("fleet", "MarkerCount", markerReceipts.Count); n = 0; if (includeAuxiliary) foreach (var receipt in markerReceipts) ini.Set("fleet", "MarkerReceipt" + (n++), receipt);
1150
ini.Set(FleetSection,"Epoch",P.Bus.Epoch);
1151
ini.Set(FleetSection, "Running", running); WriteLanes(ini,FleetSection,lanes.Values); ini.Set(FleetSection, TokenSerialKey, tokenSerial);
1152
ini.Set(FleetSection, OperatorStopCountKey, operatorStops.Count); int stopIndex = 0; foreach (var id in operatorStops) ini.Set(FleetSection, "OperatorStop" + stopIndex++, id);
1153
ini.Set(FleetSection, NextWaitingSlotKey, nextWaitingSlot); ini.Set(FleetSection, ProtectedOwnerCountKey, protectedOwners.Count); int protectedIndex = 0;
1154
foreach (var owner in protectedOwners) { ini.Set(FleetSection, "ProtectedOwnerId" + protectedIndex, owner.Key); ini.Set(FleetSection, ProtectedOwnerAddressKey + protectedIndex, owner.Value); protectedIndex++; }
1155
ini.Set(FleetSection, "CancelledThrough", cancelledThrough); ini.Set(FleetSection, CancellationCountKey, pendingCancellation.Count); int cancelIndex = 0;
1156
foreach (var cancellation in pendingCancellation) { ini.Set(FleetSection, CancellationOwnerKey + cancelIndex, cancellation.Key); ini.Set(FleetSection, "CancellationJob" + cancelIndex, cancellation.Value); cancelIndex++; }
1157
ini.Set(FleetSection, MinerCountKey, miners.Count); int n = 0;
1158
if (includeMiners) foreach (var t in miners.Values) { ini.Set(FleetSection, MinerKey + n, t.ToIni().ToString()); ini.Set(FleetSection, AddressKey + n, t.Address); n++; }
1159
ini.Set(FleetSection, AuxiliaryRecordsKey, !includeAuxiliary);
1160
ini.Set(FleetSection, ClearanceCountKey, clearanceHolds.Count); n = 0;
1161
if (includeAuxiliary) foreach (var hold in clearanceHolds.Values) { ini.Set(FleetSection, ClearanceOwnerKey + n, hold.Owner); ini.Set(FleetSection, ClearanceJobKey + n, hold.Id); ini.Set(FleetSection, ClearanceDataKey + n, hold.ToIni().ToString()); n++; }
1162
ini.Set(FleetSection, MarkerCountKey, markerReceipts.Count); n = 0; if (includeAuxiliary) foreach (var receipt in markerReceipts) ini.Set(FleetSection, MarkerReceiptKey + (n++), receipt);
1070
1163
}
1071
1164
public override void Load(MyIni ini)
1072
1165
{
@@ -1074,19 +1167,19 @@ namespace AutoMiningScript
1074
1167
}
1075
1168
void LoadFleet(MyIni ini, bool includeMiners = true)
1076
1169
{
1077
var epoch=Data.ReadLong(ini,"fleet","Epoch");if(epoch<0 || epoch>DateTime.MaxValue.Ticks)throw Data.Invalid(L.FleetStorageIdentityInvalid);P.Bus.Epoch=epoch;
1078
running = Data.Flag(ini,"fleet", "Running");ReadLanes(ini,"fleet",true);tokenSerial = (int)Data.ReadLong(ini, "fleet", "TokenSerial");
1079
cancelledThrough = Data.ReadLong(ini, "fleet", "CancelledThrough", -1); int cancellationCount = (int)Data.ReadLong(ini, "fleet", "CancellationCount");
1170
var epoch=Data.ReadLong(ini,FleetSection,"Epoch");if(epoch<0 || epoch>DateTime.MaxValue.Ticks)throw Data.Invalid(L.FleetStorageIdentityInvalid);P.Bus.Epoch=epoch;
1171
running = Data.Flag(ini,FleetSection, "Running");ReadLanes(ini,FleetSection,true);tokenSerial = (int)Data.ReadLong(ini, FleetSection, TokenSerialKey);
1172
cancelledThrough = Data.ReadLong(ini, FleetSection, "CancelledThrough", -1); int cancellationCount = (int)Data.ReadLong(ini, FleetSection, CancellationCountKey);
1080
1173
if (cancelledThrough < -1 || cancelledThrough > planner.TaskSerial || cancellationCount < 0 || cancellationCount > 16) throw Data.Invalid(L.FleetCancellationStorageInvalid);
1081
1174
pendingCancellation.Clear(); cancellationSent.Clear();
1082
for (int n = 0; n < cancellationCount; n++) { var owner = Data.Text(ini,"fleet", "CancellationOwner" + n); if (owner.Length == 0 || pendingCancellation.ContainsKey(owner)) throw Data.Invalid(L.FleetCancellationStorageInvalid); pendingCancellation[owner] = Data.Text(ini,"fleet", "CancellationJob" + n); }
1083
int count = (int)Data.ReadLong(ini, "fleet", "MinerCount"); if (count < 0 || count > 16) throw Data.Invalid(L.FleetStoredSizeInvalid);
1175
for (int n = 0; n < cancellationCount; n++) { var owner = Data.Text(ini,FleetSection, CancellationOwnerKey + n); if (owner.Length == 0 || pendingCancellation.ContainsKey(owner)) throw Data.Invalid(L.FleetCancellationStorageInvalid); pendingCancellation[owner] = Data.Text(ini,FleetSection, "CancellationJob" + n); }
1176
int count = (int)Data.ReadLong(ini, FleetSection, MinerCountKey); if (count < 0 || count > 16) throw Data.Invalid(L.FleetStoredSizeInvalid);
1084
1177
if (includeMiners) miners.Clear();
1085
if (includeMiners) for (int n = 0; n < count; n++) { var body = new MyIni(); if (!body.TryParse(Data.Text(ini,"fleet", "Miner" + n))) continue; var t = Telemetry.FromIni(body); t.Address = Data.ReadLong(ini, "fleet", "Address" + n); t.ReceivedAt = -1e9; miners[t.Id] = t; }
1178
if (includeMiners) for (int n = 0; n < count; n++) { var body = new MyIni(); if (!body.TryParse(Data.Text(ini,FleetSection, MinerKey + n))) continue; var t = Telemetry.FromIni(body); t.Address = Data.ReadLong(ini, FleetSection, AddressKey + n); t.ReceivedAt = -1e9; miners[t.Id] = t; }
1086
1179
if (!includeMiners && miners.Count != count) throw Data.Invalid(L.FleetStoredSizeInvalid);
1087
operatorStops.Clear(); int stops = StoredCount(ini, "fleet", "OperatorStopCount", 16);
1088
for (int n = 0; n < stops; n++) { var id = Data.Text(ini,"fleet", "OperatorStop" + n); if (id.Length == 0 || !operatorStops.Add(id)) throw Data.Invalid(L.FleetStoredSizeInvalid); }
1089
nextWaitingSlot = StoredCount(ini, "fleet", "NextWaitingSlot", 1000000); var slots = new HashSet<int>();
1180
operatorStops.Clear(); int stops = StoredCount(ini, FleetSection, OperatorStopCountKey, 16);
1181
for (int n = 0; n < stops; n++) { var id = Data.Text(ini,FleetSection, "OperatorStop" + n); if (id.Length == 0 || !operatorStops.Add(id)) throw Data.Invalid(L.FleetStoredSizeInvalid); }
1182
nextWaitingSlot = StoredCount(ini, FleetSection, NextWaitingSlotKey, 1000000); var slots = new HashSet<int>();
1090
1183
foreach (var t in miners.Values) if (t.WaitingSlot >= 0)
1091
1184
{
1092
1185
if (t.WaitingSlot >= 1000000 || !slots.Add(t.WaitingSlot)) throw Data.Invalid(L.FleetStoredSizeInvalid);
@@ -1097,21 +1190,21 @@ namespace AutoMiningScript
1097
1190
if (nextWaitingSlot >= 1000000) throw Data.Invalid(L.FleetWaitingSlotsExhausted);
1098
1191
t.WaitingSlot = nextWaitingSlot++;
1099
1192
}
1100
clearanceHolds.Clear(); int holds = (int)Data.ReadLong(ini, "fleet", "ClearanceCount"); if (holds < 0 || holds > 64) throw Data.Invalid(L.FleetClearanceStorageInvalid);
1193
clearanceHolds.Clear(); int holds = (int)Data.ReadLong(ini, FleetSection, ClearanceCountKey); if (holds < 0 || holds > 64) throw Data.Invalid(L.FleetClearanceStorageInvalid);
1101
1194
for (int n = 0; n < holds; n++)
1102
1195
{
1103
string owner = Data.Text(ini,"fleet", "ClearanceOwner" + n), id = Data.Text(ini,"fleet", "ClearanceJob" + n); Job held = null;
1104
if (ini.ContainsKey("fleet", "ClearanceData" + n)) { var body = new MyIni(); if (!body.TryParse(Data.Text(ini,"fleet", "ClearanceData" + n))) throw Data.Invalid(L.FleetClearanceStorageInvalid); held = Job.FromIni(body); if (held.Id != id) throw Data.Invalid(L.FleetClearanceStorageInvalid); }
1196
string owner = Data.Text(ini,FleetSection, ClearanceOwnerKey + n), id = Data.Text(ini,FleetSection, ClearanceJobKey + n); Job held = null;
1197
if (ini.ContainsKey(FleetSection, ClearanceDataKey + n)) { var body = new MyIni(); if (!body.TryParse(Data.Text(ini,FleetSection, ClearanceDataKey + n))) throw Data.Invalid(L.FleetClearanceStorageInvalid); held = Job.FromIni(body); if (held.Id != id) throw Data.Invalid(L.FleetClearanceStorageInvalid); }
1105
1198
else { Job job = planner.Find(id); if (job != null) held = Job.FromIni(job.ToIni()); }
1106
1199
if (held != null) { if (owner.Length == 0 || clearanceHolds.ContainsKey(held.Id)) throw Data.Invalid(L.FleetClearanceStorageInvalid); held.Owner = owner; clearanceHolds.Add(held.Id, held); }
1107
1200
}
1108
markerReceipts.Clear(); markerReceiptSet.Clear(); int receiptCount = (int)Data.ReadLong(ini, "fleet", "MarkerCount"); if (receiptCount < 0 || receiptCount > 128) throw Data.Invalid(L.FleetMarkerReceiptsInvalid);
1109
for (int n = 0; n < receiptCount; n++) { var receipt = Data.Text(ini,"fleet", "MarkerReceipt" + n); markerReceipts.Enqueue(receipt); markerReceiptSet.Add(receipt); }
1201
markerReceipts.Clear(); markerReceiptSet.Clear(); int receiptCount = (int)Data.ReadLong(ini, FleetSection, MarkerCountKey); if (receiptCount < 0 || receiptCount > 128) throw Data.Invalid(L.FleetMarkerReceiptsInvalid);
1202
for (int n = 0; n < receiptCount; n++) { var receipt = Data.Text(ini,FleetSection, MarkerReceiptKey + n); markerReceipts.Enqueue(receipt); markerReceiptSet.Add(receipt); }
1110
1203
foreach (var id in protectedOwners.Keys) P.Bus.ProtectPeer(id, 0); protectedOwners.Clear();
1111
int protectedCount = StoredCount(ini, "fleet", "ProtectedOwnerCount", 64); var protectedIds = new HashSet<string>();
1204
int protectedCount = StoredCount(ini, FleetSection, ProtectedOwnerCountKey, 64); var protectedIds = new HashSet<string>();
1112
1205
for (int n = 0; n < protectedCount; n++)
1113
1206
{
1114
var id = Data.Text(ini,"fleet", "ProtectedOwnerId" + n); var address = Data.ReadLong(ini, "fleet", "ProtectedOwnerAddress" + n);
1207
var id = Data.Text(ini,FleetSection, "ProtectedOwnerId" + n); var address = Data.ReadLong(ini, FleetSection, ProtectedOwnerAddressKey + n);
1115
1208
if (id.Length == 0 || address == 0 || !protectedIds.Add(id)) throw Data.Invalid(L.FleetStoredSizeInvalid);
1116
1209
ProtectOwner(id, address);
1117
1210
}
@@ -266,6 +266,18 @@ namespace AutoMiningScript
266
266
if(seen.Count==16)seen.RemoveAt(0);
267
267
seen.Add(new SeenPoint {Position=point,Velocity=velocity,At=p.Now,Entity=entity});
268
268
}
269
// Cached hits can reject an occupied waypoint, never certify an unseen route.
270
// Move still scans the whole hull envelope before authorising motion to it.
271
public bool KnownPointBlocked(Vector3D target)
272
{
273
double radius=h.Radius+.5;
274
foreach(var item in seen)
275
{
276
var age=p.Now-item.At;if(age>(Data.LengthSquared(item.Velocity)>.01?3:30))continue;
277
if(Data.DistanceSquared(target,item.Position+item.Velocity*age)<radius*radius)return true;
278
}
279
return false;
280
}
269
281
double KnownClearance(Vector3D position,Vector3D direction,double distance)
270
282
{
271
283
foreach(var item in seen)
@@ -30,6 +30,9 @@ namespace AutoMiningScript
30
30
int retry,returnIndex=-1,sampleConfirm,entryProbe;
31
31
double entryNearest,entryFarthest;
32
32
double lastProgress;
33
double dockJoinDepth=-1,dockJoinAt,dockJoinBest,trafficDelay;
34
int dockJoinAttempt;
35
long dockJoinPort;
33
36
string sampleOre="",watchdogSession="";
34
37
bool watchdogReady;
35
38
long cancelledThrough=-1;
@@ -68,7 +71,7 @@ namespace AutoMiningScript
68
71
bool ReturningToDock => state==FlightState.Returning || state==FlightState.Holding || state==FlightState.DockAlign || state==FlightState.DockApproach || state==FlightState.DockRetreat;
69
72
void Set(FlightState next,string text,bool keepRoute=false)
70
73
{
71
if(state!=next) {P.Log(L.State(next)+": "+text);state=next;stateSince=P.Now;progressAt=P.Now;lastProgress=next==FlightState.Transit?double.MaxValue:0;waitingAligning=false;if(!keepRoute)flight.ResetRoute();if(next==FlightState.Align) {entryProbe=0;entryNearest=double.MaxValue;entryFarthest=double.MinValue;}if(next==FlightState.DockAlign || next==FlightState.DockApproach) {lastProgress=double.MaxValue;stableSince=-1;}}
74
if(state!=next) {P.Log(L.State(next)+": "+text);state=next;stateSince=P.Now;progressAt=P.Now;trafficDelay=0;lastProgress=next==FlightState.Transit?double.MaxValue:0;waitingAligning=false;if(!keepRoute)flight.ResetRoute();if(next==FlightState.Align) {entryProbe=0;entryNearest=double.MaxValue;entryFarthest=double.MinValue;}if(next==FlightState.DockAlign || next==FlightState.DockApproach) {lastProgress=double.MaxValue;stableSince=-1;}if(next==FlightState.Returning || next==FlightState.Servicing || next==FlightState.Departing)dockJoinDepth=-1;}
72
75
reason=text;
73
76
}
74
77
public override void Tick(double dt)
@@ -160,8 +163,13 @@ namespace AutoMiningScript
160
163
}
161
164
else if(near)flight.Move(Position,dock.PointVelocity(Position),ship.Controller.WorldMatrix.Forward,ship.Controller.WorldMatrix.Up,0,.3,false,dt);
162
165
else flight.Hold(dt);
163
stableSince=-1;progressAt=P.Now;reason=L.F(L.MinerTrafficYield,flight.TrafficPeer);
166
// Short encounters earn bounded grace. Repeated side-steps are not progress
167
// toward the task and must not erase its stalled-navigation watchdog.
168
double grace=Data.Min(Data.Max(0,dt),Data.Max(0,10-trafficDelay));trafficDelay+=grace;progressAt+=grace;
169
stableSince=-1;reason=L.F(L.MinerTrafficYield,flight.TrafficPeer);
164
170
if(flight.TrafficAvoid && flight.Problem.Length>0)reason+=" / "+flight.Problem;
171
if(SurveyTransitResult(dt,false))return true;
172
if(state==FlightState.DockAlign || state==FlightState.DockApproach)CheckDockTimeout();
165
173
return true;
166
174
}
167
175
void DockedTick(double dt)
@@ -231,26 +239,29 @@ namespace AutoMiningScript
231
239
var travel=Data.Unit(target-Position,f);
232
240
var cruiseUp=Data.LengthSquared(gravity)>0.001?Data.Unit(-gravity,u):u;
233
241
var distance=Data.Distance(Position,target);
234
// Local detour motion is not progress toward an observation entry.
235
// Keep the window alive only after gaining two metres on its best approach.
236
if(surveying && distance<lastProgress-2) {lastProgress=distance;progressAt=P.Now;}
237
242
var face=surveying || distance>ship.Radius*3?travel:f;
238
243
if(!surveying && !flight.CanSupport(face,Data.Perpendicular(face,cruiseUp),0.3)) {BeginReturn(L.MinerTargetAttitudeThrustLow);return;}
239
244
// Observation rays may point sideways from the arrival path. Keep the
240
245
// travel camera facing the actual leg (including detours) until stopped.
241
246
var arrived=flight.Move(target,Data.Zero,face,Data.Perpendicular(face,cruiseUp),P.Config.Number("Flight","CruiseSpeed",15,1,80),surveying?1:.4,true,dt,true);
242
// Safety rays are also real surface observations. An asteroid in front
243
// of an observation waypoint must become a probe, not a route failure.
244
if(surveying && flight.SurfaceHit!=null)
245
{var report=flight.SurfaceHit;report.JobId=job.Id;CompleteJob(report);return;}
247
if(SurveyTransitResult(dt,arrived))return;
246
248
reason=L.F(L.MinerDockNavigationProgress,L.MinerTravelTaskEntry,distance,Data.Length(ship.Controller.GetShipVelocities().LinearVelocity),flight.AttitudeError*180/Math.PI);
247
249
if(flight.Problem.Length>0)reason=flight.Problem+" / "+reason;
248
250
RecordBreadcrumb();
249
if(surveying && !arrived && P.Now-progressAt>45)
250
{flight.Hold(dt);FinishBlocked(L.MinerSurveyEntryStalled);return;}
251
251
if(!surveying && flight.Blocked && P.Now-stateSince>45) {FinishBlocked(L.MinerRouteBlocked+" / "+flight.Problem);return;}
252
252
if(arrived)Set(surveying?FlightState.Survey:FlightState.Align,L.MinerTaskEntryReached);
253
253
}
254
bool SurveyTransitResult(double dt,bool arrived)
255
{
256
if(state!=FlightState.Transit || job==null || job.Kind!=JobKind.Survey)return false;
257
double distance=Data.Distance(Position,job.Entry);
258
// Detours/yields must actually advance toward the observation entry.
259
if(distance<lastProgress-2){lastProgress=distance;progressAt=P.Now;trafficDelay=0;}
260
// A real safety-camera surface observation takes precedence over timeout.
261
if(flight.SurfaceHit!=null){var hit=flight.SurfaceHit;hit.JobId=job.Id;CompleteJob(hit);return true;}
262
if(arrived || P.Now-progressAt<=45)return false;
263
flight.Hold(dt);var report=MakeReport("Blocked","",L.MinerSurveyEntryStalled);report.EntryUnreachable=true;CompleteJob(report);return true;
264
}
254
265
void Align(double dt)
255
266
{
256
267
Vector3D f,u;var target=TipTarget(-Approach,out f,out u);
@@ -557,18 +568,47 @@ namespace AutoMiningScript
557
568
}
558
569
// An on-axis ship scans its way to the entry plane without braking to
559
570
// a stop there. Off-axis arrivals first join the guide point normally.
560
var target=onAxis?contact:contact+normal*DockDistance;
571
var target=contact;
572
if(!onAxis && !DockJoinTarget(contact,normal,out target))
573
{flight.Hold(dt);reason=L.MinerDockAlignTimeout;CheckDockTimeout();return;}
561
574
// Before entry, even an axial route may be behind the mining cameras.
562
575
// Travel facing the route; DockApproach acquires the mating pose at entry.
563
if(flight.Move(target,dock.PointVelocity(target),f,TravelUp(Data.Unit(target-Position,f)),10,1,true,dt,faceTravel:true,allowDetour:!onAxis&&Data.Length(offset)>DockDistance+ship.Radius))
576
if(flight.Move(target,dock.PointVelocity(target),f,TravelUp(Data.Unit(target-Position,f)),10,1,true,dt,faceTravel:true,allowDetour:!onAxis))
564
577
{Set(FlightState.DockApproach,L.MinerDockFollowAxis);return;}
565
578
reason=DockNavigationReason(L.MinerDockOuterEntry,target);
566
579
CheckDockProgress(target);
567
580
}
581
bool DockJoinTarget(Vector3D contact,Vector3D normal,out Vector3D target)
582
{
583
if(dockJoinDepth<0 || dockJoinPort!=dock.ConnectorId)
584
{dockJoinPort=dock.ConnectorId;dockJoinDepth=DockDistance;dockJoinAttempt=0;dockJoinAt=P.Now;dockJoinBest=double.MaxValue;}
585
target=contact+normal*dockJoinDepth;
586
if(dockJoinAttempt>6)return false;
587
double distance=Data.Distance(Position,target);
588
if(distance<dockJoinBest-.5){dockJoinBest=distance;dockJoinAt=P.Now;}
589
if(!flight.KnownPointBlocked(target) && P.Now-dockJoinAt<=8)return true;
590
// The outer guide is an optional axis-joining point, not a required stop.
591
// Keep a chosen depth in the connector frame until capture, new evidence,
592
// or sustained lack of progress; do not chase a point inside a parked ship.
593
double step=Data.Max(10,ship.Radius*3),minimum=Data.Max(5,ship.Radius*2+1);
594
while(++dockJoinAttempt<=6)
595
{
596
double depth=DockDistance+(dockJoinAttempt%2==1?-1:1)*((dockJoinAttempt+1)/2)*step;
597
if(depth<minimum)continue;
598
var candidate=contact+normal*depth;if(flight.KnownPointBlocked(candidate))continue;
599
dockJoinDepth=depth;target=candidate;dockJoinAt=P.Now;dockJoinBest=Data.Distance(Position,target);
600
flight.ResetRoute();lastProgress=double.MaxValue;return true;
601
}
602
return false;
603
}
568
604
void CheckDockProgress(Vector3D target,string timeout=null)
569
605
{
570
606
var metric=Data.Distance(Position,target)+flight.AttitudeError*5;
571
if(metric<lastProgress-0.2) {lastProgress=metric;progressAt=P.Now;}
607
if(metric<lastProgress-0.2) {lastProgress=metric;progressAt=P.Now;trafficDelay=0;}
608
CheckDockTimeout(timeout);
609
}
610
void CheckDockTimeout(string timeout=null)
611
{
572
612
if(P.Now-progressAt>60)
573
613
{
574
614
var why=(timeout??L.MinerDockAlignTimeout)+" / "+reason;
@@ -615,6 +655,14 @@ namespace AutoMiningScript
615
655
var captureDistance=calibrated?3:Data.Max(6,ship.Radius*2);
616
656
var synchronizing=angle>=2 || upAngle>=2 || (depth<=captureDistance && (stableSince<0 || P.Now-stableSince<2));
617
657
if(synchronizing)target+=normal*Data.Max(0,depth);
658
if(calibrated)
659
{
660
// A saved mating point can precede magnetic capture. Keep the flight
661
// target beyond the bounded capture segment so its direction cannot
662
// reverse at that old point. Connectable is checked before this limit.
663
if(depth<-.35) {flight.Hold(dt);AbortDock(L.MinerDockCaptureLimit);return;}
664
if(closing && !synchronizing)target-=normal*.6;
665
}
618
666
// Keep the pose-acquisition distance; only the measured final metre
619
667
// needs constant capture speed. First contact stays conservative.
620
668
var slowDistance=calibrated?1:captureDistance;
@@ -685,7 +733,7 @@ namespace AutoMiningScript
685
733
t.ServiceFlags=resources.ServiceFlags;
686
734
t.NextAfter=continuing && running && !manualStop && nextReport!=null?nextReport.JobId:"";
687
735
t.HasRoute=flight.HasRoute;t.RouteTarget=flight.RouteTarget;
688
if(ship.Controller!=null) {t.Velocity=ship.Controller.GetShipVelocities().LinearVelocity;t.Forward=ship.Controller.WorldMatrix.Forward;t.Up=ship.Controller.WorldMatrix.Up;t.Gravity=Data.Length(ship.Controller.GetNaturalGravity());}
736
if(ship.Controller!=null) {var motion=ship.Controller.GetShipVelocities();t.AngularVelocity=motion.AngularVelocity;t.RotationRadius=Data.Distance(Position,ship.Controller.CenterOfMass);t.Velocity=FlightController.PointVelocity(motion.LinearVelocity,motion.AngularVelocity,Position,ship.Controller.CenterOfMass);t.Forward=ship.Controller.WorldMatrix.Forward;t.Up=ship.Controller.WorldMatrix.Up;t.Gravity=Data.Length(ship.Controller.GetNaturalGravity());t.HasBodyBounds=healthy;t.BodyMin=ship.BodyMin;t.BodyMax=ship.BodyMax;}
689
737
t.BrakeResponse=1/Data.Min(1.6,P.Config.Number("Flight","VelocityGain",1.6,.2,5));
690
738
if(ship.Connector!=null) {t.ConnectorId=ship.Connector.EntityId;if(ship.Connector.OtherConnector!=null)t.PeerConnectorId=ship.Connector.OtherConnector.EntityId;}
691
739
var ores=new List<string>();foreach(var ore in resources.Ores)if(ore.Value>0)ores.Add(ore.Key+":"+ore.Value.ToString("F1",Data.Culture));t.OreSummary=string.Join(", ",ores);return t;
@@ -59,6 +59,7 @@ namespace AutoMiningScript
59
59
readonly Dictionary<string,Job> jobsById=new Dictionary<string,Job>();
60
60
readonly Dictionary<string,List<Job>> surfaceCells=new Dictionary<string,List<Job>>();
61
61
readonly Dictionary<string,MiningRegion> regionsById=new Dictionary<string,MiningRegion>();
62
readonly HashSet<string> unreachableEntries=new HashSet<string>();
62
63
int reserved,serial,advanceCursor,advanceJobCount,advanceRegionCount;
63
64
MiningRegion advancing;
64
65
bool advancePending,advanceBlocked;
@@ -117,10 +118,25 @@ namespace AutoMiningScript
117
118
}
118
119
public int JobPriority(Job j)
119
120
{
121
if(SkipUnreachable(j))return -1;
120
122
if(j.Kind==JobKind.Manual)return 100;if(j.Kind==JobKind.Survey)return 0;
121
123
var r=Region(j.RegionId);var ore=r!=null&&r.Patch?r.Ore:j.TargetOre;
122
124
return ore.Length>0&&PreferredOre(ore).Length==0?-1:10+OrePriority(ore);
123
125
}
126
void IndexUnreachable(Job j)
127
{
128
if(!j.EntryUnreachable || j.Kind!=JobKind.Survey || j.Outcome!="Blocked")return;
129
unreachableEntries.Add(EntryKey(j));
130
}
131
string EntryKey(Job j) => j.RegionId+"|"+Data.Vector(j.Entry);
132
bool SkipUnreachable(Job j)
133
{
134
// An inaccessible observation position does not invalidate another
135
// miner's active ray, a real discovery, or a nearby observation.
136
if(j.Kind!=JobKind.Survey || j.Owner.Length>0 || unreachableEntries.Count==0 || !unreachableEntries.Contains(EntryKey(j)))return false;
137
if(!Finished(j)) {Dirty(j);j.Outcome="Blocked";PendingCount--;QueueAdvance(j.RegionId);}
138
return true;
139
}
124
140
static string MergeOres(string first,string second)
125
141
{
126
142
var result=new List<string>();var seen=new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -287,6 +303,8 @@ namespace AutoMiningScript
287
303
}
288
304
}
289
305
j.Outcome=report.Outcome;
306
j.EntryUnreachable=report.EntryUnreachable && j.Kind==JobKind.Survey && report.Outcome=="Blocked";
307
IndexUnreachable(j);
290
308
if(Finished(j)) {PendingCount--;SetOwner(j,"");QueueAdvance(j.RegionId);}
291
309
}
292
310
void QueueAdvance(string id) {if(id.Length>0&&advanceQueued.Add(id)) advanceQueue.Enqueue(id);}
@@ -309,7 +327,7 @@ namespace AutoMiningScript
309
327
int index=advanceCursor++;
310
328
if(index<Jobs.Count)
311
329
{
312
Job j=Jobs[index];if(j.RegionId==r.Id) {if(!Finished(j))advancePending=true;if(j.Outcome=="Blocked"||j.Outcome=="InvalidSample")advanceBlocked=true;}
330
Job j=Jobs[index];if(j.RegionId==r.Id) {SkipUnreachable(j);if(!Finished(j))advancePending=true;if(j.Outcome=="Blocked"||j.Outcome=="InvalidSample")advanceBlocked=true;}
313
331
}
314
332
else if(r.Search)
315
333
{
@@ -421,11 +439,11 @@ namespace AutoMiningScript
421
439
}
422
440
void ClearLoadedTasks()
423
441
{
424
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();
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();
425
443
}
426
444
void RestoreJob(Job j)
427
445
{
428
Jobs.Add(j);jobsById.Add(j.Id,j);IndexSurface(j);
446
Jobs.Add(j);jobsById.Add(j.Id,j);IndexSurface(j);IndexUnreachable(j);
429
447
if(!Finished(j)){PendingCount++;if(j.Owner.Length>0)OwnedJobs[j.Owner]=j;}
430
448
}
431
449
void RestoreRegion(MiningRegion r)
@@ -1,5 +1,5 @@
1
1
{
2
"MinerAutomaticCalibrationComplete": "Dock confirmed; auto calibration complete",
3
"MinerWaitingAttitudeAlignment": "At waiting point; matching berth attitude",
4
"MinerAutoCalibratePending": "Awaiting connection for auto calibration"
2
"MinerAutomaticCalibrationComplete": "Docked; auto calibrated",
3
"MinerWaitingAttitudeAlignment": "At hold point; match berth pose",
4
"MinerAutoCalibratePending": "Await dock for auto calibration"
5
5
}
@@ -1,24 +1,24 @@
1
1
{
2
"CameraDisabled": "'{0}' off; enable or rescan docked",
3
"CameraDamaged": "'{0}' damaged; cannot scan",
4
"CameraUnpowered": "'{0}' inactive; check power",
5
"CameraRaycastDisabled": "'{0}' raycast off; check scripts",
6
"CameraView": "'{0}' view: pitch {1:F1}/yaw {2:F1} deg, limit +/-{3:F1}; {4:F1} m, ray {5}/18",
7
"CameraDistanceLimit": "'{0}': {1:F1} m exceeds {2:F1} m limit",
8
"CameraCharging": "'{0}' charging: {1:F1}/{2:F1} m, ETA {3:F1}s",
9
"CameraSelfHit": "'{0}' hit own ship; trying another lens",
2
"CameraDisabled": "'{0}' OFF; enable/rescan docked",
3
"CameraDamaged": "'{0}' broken; no scan",
4
"CameraUnpowered": "'{0}' OFF; check power",
5
"CameraRaycastDisabled": "'{0}' raycast OFF; check scripts",
6
"CameraView": "'{0}': pitch {1:F1}/yaw {2:F1}deg, +/-{3:F1} limit; {4:F1}m, ray {5}/18",
7
"CameraDistanceLimit": "'{0}': {1:F1}m > {2:F1}m limit",
8
"CameraCharging": "'{0}' charge {1:F1}/{2:F1}m, ETA {3:F1}s",
9
"CameraSelfHit": "'{0}' self-hit; try another",
10
10
"CameraMissing": "No local camera; check group",
11
"CameraInventory": "Cameras: {0} found / {1} working / {2} ray-ready",
12
"CameraRay": "Scan direction {0}, ray {1}/18, age {2:F1} s",
13
"CameraScanInactive": "No recent scan request",
11
"CameraInventory": "Cameras: {0} total/{1} active/{2} ray-ready",
12
"CameraRay": "Scan {0}, ray {1}/18, age {2:F1}s",
13
"CameraScanInactive": "No recent scan",
14
14
"CameraFront": "forward",
15
15
"CameraRear": "backward",
16
16
"CameraLeft": "left",
17
17
"CameraRight": "right",
18
18
"CameraUp": "up",
19
19
"CameraDown": "down",
20
"CameraCoverage": "Combined: {0}/18 regions, {1} cameras",
21
"CameraRegionUnavailable": "Region {0}/18 unseen; {1} cameras checked",
22
"CameraObstacle": "Detected {0}, hull clearance {1:F1} m, scan region {2}/18",
23
"CameraObstacleUnknown": "Detected {0}; invalid hit position, holding brakes"
20
"CameraCoverage": "Combined: {0}/18 areas, {1} cameras",
21
"CameraRegionUnavailable": "Area {0}/18 unseen; {1} cameras checked",
22
"CameraObstacle": "{0}: hull clearance {1:F1}m, ray {2}/18",
23
"CameraObstacleUnknown": "{0}: bad hit position; braking"
24
24
}
@@ -8,5 +8,6 @@
8
8
"MinerDockPrecision": "接口横向偏移 {0:F2} m / 角速度差 {1:F2}°/s",
9
9
"MinerDockDirectControl": "直接对接:扫描关闭,保留近距让行 / 指令速度 {0:F2} m/s",
10
10
"MinerDockAngularSync": "同步角速度,差值 {0:F2}°/s",
11
"MinerDockAlignmentDrift": "偏离接近点或连接器轴线,向外退避后重新申请泊位"
11
"MinerDockAlignmentDrift": "偏离接近点或连接器轴线,向外退避后重新申请泊位",
12
"MinerDockCaptureLimit": "接口未捕获,已达接近限位,退出重试"
12
13
}