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

AutoMiningScript

【SpaceEngineer】全自动挖矿脚本

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

SpaceEngineersModDev/AutoMiningScript

修复实机探索续派、船体避让与对接捕获

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

代码差异

19 个文件 +1084 -406
Modified AutoMiningScript.Tests/DockingNavigationTests.cs +212 -9
@@ -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]
Added AutoMiningScript.Tests/FleetHullTrafficTests.cs +143 -0
@@ -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 }
Modified AutoMiningScript.Tests/FleetTrafficTests.cs +1 -1
@@ -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 {
Added AutoMiningScript.Tests/UnreachableSurveyTests.cs +133 -0
@@ -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 }
Modified AutoMiningScript/Core.cs +13 -2
@@ -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 }
Modified AutoMiningScript/FleetController.cs +213 -120
@@ -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 }
Modified AutoMiningScript/Flight.cs +12 -0
@@ -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)
Modified AutoMiningScript/MinerController.cs +63 -15
@@ -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;
Modified AutoMiningScript/Planning.cs +21 -3
@@ -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)
Modified Localization/automatic.en-US.json +3 -3
@@ -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 }
Modified Localization/camera.en-US.json +15 -15
@@ -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 }
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
@@ -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 }
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