using System; using System.Collections.Generic; using Sandbox.ModAPI.Ingame; using VRage.Game.ModAPI.Ingame.Utilities; using VRageMath; namespace AutoMiningScript { public partial class Program { public sealed class MinerController : RoleLogic { readonly ShipHardware ship; readonly FlightController flight; readonly Resources resources; readonly List breadcrumbs=new List(); readonly Dictionary sample=new Dictionary(); readonly HashSet discovered=new HashSet(); readonly List view=new List(); Job job; JobReport nextReport; bool continuing; double nextReportAt; DockFrame home,dock; FlightState state=FlightState.Boot; string reason="",lane="",laneKind="",completion=""; bool healthy,calibrated,running,manualStop,pausedAfterRetreat,leaseConfirmed,insideBore,invalidSample; Vector3D calibratedPosition,calibratedForward,calibratedUp,abortPoint,abortVelocity; double nextTelemetry,nextResources,nextRequest,nextSample,nextHeartbeat,stateSince,stableSince=-1,progressAt,sampleDepth,healthAt,abortAt,watchdogAt=-100; int retry,returnIndex=-1,sampleConfirm,entryProbe; double entryNearest,entryFarthest; double lastProgress; string sampleOre="",watchdogSession=""; bool watchdogReady; long cancelledThrough=-1; long fleetEpoch; bool canceling; bool dockInitialized,waitingAligning; double nextInitialization; public MinerController(Program p):base(p) { ship=new ShipHardware(p);flight=new FlightController(p,ship);resources=new Resources(p,ship); string error;healthy=ship.Scan(out error);ship.Release(); Set(healthy?(Connected?FlightState.Docked:FlightState.Paused):FlightState.Fault,healthy?L.MinerWaitCalibrationTask:error); } bool Connected => ship.Connector!=null && ship.Connector.Status==MyShipConnectorStatus.Connected; Vector3D Position => ship.Controller==null?P.Me.GetPosition():ship.Controller.GetPosition(); double HomeDistance => home==null?0:Data.Distance(Position,home.Center); double BudgetDistance { get { if(home==null)return HomeDistance; var speed=P.Config.Number("Flight","CruiseSpeed",15,1,80); var time=Data.InterceptTime(home.Center-Position,home.Velocity,speed); return time<0?10000000:Math.Max(HomeDistance,time*speed); } } double Approach => P.Config.Number("Mining","ApproachDistance",20,5,200)+ship.Radius; double DockDistance => Math.Max(P.Config.Number("Dock","ApproachDistance",30,10,300),ship.Radius*2); bool InHole => job!=null && job.Kind!=JobKind.Survey && (insideBore || state==FlightState.Drilling || state==FlightState.Retreat); bool Idle => state==FlightState.Paused || state==FlightState.Boot || state==FlightState.Docked || state==FlightState.Servicing || state==FlightState.Ready; void Set(FlightState next,string text,bool keepRoute=false) { if(state!=next) {P.Log(L.State(next)+": "+text);state=next;stateSince=P.Now;progressAt=P.Now;lastProgress=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;}} reason=text; } public override void Tick(double dt) { if(P.Now>=nextHeartbeat) {Heartbeat();nextHeartbeat=P.Now+0.5;} if(P.Now>=nextTelemetry) {Publish();nextTelemetry=P.Now+1;} if(!healthy) { if(ship.Controller!=null && ship.Controller.IsUnderControl && !Connected)return; if(P.Now>=nextInitialization && P.HasBudget(0.3)) { nextInitialization=P.Now+5;string error;ship.Release();healthy=ship.Scan(out error);dockInitialized=false; if(healthy) {resources.ResetInventoryScan();nextResources=0;Set(Connected?FlightState.Servicing:FlightState.Paused,L.MinerHardwareRescanned);} else reason=error; } if(!healthy)return; } if(P.Now>=healthAt) { string error;healthAt=P.Now+1; if(!ship.Healthy(out error)) {Emergency(error);return;} } if(P.Now>=nextResources) { nextResources=P.Now+0.5;resources.Update(0.5,state,BudgetDistance,job==null || job.Kind==JobKind.Survey?0:job.Progress); } ship.UpdateDepartureMass(); if(!Connected)dockInitialized=false; else if(!dockInitialized && P.Config.Flag("Hardware","AutoCalibrate",true))CalibrateDock(true); if(ship.Controller.IsUnderControl && !Connected) { if(state!=FlightState.Manual) {ship.Release();running=false;manualStop=true;Set(FlightState.Manual,L.MinerManualControlResume);} return; } if(state==FlightState.Manual) return; if(Connected) {DockedTick(dt);return;} if(state==FlightState.Fault) {flight.Hold(dt);return;} if(!manualStop && Idle && (calibrated || P.Config.Flag("Hardware","AutoCalibrate",true)))BeginReturn(L.MinerAutomaticReturn); if(canceling && state==FlightState.Paused && !manualStop)BeginReturn(L.MinerTaskCancelledReturn); if(P.Config.Flag("Watchdog","Enabled",false) && (!watchdogReady || P.Now-watchdogAt>3) && running) { BeginReturn(L.MinerWatchdogHandshakeMissing); } if(Idle) {flight.Hold(dt);return;} string returnReason; if(state!=FlightState.Returning && state!=FlightState.Holding && state!=FlightState.DockAlign && state!=FlightState.DockApproach && state!=FlightState.DockRetreat && state!=FlightState.Retreat && resources.MustReturn(out returnReason))BeginReturn(returnReason); if(home!=null && P.Now-home.ReceivedAt>5) { if(state==FlightState.DockApproach || state==FlightState.DockAlign || state==FlightState.Departing)AbortDock(L.MinerBaseTimeoutAbortDock); else if(state!=FlightState.Returning && state!=FlightState.Holding && state!=FlightState.DockRetreat && state!=FlightState.Retreat)BeginReturn(L.MinerBaseTimeout); } switch(state) { case FlightState.Departing: Departure(dt);break; case FlightState.Transit: Transit(dt);break; case FlightState.Survey: Survey(dt);break; case FlightState.Align: Align(dt);break; case FlightState.Drilling: Drill(dt);break; case FlightState.Retreat: Retreat(dt);break; case FlightState.Returning: ReturnHome(dt);break; case FlightState.Holding: Waiting(dt);break; case FlightState.DockAlign: DockAlign(dt);break; case FlightState.DockApproach: DockApproach(dt);break; case FlightState.DockRetreat: DockRetreat(dt);break; } } void DockedTick(double dt) { if(state!=FlightState.Servicing && state!=FlightState.Docked && state!=FlightState.Ready && state!=FlightState.Fault) { ReleaseLane();ship.Release();retry=0;Set(FlightState.Servicing,L.MinerConnectedService); } if(dock!=null && ship.Connector.OtherConnector!=null && dock.ConnectorId!=ship.Connector.OtherConnector.EntityId) {ship.Release();running=false;Set(FlightState.Fault,L.MinerWrongDock);return;} flight.Release();resources.Service(dt); if(canceling) {job=null;canceling=false;insideBore=false;leaseConfirmed=false;breadcrumbs.Clear();discovered.Clear();P.Save();} if(state==FlightState.Fault)return; if(resources.ScanProblem.Length>0) {reason=resources.ScanProblem;return;} if(!running || manualStop || job==null) {reason=resources.DepartureReady?L.MinerDockedStandby:resources.DepartureWaitReason;return;} if(!calibrated) {reason=P.Config.Flag("Hardware","AutoCalibrate",true)?L.MinerAutoCalibratePending:L.MinerRunCalibrate;return;} if(!leaseConfirmed) {reason=L.MinerWaitTaskReconfirmation;return;} if(home==null || P.Now-home.ReceivedAt>5) {reason=L.MinerWaitBaseTelemetry;return;} if(!resources.DepartureReady) {reason=resources.DepartureWaitReason;return;} string why; var sortieDepth=job.Kind==JobKind.Survey?0:Math.Min(job.Depth,job.Progress+P.Config.Number("Mining","SortieDepthBudget",15,1,100)); if(!resources.ReadyForSortie(Data.Distance(Position,job.Entry),sortieDepth,out why)) {reason=why;return;} if(P.Config.Flag("Watchdog","Enabled",false) && (!watchdogReady || P.Now-watchdogAt>3)) {reason=L.MinerRunWatchdogArm;return;} if(!flight.CanSupport(ship.Controller.WorldMatrix.Forward,ship.Controller.WorldMatrix.Up,0.3)) {reason=flight.Problem;return;} Request("DEPART_REQUEST"); var permit=DockProblem("depart"); if(permit.Length>0) {stableSince=-1;reason=permit;return;} if(stableSince<0)stableSince=P.Now; if(P.Now-stableSince<2)return; ship.PrepareFlight();ship.Connector.Disconnect(); if(!Connected) {breadcrumbs.Clear();returnIndex=-1;Set(FlightState.Departing,L.MinerDepartingLane);} } void Departure(double dt) { if(!DockValid()) {AbortDock(L.MinerDepartureWindowInvalid);return;} Vector3D f,u;var target=DockTarget(Math.Max(DockDistance,dock.LaneClearance+ship.Radius+2),out f,out u,false); flight.SetDockContact(dock.GridId,dock.Position,Math.Max(1.5,P.Me.CubeGrid.GridSize)); if(flight.Move(target,dock.PointVelocity(target),f,u,2,0.7,true,dt)) { ReleaseLane();flight.ClearDockContact();breadcrumbs.Add(Position);Set(FlightState.Transit,L.MinerTravelTaskEntry); return; } reason=flight.Problem.Length>0?flight.Problem:L.MinerDepartingLane; if(flight.AttitudeError>0.035)reason=L.F(L.MinerDepartureAttitude,flight.AttitudeError*180/Math.PI)+(flight.Problem.Length>0?" / "+flight.Problem:""); } MatrixD MiningOrientation() { var local=Data.Frame(Data.Zero,ship.DrillForwardLocal,ship.DrillUpLocal); return MatrixD.Transpose(local)*Data.Frame(Data.Zero,job.Direction,job.Up); } Vector3D DrillTip() => ship.Controller==null?Position:Vector3D.Transform(ship.DrillTipLocal,ship.Controller.WorldMatrix); Vector3D TipTarget(double depth,out Vector3D f,out Vector3D u) { var orientation=MiningOrientation();f=orientation.Forward;u=orientation.Up; return job.Entry+job.Direction*depth-Vector3D.TransformNormal(ship.DrillTipLocal,orientation); } void Transit(double dt) { if(job==null) {BeginReturn(L.MinerNoTask);return;} Vector3D f,u,target; var surveying=job.Kind==JobKind.Survey; if(surveying) {target=job.Entry;f=job.Direction;u=job.Up;} else target=TipTarget(-Approach,out f,out u); var gravity=ship.Controller.GetNaturalGravity(); var travel=Data.Unit(target-Position,f); var cruiseUp=gravity.LengthSquared()>0.001?Data.Unit(-gravity,u):u; var distance=Data.Distance(Position,target); var face=surveying || distance>ship.Radius*3?travel:f; if(!surveying && !flight.CanSupport(face,Data.Perpendicular(face,cruiseUp),0.3)) {BeginReturn(L.MinerTargetAttitudeThrustLow);return;} // Observation rays may point sideways from the arrival path. Keep the // travel camera facing the actual leg (including detours) until stopped. 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); // Safety rays are also real surface observations. An asteroid in front // of an observation waypoint must become a probe, not a route failure. if(surveying && flight.SurfaceHit!=null) {var report=flight.SurfaceHit;report.JobId=job.Id;CompleteJob(report);return;} reason=L.F(L.MinerDockNavigationProgress,L.MinerTravelTaskEntry,distance,ship.Controller.GetShipVelocities().LinearVelocity.Length(),flight.AttitudeError*180/Math.PI); if(flight.Problem.Length>0)reason=flight.Problem+" / "+reason; RecordBreadcrumb(); if(flight.Blocked && P.Now-stateSince>45) {FinishBlocked(L.MinerRouteBlocked+" / "+flight.Problem);return;} if(arrived)Set(surveying?FlightState.Survey:FlightState.Align,L.MinerTaskEntryReached); } void Align(double dt) { Vector3D f,u;var target=TipTarget(-Approach,out f,out u); if(!flight.CanSupport(f,u,0.3)) {FinishBlocked(L.MinerDrillAttitudeUnsupported);return;} if(!flight.Move(target,Data.Zero,f,u,2,0.5,true,dt))return; if(!CheckBore()) {FinishBlocked(L.MinerDrillCoverageInsufficient);return;} // Confirm the intended voxel before disabling collision checks on the drilled axis. bool ready=false,voxel=false,obstructed=false; var relief=P.Config.Number("Mining","MaxEntryRelief",5,0.5,20); var offset=Data.Zero; var right=Data.Cross(job.Direction,job.Up); double halfWidth=P.Config.Number("Mining","FootprintWidth",6,0.5,100)*0.4,halfHeight=P.Config.Number("Mining","FootprintHeight",6,0.5,100)*0.4; if(entryProbe==1)offset=right*halfWidth; else if(entryProbe==2)offset=-right*halfWidth; else if(entryProbe==3)offset=job.Up*halfHeight; else if(entryProbe==4)offset=-job.Up*halfHeight; var test=job.Entry+offset+job.Direction*Math.Min(job.Depth,Math.Max(relief,job.Progress+3)); foreach(var camera in ship.Cameras) { if(!camera.IsWorking || camera.CubeGrid!=P.Me.CubeGrid || !camera.CanScan(test))continue; var hit=camera.Raycast(test);if(!hit.IsEmpty() && hit.EntityId==P.Me.CubeGrid.EntityId)continue; ready=true; if(!hit.IsEmpty() && hit.Type!=MyDetectedEntityType.Asteroid && hit.Type!=MyDetectedEntityType.Planet) {obstructed=true;break;} if(!hit.IsEmpty() && hit.HitPosition.HasValue && (hit.Type==MyDetectedEntityType.Asteroid || hit.Type==MyDetectedEntityType.Planet) && (job.EntityId==0 || hit.EntityId==job.EntityId)) { voxel=true;var height=Data.Dot(hit.HitPosition.Value-job.Entry,job.Direction);entryNearest=Math.Min(entryNearest,height);entryFarthest=Math.Max(entryFarthest,height);break; } } if(!ready) {reason=L.MinerWaitEntryScan;if(P.Now-stateSince>60)FinishBlocked(reason);return;} if(obstructed) {FinishBlocked(L.MinerEntryOccupied);return;} if(!voxel && job.Progress<=0) {FinishBlocked(L.MinerEntryVoxelUnconfirmed);return;} if(job.Progress<=0 && entryFarthest-entryNearest>relief) {FinishBlocked(L.MinerEntryReliefExcessive);return;} if(++entryProbe<5) {reason=L.F(L.MinerEntryScanProgress, entryProbe);return;} ship.SetSampling(true);ResetSample();sampleDepth=job.Progress;lastProgress=job.Progress;progressAt=P.Now; Set(FlightState.Drilling,L.MinerDrillingAxis); lastProgress=Data.Dot(DrillTip()-job.Entry,job.Direction);progressAt=P.Now; } bool CheckBore() { double width=P.Config.Number("Mining","FootprintWidth",6,0.5,100),height=P.Config.Number("Mining","FootprintHeight",6,0.5,100); // Project all eight ship-bound corners onto the drill frame, including controller offset. var local=Data.Frame(Data.Zero,ship.DrillForwardLocal,ship.DrillUpLocal);double x=0,y=0; for(int n=0;n<8;n++) { var corner=new Vector3D((n&1)==0?-ship.BodyHalfSize.X:ship.BodyHalfSize.X,(n&2)==0?-ship.BodyHalfSize.Y:ship.BodyHalfSize.Y,(n&4)==0?-ship.BodyHalfSize.Z:ship.BodyHalfSize.Z)-ship.DrillTipLocal; x=Math.Max(x,Math.Abs(Data.Dot(corner,local.Right)));y=Math.Max(y,Math.Abs(Data.Dot(corner,local.Up))); } return width>=2*x+0.4 && height>=2*y+0.4; } void Drill(double dt) { ship.SetSampling(true);ship.SetDrills(true); Vector3D f,u;var projected=Data.Dot(DrillTip()-job.Entry,job.Direction); job.Progress=Math.Max(job.Progress,Data.Clamp(projected,0,job.Depth));insideBore=projected>0; var target=TipTarget(job.Depth,out f,out u); if(!flight.CanSupport(f,u,0.3)) {BeginReturn(L.MinerMiningThrustLow);return;} var arrived=flight.Move(target,Data.Zero,f,u,P.Config.Number("Mining","DrillSpeed",0.5,0.05,1),0.15,false,dt); if(projected>lastProgress+0.1) {lastProgress=projected;progressAt=P.Now;} if(P.Now-progressAt>30) {completion="Blocked";BeginReturn(L.MinerDrillingStalled);return;} if(P.Now>=nextSample) { nextSample=P.Now+0.5; if(projected>=sampleDepth+5 || arrived)SampleOre(); } if(arrived) { if(sampleConfirm==1)return; job.Progress=job.Depth;completion=invalidSample && job.Kind==JobKind.Probe?"Blocked":"Complete";pausedAfterRetreat=false;Set(FlightState.Retreat,invalidSample?L.MinerInvalidSampleRetreat:L.MinerBoreCompleteRetreat); } } void ResetSample() { sample.Clear();foreach(var item in resources.Ores)sample[item.Key]=item.Value;sampleConfirm=0;sampleOre="";resources.ResetSampleValidation(); } void SampleOre() { if(!resources.SamplingValid) {reason=L.MinerSampleInvalid;invalidSample=true;sampleDepth=job.Progress;ResetSample();return;} var targets=job.TargetOre.Length>0?job.TargetOre:P.Config.Text("Mining","TargetOres","Iron,Nickel,Cobalt"); var found="";foreach(var raw in targets.Split(',')) { var ore=raw.Trim();double current,previous;resources.Ores.TryGetValue(ore,out current);sample.TryGetValue(ore,out previous); if(current-previous>=P.Config.Number("Mining","DetectionAmount",0.01,0.000001,1000)) {found=ore;break;} } if(found.Length>0) { sampleConfirm=sampleOre==found?sampleConfirm+1:1;sampleOre=found; if(sampleConfirm<2)return; if(discovered.Add(found))Report("Discovery",found,L.MinerOreConfirmed); } sampleDepth=job.Progress;ResetSample(); } void Retreat(double dt) { ship.SetDrills(false);ship.SetSampling(false); Vector3D f,u;var target=TipTarget(-Approach,out f,out u); if(flight.Move(target,Data.Zero,f,u,1,0.5,false,dt)) { insideBore=false; var result=completion;completion=""; if(!canceling) { if(result.Length>0) { var report=MakeReport(result,string.Join(",",discovered),reason); if(job.Kind!=JobKind.Manual && running && !manualStop && !pausedAfterRetreat) {CompleteJob(report);return;} P.Bus.Send("RESULT",P.Config.BaseId,report.ToIni(),true);job=null;leaseConfirmed=false;discovered.Clear(); } else Report("Paused","",reason); } if(pausedAfterRetreat) {running=false;Set(FlightState.Paused,L.MinerRetreatedPaused);} else {returnIndex=breadcrumbs.Count-1;Set(FlightState.Returning,L.MinerReturnVerifiedRoute);} } } void Survey(double dt) { if(job==null && continuing && nextReport!=null) { flight.Hold(dt); if(P.Now>=nextReportAt) {nextReportAt=P.Now+2;P.Bus.Send("RESULT",P.Config.BaseId,nextReport.ToIni(),true);Publish();} return; } if(job==null) {BeginReturn(L.MinerNoTask);return;} if(P.Now-stateSince>60) {FinishBlocked(L.MinerScanBudgetExceeded);return;} if(!flight.CanSupport(job.Direction,job.Up,0.3)) {FinishBlocked(L.MinerTargetAttitudeThrustLow);return;} // Transit already accepted the observation position. Brake and turn in // place; a tighter sideways correction must not gate the survey ray. if(!flight.Move(Position,Data.Zero,job.Direction,job.Up,0,0.5,false,dt)) {reason=flight.Problem.Length>0?flight.Problem:L.MinerWaitSurveyScan;return;} if(P.Now10) { if(breadcrumbs.Count>=128) {BeginReturn(L.MinerRouteBudgetFull);return;} breadcrumbs.Add(Position); } } void ReturnHome(double dt) { Request("DOCK_REQUEST"); // A restart near the assigned port must not send the ship back out to // the distant end of its old mining trail. if(calibrated && DockProblem("dock").Length==0 && Data.Distance(Position,dock.Predicted(P.Now).Translation)<=DockDistance+ship.Radius*2) {returnIndex=-1;Set(FlightState.DockAlign,L.MinerDockOuterEntry,true);DockAlign(dt);return;} if(returnIndex>=0) { // Blend nearly straight verified segments; sharp corners still require // braking. Each resulting motion continues to require fresh scans. if(returnIndex>0) { Vector3D leg=breadcrumbs[returnIndex]-Position,next=breadcrumbs[returnIndex-1]-breadcrumbs[returnIndex]; double look=Math.Max(2,ship.Controller.GetShipVelocities().LinearVelocity.Length()*1.5); if(leg.Length()0.98))returnIndex--; } var target=breadcrumbs[returnIndex];var f=Data.Unit(target-Position,ship.Controller.WorldMatrix.Forward);var up=TravelUp(f); if(flight.Move(target,Data.Zero,f,up,10,2,true,dt,true))returnIndex--; if(flight.Problem.Length>0)reason=flight.Problem;return; } if(home==null || P.Now-home.ReceivedAt>5) {reason=L.MinerBaseLostHold;flight.Hold(dt);return;} if(dock==null || P.Now-dock.ReceivedAt>1) {flight.Hold(dt);reason=home.Problem.Length>0?home.Problem:L.MinerWaitBerthPoint;return;} Set(FlightState.Holding,L.MinerTravelWaitingPoint); } Vector3D TravelUp(Vector3D f) { var gravity=ship.Controller.GetNaturalGravity();return Data.Perpendicular(f,gravity.LengthSquared()>0.001?-gravity:ship.Controller.WorldMatrix.Up); } void Waiting(double dt) { Request("DOCK_REQUEST"); if(dock==null || P.Now-dock.ReceivedAt>P.Config.Number("Dock","MaxTelemetryAge",0.5,0.1,2)) {stableSince=-1;flight.Hold(dt);reason=dock==null?L.MinerWaitBerthPoint:L.F(L.MinerDockPoseStale,Math.Max(0,P.Now-dock.ReceivedAt));return;} var permit=DockProblem("dock"); if(permit.Length==0) { Set(FlightState.DockAlign,L.MinerDockOuterEntry,true);DockAlign(dt);return; } var frame=dock.Predicted(P.Now); var local=dock.WaitingLocal.LengthSquared()>1?dock.WaitingLocal:new Vector3D(0,ship.Radius*2,-Math.Max(80,DockDistance*2)); Vector3D target=Vector3D.Transform(local,frame),f,u;DockTarget(DockDistance,out f,out u); var motion=ship.Controller.GetShipVelocities(); var distance=Data.Distance(Position,target); double relativeSpeed=(FlightController.PointVelocity(motion.LinearVelocity,motion.AngularVelocity,Position,ship.Controller.CenterOfMass)-dock.PointVelocity(target)).Length(); if(waitingAligning && distance>4) {waitingAligning=false;stableSince=-1;flight.ResetRoute();} if(!waitingAligning && distance<=2 && relativeSpeed<=0.3) {waitingAligning=true;flight.ResetRoute();} var openLeg=!waitingAligning && Data.Distance(Position,frame.Translation)>=Math.Max(DockDistance,dock.LaneClearance+ship.Radius); var reached=flight.Move(target,dock.PointVelocity(target),f,openLeg?TravelUp(Data.Unit(target-Position,f)):u,10,2,true,dt,openLeg); reached=reached && waitingAligning; stableSince=-1; if(reached)reason=permit; else { reason=L.F(L.MinerDockWaitingPoint,distance,relativeSpeed,flight.AttitudeError*180/Math.PI); if(waitingAligning)reason=L.MinerWaitingAttitudeAlignment+" / "+reason; if(flight.Problem.Length>0)reason=flight.Problem+" / "+reason; reason+=" / "+permit; } // A timeout adds context; it must not replace the actual navigation or grant failure. if(P.Now-stateSince>P.Config.Number("Dock","WaitBudgetSeconds",120,10,900))reason+=" / "+L.MinerDockWaitBudgetExceeded; } string DockProblem(string expectedKind) { if(home!=null && P.Now-home.ReceivedAt<=5 && home.Problem.Length>0)return home.Problem; if(dock==null)return L.MinerWaitBerthPoint; var age=Math.Max(0,P.Now-dock.ReceivedAt); if(age>P.Config.Number("Dock","MaxTelemetryAge",0.5,0.1,2))return L.F(L.MinerDockPoseStale,age); if(lane.Length==0)return L.MinerDockGrantMissing; if(laneKind!=expectedKind || (laneKind!="dock"&&laneKind!="depart"))return L.MinerDockGrantDirection; if(dock.Owner!=P.Config.Id)return L.MinerDockGrantOwner; if(dock.Token!=lane)return L.MinerDockGrantChanged; if(laneKind=="dock" && dock.Connected && !Connected)return L.MinerDockBerthOccupied; if(!dock.IsWindow(P.Config,P.Now))return L.F(L.MinerDockMotionLimit,dock.PointVelocity(dock.Position).Length(),dock.AngularVelocity.Length()*180/Math.PI,dock.Acceleration.Length()); return ""; } bool DockValid() => DockProblem(laneKind).Length==0; bool CalibrateDock(bool automatic) { if(!Connected || ship.Controller==null || !ship.Controller.IsFunctional || ship.Connector.OtherConnector==null)return false; var other=ship.Connector.OtherConnector; if(other.CubeGrid==P.Me.CubeGrid || (automatic && dock!=null && dock.ConnectorId!=0 && dock.ConnectorId!=other.EntityId))return false; MatrixD inverse=MatrixD.Transpose(other.WorldMatrix); Vector3D position=Vector3D.TransformNormal(Position-other.GetPosition(),inverse); Vector3D forward=Vector3D.TransformNormal(ship.Controller.WorldMatrix.Forward,inverse),up=Vector3D.TransformNormal(ship.Controller.WorldMatrix.Up,inverse); if(!Data.Finite(position.LengthSquared())||!Data.Finite(forward.LengthSquared())||!Data.Finite(up.LengthSquared()))return false; if(Math.Abs(forward.Length()-1)>0.05||Math.Abs(up.Length()-1)>0.05||Math.Abs(Data.Dot(forward,up))>0.05)return false; calibratedPosition=position;calibratedForward=forward;calibratedUp=up;calibrated=true;dockInitialized=true; P.Log(automatic?L.MinerAutomaticCalibrationComplete:L.MinerDockCalibrationComplete);P.Save();return true; } Vector3D DockTarget(double distance,out Vector3D f,out Vector3D u,bool mating=true) { var frame=dock.Predicted(P.Now);f=Vector3D.TransformNormal(calibratedForward,frame);u=Vector3D.TransformNormal(calibratedUp,frame); if(!calibrated) {f=ship.Controller.WorldMatrix.Forward;u=ship.Controller.WorldMatrix.Up;} if(mating && ship.ConnectorForwardLocal.LengthSquared()>0.5 && ship.ConnectorUpLocal.LengthSquared()>0.5) { // Use the actual connector mount: mating normals oppose each other, // and connector up vectors agree. Calibration supplies centre spacing, // not the small lateral/angular error of a manually connected ship. var saved=Data.Frame(Data.Zero,calibratedForward,calibratedUp); // Without a saved contact distance, follow the connector axis until // the game reports Connectable. Lock before reaching either centre. double spacing=calibrated?Math.Max(0,-(calibratedPosition+Vector3D.TransformNormal(ship.ConnectorLocal,saved)).Z):0; var pose=FlightController.ReferenceOrientationForConnector(ship.ConnectorForwardLocal,ship.ConnectorUpLocal,frame.Forward,frame.Up); f=pose.Forward;u=pose.Up; return FlightController.ReferencePositionForPoint(frame.Translation+frame.Forward*(spacing+distance),ship.ConnectorLocal,pose); } return Vector3D.Transform(calibratedPosition,frame)+frame.Forward*distance; } void DockAlign(double dt) { if(!calibrated && !P.Config.Flag("Hardware","AutoCalibrate",true)) {running=false;Set(FlightState.Paused,L.MinerUncalibratedDockManually);return;} var permit=DockProblem("dock");if(permit.Length>0) {AbortDock(permit);return;} Vector3D f,u;Vector3D contact=DockTarget(0,out f,out u),normal=dock.Predicted(P.Now).Forward; Vector3D offset=Position-contact,lateral=offset-normal*Data.Dot(offset,normal); if(!flight.CanSupport(f,u,0.3)) {AbortDock(L.MinerDockAttitudeThrustLow);return;} var depth=Data.Dot(offset,normal); // Capture the approach axis where the ship already is. The front guide // point is for joining from the side, never a mandatory outward excursion. var onAxis=depth>=-0.1 && lateral.Length()DockDistance+ship.Radius)) {Set(FlightState.DockApproach,L.MinerDockFollowAxis);return;} reason=DockNavigationReason(L.MinerDockOuterEntry,target); CheckDockProgress(target); } void CheckDockProgress(Vector3D target,string timeout=null) { var metric=Data.Distance(Position,target)+flight.AttitudeError*5; if(metric60) { var why=(timeout??L.MinerDockAlignTimeout)+" / "+reason; if(state==FlightState.DockAlign) { Set(FlightState.Holding,why);Publish();ReleaseLane();nextRequest=P.Now+5; } else AbortDock(why); } } string DockNavigationReason(string phase,Vector3D target) { var motion=ship.Controller.GetShipVelocities(); var velocity=FlightController.PointVelocity(motion.LinearVelocity,motion.AngularVelocity,Position,ship.Controller.CenterOfMass); var detail=L.F(L.MinerDockNavigationProgress,phase,Data.Distance(Position,target),(velocity-dock.PointVelocity(target)).Length(),flight.AttitudeError*180/Math.PI); return flight.Problem.Length>0?flight.Problem+" / "+detail:detail; } void DockApproach(double dt) { var permit=DockProblem("dock");if(permit.Length>0) {AbortDock(permit);return;} Vector3D f,u;var target=DockTarget(0,out f,out u); var contact=target; if(!flight.CanSupport(f,u,0.3)) {AbortDock(L.MinerApproachThrustLow);return;} var normal=dock.Predicted(P.Now).Forward; var error=Position-target; var velocity=ship.Controller.GetShipVelocities();var ownPort=ship.Connector.GetPosition(); Vector3D portError=ownPort-(contact+Vector3D.TransformNormal(ship.ConnectorLocal,Data.Frame(Data.Zero,f,u))); var lateral=portError-normal*Data.Dot(portError,normal); Vector3D ownVelocity=velocity.LinearVelocity+Data.Cross(velocity.AngularVelocity,ownPort-ship.Controller.CenterOfMass); var relative=ownVelocity-dock.PointVelocity(ownPort); double angle=Math.Acos(Data.Clamp(Data.Dot(ship.Controller.WorldMatrix.Forward,f),-1,1))*180/Math.PI; double upAngle=Math.Acos(Data.Clamp(Data.Dot(ship.Controller.WorldMatrix.Up,u),-1,1))*180/Math.PI; var lateralSpeed=(relative-normal*Data.Dot(relative,normal)).Length(); double angular=(velocity.AngularVelocity-dock.AngularVelocity).Length()*180/Math.PI,depth=Data.Dot(error,normal); var aligned=lateral.Length()<0.15 && angle<1 && upAngle<1 && lateralSpeed<0.1 && angular<0.1; // Acquire the tight window once, then use an exit band. Tiny angular // telemetry fluctuations must not restart a two-second axial stop. var closing=stableSince>=0 && P.Now-stableSince>=2; var retained=closing && lateral.Length()<0.25 && angle<2 && upAngle<2 && lateralSpeed<0.2 && angular<0.2; if(aligned || retained) {if(stableSince<0)stableSince=P.Now;}else stableSince=-1; // The first contact has no measured centre spacing yet. Acquire the // stable, slow capture window before reaching either connector face. var captureDistance=calibrated?3:Math.Max(6,ship.Radius*2); var synchronizing=angle>=2 || upAngle>=2 || (depth<=captureDistance && (stableSince<0 || P.Now-stableSince<2)); if(synchronizing)target+=normal*Math.Max(0,depth); var finalSpeed=P.Config.Number("Dock","FinalSpeed",0.2,0.05,0.5); var speed=Math.Min(10,Math.Sqrt(finalSpeed*finalSpeed+0.3*Math.Max(0,depth-captureDistance))); flight.SetDockContact(dock.GridId,dock.Predicted(P.Now).Translation,Math.Max(1.5,P.Me.CubeGrid.GridSize)); // Once the entry point/axis is captured, the reserved approach follows // connector geometry directly. Camera coverage and hits no longer gate it. flight.Move(target,dock.PointVelocity(target),f,u,speed,0.1,false,dt,false,false); reason=DockNavigationReason(synchronizing?L.MinerDockSyncPose:depth>captureDistance?L.MinerDockFollowAxis:L.MinerSlowRelativeApproach,contact)+" / "+L.F(L.MinerDockPrecision,lateral.Length(),angular); if(ship.Connector.Status==MyShipConnectorStatus.Connectable && aligned && relative.Length()<0.3) { ship.Connector.Connect(); if(Connected && ship.Connector.OtherConnector!=null && ship.Connector.OtherConnector.EntityId==dock.ConnectorId) {ReleaseLane();flight.ClearDockContact();ship.Release();Set(FlightState.Servicing,L.MinerDockConfirmedService);} } if(state==FlightState.DockApproach)CheckDockProgress(contact,L.MinerDockTimeout); } void AbortDock(string why) { if(Connected)return; var outward=dock==null?ship.Controller.WorldMatrix.Backward:dock.Predicted(P.Now).Forward; abortPoint=Position+outward*Math.Max(DockDistance,ship.Radius*3);abortVelocity=dock==null?Data.Zero:dock.PointVelocity(Position);abortAt=P.Now;if(state==FlightState.DockApproach)retry++;stableSince=-1; Set(FlightState.DockRetreat,why); } void DockRetreat(double dt) { var target=abortPoint+abortVelocity*Math.Min(P.Now-abortAt,1);var f=ship.Controller.WorldMatrix.Forward; var velocity=P.Now-abortAt<1?abortVelocity:Data.Zero; if(flight.Move(target,velocity,f,ship.Controller.WorldMatrix.Up,2,1,true,dt,false,false)) { ReleaseLane();flight.ClearDockContact(); if(retry>=3) {running=false;Set(FlightState.Fault,L.MinerDockFailedThreeTimes+" / "+reason);} else Set(FlightState.Holding,L.MinerDockRetreatedRetry); } } void Request(string kind) { if(P.Now0) {var i=new MyIni();i.Set("lease","Token",lane);P.Bus.Send("LANE_RELEASE",P.Config.BaseId,i,true);} lane="";laneKind="";stableSince=-1; } void Report(string outcome,string ore,string message) { if(job==null)return; P.Bus.Send("RESULT",P.Config.BaseId,MakeReport(outcome,ore,message).ToIni(),true); } JobReport MakeReport(string outcome,string ore,string message) => new JobReport {JobId=job.Id,Outcome=outcome,Ore=ore,Progress=job.Progress,Position=job.Entry,Direction=job.Direction,Up=job.Up,EntityId=job.EntityId,Message=message}; Telemetry Snapshot() { var t=new Telemetry {Id=P.Config.Id,State=state,Position=Position,Reason=reason,Battery=resources.Battery,Hydrogen=resources.Hydrogen,Cargo=resources.Cargo,OreAmount=resources.OreAmount,StoredMWh=resources.StoredMWh,CapacityMWh=resources.CapacityMWh,NetMW=resources.NetMW,EnduranceSeconds=resources.EnduranceSeconds,DistanceHome=HomeDistance,ThrustMargin=flight.ThrustMargin,Radius=ship.Radius,ReceivedAt=P.Now,Connected=Connected,JobId=job==null?"":job.Id,Depth=job==null?0:job.Progress,TargetDepth=job==null?0:job.Depth,Address=P.Me.EntityId}; t.CancelledThrough=cancelledThrough; t.NextAfter=continuing && running && !manualStop && nextReport!=null?nextReport.JobId:""; t.HasRoute=flight.HasRoute;t.RouteTarget=flight.RouteTarget; if(ship.Controller!=null) {t.Velocity=ship.Controller.GetShipVelocities().LinearVelocity;t.Forward=ship.Controller.WorldMatrix.Forward;t.Up=ship.Controller.WorldMatrix.Up;t.Gravity=ship.Controller.GetNaturalGravity().Length();} if(ship.Connector!=null) {t.ConnectorId=ship.Connector.EntityId;if(ship.Connector.OtherConnector!=null)t.PeerConnectorId=ship.Connector.OtherConnector.EntityId;} var ores=new List();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; } void Publish() {P.Bus.Send(home==null?"HELLO":"TELEMETRY",P.Config.BaseId,Snapshot().ToIni());} void Heartbeat() { if(!P.Config.Flag("Watchdog","Enabled",false))return; var i=new MyIni();i.Set("watchdog","Grid",P.Me.CubeGrid.EntityId);i.Set("watchdog","Business",P.Me.EntityId);i.Set("watchdog","State",state.ToString()); P.Bus.Send("WATCH_HEARTBEAT",P.Config.Text("Watchdog","Id",P.Config.Id+"-watchdog"),i); } public override void Receive(Packet packet) { if(packet.Kind=="WATCH_READY" || packet.Kind=="WATCH_TRIPPED") { if(packet.From!=P.Config.Text("Watchdog","Id",P.Config.Id+"-watchdog") || Data.ReadLong(packet.Body,"watchdog","Grid")!=P.Me.CubeGrid.EntityId)return; watchdogReady=packet.Kind=="WATCH_READY";watchdogSession=packet.Session;watchdogAt=P.Now; if(!watchdogReady) {running=false;manualStop=true;ship.Release();Set(FlightState.Manual,L.MinerWatchdogTripped);}return; } if(packet.From!=P.Config.BaseId)return; if(packet.EpochfleetEpoch) { fleetEpoch=packet.Epoch;P.Bus.Epoch=fleetEpoch;P.Bus.CancelPending(null,P.Config.BaseId); cancelledThrough=-1;nextReport=null;continuing=false;retry=0;lane="";laneKind="";dock=null; CancelCurrentTask();flight.ResetRoute(); if(healthy && !Connected && !insideBore && state!=FlightState.Manual && ship.Controller!=null && !ship.Controller.IsUnderControl) {ship.PrepareFlight();Set(FlightState.Returning,L.MinerTaskCancelledReturn);} P.Save(); } // Repeated reset recalls must not override manual control or a hardware fault. if(packet.Kind=="COMMAND" && Data.Flag(packet.Body,"command","Reset"))return; if(packet.Kind=="TRAFFIC") { flight.TrafficWait=Data.Flag(packet.Body,"traffic","Wait");flight.TrafficAvoid=Data.Flag(packet.Body,"traffic","Avoid"); flight.TrafficPoint=Data.ReadVector(packet.Body,"traffic","Point",Position);flight.TrafficPeer=Data.Text(packet.Body,"traffic","Peer");flight.TrafficAt=P.Now;return; } if(packet.Kind=="NEXT") { if(!continuing || nextReport==null || Data.Text(packet.Body,"next","After")!=nextReport.JobId)return; if(Data.Flag(packet.Body,"next","Done"))BeginReturn(L.MinerSearchCompleteReturn);else reason=L.MinerWaitNextTask; return; } if(packet.Kind=="BASE") {home=DockFrame.FromIni(packet.Body);home.Source=packet.Source;home.ReceivedAt=P.Now;return;} if(packet.Kind=="DOCKPOSE" || packet.Kind=="DOCK_GRANT" || packet.Kind=="DEPART_GRANT") { var frame=DockFrame.FromIni(packet.Body);if(frame.BaseId!=P.Config.BaseId || (frame.Owner.Length>0 && frame.Owner!=P.Config.Id))return; if(dock!=null && dock.ConnectorId!=frame.ConnectorId) {waitingAligning=false;stableSince=-1;flight.ResetRoute();} frame.Source=packet.Source;frame.ReceivedAt=P.Now;dock=frame; if(packet.Kind=="DOCK_GRANT" || packet.Kind=="DEPART_GRANT") {var kind=packet.Kind=="DOCK_GRANT"?"dock":"depart";if(lane!=frame.Token || laneKind!=kind)stableSince=-1;lane=frame.Token;laneKind=kind;} return; } if(packet.Kind=="JOB") { Job received=Job.FromIni(packet.Body);if(received.Owner.Length>0 && received.Owner!=P.Config.Id)return; if(nextReport!=null && received.Id==nextReport.JobId)return; if(continuing && nextReport!=null && Data.Text(packet.Body,"next","After")!=nextReport.JobId)return; if(canceling || IsCancelled(received)) {P.Log(L.F(L.MinerCancelledTaskRefused,received.Id));return;} if(job!=null && job.Id!=received.Id) {P.Log(L.F(L.MinerCompetingTaskRefused, received.Id));return;} if(job!=null)received.Progress=Math.Max(received.Progress,job.Progress);else {discovered.Clear();invalidSample=false;} var airborne=continuing && running && !manualStop && !Connected && state==FlightState.Survey; job=received;leaseConfirmed=true;continuing=false; if(!manualStop)running=true; if(airborne) { var distance=Data.Distance(Position,job.Entry); var depth=job.Kind==JobKind.Survey?0:job.Depth;string why; if(home==null || P.Now-home.ReceivedAt>5)BeginReturn(L.MinerWaitBaseTelemetry); else if(!resources.ReadyForSortie((BudgetDistance+distance*2)/2,depth,out why,true))BeginReturn(why); else Set(FlightState.Transit,L.MinerResumeTaskEntry); } P.Log(L.F(L.MinerTaskConfirmed, job.Id));return; } if(packet.Kind=="COMMAND") { var command=Settings.NormalizeCommand(Data.Text(packet.Body,"command","Text"));Command(command); if(command=="diagnose") {var report=new MyIni();report.Set("diagnostics","Text",Diagnostics);P.Bus.Send("DIAGNOSTICS",P.Config.BaseId,report,false,packet.Source);} } } public override void Command(string command) { var op=Settings.NormalizeCommand(command).ToLowerInvariant(); if(op.StartsWith("cancel-through ",Data.Ordinal)) { long watermark; if(!long.TryParse(op.Substring(15),out watermark) || watermark<0 || watermark>1000000000)throw Data.Invalid("cancel-through"); CancelThrough(watermark);return; } if(op=="diagnose") {string error;P.Log(ship.Healthy(out error)?Diagnostics:error);return;} if(op=="calibrate") { if(!CalibrateDock(false))P.Log(L.MinerCalibrationRequiresDock);return; } if(op=="rescan" || op=="init") { if(!Connected && state!=FlightState.Manual && state!=FlightState.Paused) {P.Log(L.MinerRescanStateRequired);return;} ship.Release();string error;healthy=ship.Scan(out error);dockInitialized=false;resources.ResetInventoryScan();nextResources=0;if(!healthy)Set(FlightState.Fault,error);else Set(Connected?FlightState.Docked:FlightState.Paused,L.MinerHardwareRescanned);return; } if(op=="release") {running=false;manualStop=true;ship.Release();Set(FlightState.Manual,L.MinerControlReleased);return;} if(op=="pause") { running=false;manualStop=true;pausedAfterRetreat=true; if(InHole)Set(FlightState.Retreat,L.MinerPauseRetreatFirst);else if(!Connected)Set(FlightState.Paused,L.MinerTaskPaused);else reason=L.MinerTaskPaused;return; } if(op=="return" || op=="return-all") {running=false;manualStop=true;if(!Connected)BeginReturn(L.MinerManualReturn);return;} if(op=="start" || op=="resume") { if(!healthy) {P.Log(L.MinerRepairBeforeRescan);return;} if(canceling) { manualStop=false;pausedAfterRetreat=false;running=false;retry=0; if(Connected)Set(FlightState.Servicing,L.MinerConnectedService);else ContinueCancelledReturn(); return; } running=true;manualStop=false;pausedAfterRetreat=false;retry=0; if(Connected)Set(FlightState.Servicing,L.MinerAutomaticCycleEnabled); else if(InHole && job!=null && leaseConfirmed) {ResetSample();Set(FlightState.Drilling,L.MinerResumeCurrentBore);} else if(job!=null && leaseConfirmed)Set(FlightState.Transit,L.MinerResumeTaskEntry); else if(continuing && nextReport!=null)Set(FlightState.Survey,L.MinerWaitNextTask); else BeginReturn(L.MinerResumeReturnFirst);return; } P.Log(L.F(L.MinerUnknownCommand, command)); } bool IsCancelled(Job candidate) { if(cancelledThrough<0)return false; var prefix=P.Config.BaseId+"-job-";long serial; // After a clear, unversioned legacy identifiers cannot prove that they // belong to a newly issued task either. return !candidate.Id.StartsWith(prefix,Data.Ordinal) || !long.TryParse(candidate.Id.Substring(prefix.Length),out serial) || serial<=cancelledThrough; } void ContinueCancelledReturn() { if(state==FlightState.Departing)AbortDock(L.MinerTaskCancelledReturn); else if(state!=FlightState.DockAlign && state!=FlightState.DockApproach && state!=FlightState.DockRetreat && state!=FlightState.Holding && state!=FlightState.Returning)BeginReturn(L.MinerTaskCancelledReturn); } void CancelThrough(long watermark) { if(watermark<=cancelledThrough)return; cancelledThrough=watermark;P.Bus.CancelPending("DEPART_REQUEST",P.Config.BaseId); continuing=false; // A newer job can arrive before an older cancellation retry after reconnect. if(job!=null && !IsCancelled(job)) {P.Save();return;} CancelCurrentTask();P.Save(); } void CancelCurrentTask() { running=false;leaseConfirmed=false;completion="";pausedAfterRetreat=false; ship.SetDrills(false);ship.SetSampling(false); if(Connected) {ReleaseLane();job=null;canceling=false;insideBore=false;breadcrumbs.Clear();discovered.Clear();reason=L.MinerTasksCleared;} else { canceling=true; insideBore=InHole; if(healthy && ship.Controller!=null && state!=FlightState.Manual && !ship.Controller.IsUnderControl) { manualStop=false; ContinueCancelledReturn(); } } } public override void Emergency(string text) { running=false;manualStop=true;ship.SetDrills(false);ship.SetSampling(false); if(ship.Controller==null || !ship.Controller.IsFunctional) {ship.Release();Set(FlightState.Fault,text);return;} if(InHole) {pausedAfterRetreat=true;Set(FlightState.Retreat,text);} else if(state==FlightState.DockAlign || state==FlightState.DockApproach || state==FlightState.Departing)AbortDock(text); else {ship.Release();Set(FlightState.Fault,text);} } public override List GetTelemetry() {view.Clear();view.Add(Snapshot());return view;} public override double MeasuredDepartureMass => ship.AutoDepartureMass; public override List GetJobs() {var result=new List();if(job!=null)result.Add(job);return result;} public override string Diagnostics { get { var permit=DockProblem(Connected || state==FlightState.Departing?"depart":"dock"); var laneStatus=L.F(L.MinerDockPermitDiagnostics,permit.Length==0?L.MinerDockGrantValid:permit); var control=state==FlightState.DockApproach?L.F(L.MinerDockDirectControl,flight.CommandSpeed):L.F(L.MinerFlightDiagnostics,flight.AttitudeError*180/Math.PI,flight.ScanClearance,flight.Problem)+"\n"+flight.CameraDiagnostics; return L.F(L.MinerDiagnostics,L.State(state),reason,L.Bool(calibrated),L.Bool(leaseConfirmed),P.Bus.PendingCount,laneStatus+"\n"+control); } } public override void Save(MyIni i) { i.Set("miner","FleetEpoch",fleetEpoch); i.Set("miner","Calibrated",calibrated);Data.PutVector(i,"miner","Position",calibratedPosition);Data.PutVector(i,"miner","Forward",calibratedForward);Data.PutVector(i,"miner","Up",calibratedUp);i.Set("miner","Running",running);i.Set("miner","ManualStop",manualStop);i.Set("miner","State",(int)state);i.Set("miner","Discovered",string.Join(",",discovered)); i.Set("miner","InsideBore",insideBore);i.Set("miner","InvalidSample",invalidSample); i.Set("miner","CancelledThrough",cancelledThrough);i.Set("miner","Canceling",canceling); i.Set("miner","Continuing",continuing);if(nextReport!=null)i.Set("miner","NextReport",nextReport.ToIni().ToString()); if(job!=null)job.Write(i,"activeJob");i.Set("miner","BreadcrumbCount",breadcrumbs.Count);for(int n=0;nDateTime.MaxValue.Ticks)throw Data.Invalid("miner.FleetEpoch");P.Bus.Epoch=fleetEpoch; calibrated=Data.Flag(i,"miner","Calibrated");calibratedPosition=Data.ReadVector(i,"miner","Position",Data.Zero);calibratedForward=Data.ReadVector(i,"miner","Forward",Data.Forward);calibratedUp=Data.ReadVector(i,"miner","Up",Data.Up); if(calibrated && (Math.Abs(calibratedForward.Length()-1)>0.05 || Math.Abs(calibratedUp.Length()-1)>0.05 || Math.Abs(Data.Dot(calibratedForward,calibratedUp))>0.05))throw Data.Invalid(L.MinerInvalidDockCalibration); running=Data.Flag(i,"miner","Running");manualStop=Data.Flag(i,"miner","ManualStop");insideBore=Data.Flag(i,"miner","InsideBore");invalidSample=Data.Flag(i,"miner","InvalidSample");if(i.ContainsKey("activeJob","Id"))job=Job.Read(i,"activeJob"); cancelledThrough=i.ContainsKey("miner","CancelledThrough")?Data.ReadLong(i,"miner","CancelledThrough"):-1; if(cancelledThrough < -1 || cancelledThrough>1000000000)throw Data.Invalid("miner.CancelledThrough"); canceling=Data.Flag(i,"miner","Canceling") || (job!=null && IsCancelled(job)); continuing=Data.Flag(i,"miner","Continuing");var reportIni=new MyIni(); if(i.ContainsKey("miner","NextReport") && reportIni.TryParse(Data.Text(i,"miner","NextReport")))nextReport=JobReport.FromIni(reportIni); foreach(var ore in Data.Text(i,"miner","Discovered").Split(','))if(ore.Length>0)discovered.Add(ore); int count=(int)Data.ReadLong(i,"miner","BreadcrumbCount");if(count<0 || count>128)throw Data.Invalid(L.MinerInvalidBreadcrumbCount);for(int n=0;nFlightState.Fault)throw Data.Invalid(L.CommonFlightStateInvalid); // A return request is a stop to mining, not a stop to navigation. if(previous==FlightState.Returning || previous==FlightState.Holding || previous==FlightState.DockAlign || previous==FlightState.DockApproach || previous==FlightState.DockRetreat)manualStop=false; running=false; } } } } }