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<Vector3D> breadcrumbs=new List<Vector3D>();
readonly Dictionary<string,double> sample=new Dictionary<string,double>();
readonly HashSet<string> discovered=new HashSet<string>();
readonly List<Telemetry> view=new List<Telemetry>();
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;
bool AutoCalibrate => P.Config.Flag("Hardware","AutoCalibrate",true);
bool WatchdogEnabled => P.Config.Flag("Watchdog","Enabled",false);
string WatchdogId => P.Config.Text("Watchdog","Id",P.Config.Id+"-watchdog");
double MaxDockAge => P.Config.Number("Dock","MaxTelemetryAge",0.5,0.1,2);
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:Data.Max(HomeDistance,time*speed);
}
}
double Approach => P.Config.Number("Mining","ApproachDistance",20,5,200)+ship.Radius;
double DockDistance => Data.Max(dock!=null && dock.ApproachDistance>0?dock.ApproachDistance: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;
bool ReturningToDock => state==FlightState.Returning || state==FlightState.Holding || state==FlightState.DockAlign || state==FlightState.DockApproach || state==FlightState.DockRetreat;
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+(state==FlightState.DockAlign || state==FlightState.DockApproach?.2: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;ship.ParkThrusters(false);}
else if(!dockInitialized && AutoCalibrate)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) {if(Connected)ship.ParkThrusters(true);return;}
if(Connected) {DockedTick(dt);return;}
if(state==FlightState.Fault) {flight.Hold(dt);return;}
if(!manualStop && Idle && (calibrated || AutoCalibrate))BeginReturn(L.MinerAutomaticReturn);
if(canceling && state==FlightState.Paused && !manualStop)BeginReturn(L.MinerTaskCancelledReturn);
if(WatchdogEnabled && (!watchdogReady || P.Now-watchdogAt>3) && running)
{
BeginReturn(L.MinerWatchdogHandshakeMissing);
}
if(Idle)
{flight.Hold(dt);return;}
if(ReturningToDock && TryConnect())return;
string returnReason;
if(!ReturningToDock && 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);
}
if(((ReturningToDock && state!=FlightState.DockRetreat) || state==FlightState.Transit) && YieldAtPort(dt))return;
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;
}
}
bool YieldAtPort(double dt)
{
if(P.Now-flight.TrafficAt>1.5 || (!flight.TrafficWait && !flight.TrafficAvoid))return false;
var range=dock==null?double.MaxValue:Data.Distance(Position,dock.Position);
var near=range<ship.Radius*3 && P.Now-dock.ReceivedAt<3;
if(flight.TrafficAvoid)
{
if(state==FlightState.DockApproach)Set(FlightState.DockAlign,L.F(L.MinerTrafficYield,flight.TrafficPeer));
flight.ClearDockContact();var point=flight.TrafficPoint;
if(range<Data.Max(30,ship.Radius*6))
{
var pose=dock.Predicted(P.Now);var normal=pose.Forward;var step=point-Position;
if(Data.Length(step)>2)step=Data.Unit(step,normal)*2;
step+=normal*Data.Max(0,Data.Min(0,ship.Radius-Data.Dot(Position-pose.Translation,normal))-Data.Dot(step,normal));
if(Data.LengthSquared(step)<.01)step=Data.Perpendicular(normal,ship.Controller.WorldMatrix.Up)*2;
point=Position+step;
}
var f=near?ship.Controller.WorldMatrix.Forward:Data.Unit(point-Position,ship.Controller.WorldMatrix.Forward);
flight.Move(point,near?dock.PointVelocity(point):Data.Zero,f,near?ship.Controller.WorldMatrix.Up:TravelUp(f),1.5,.3,true,dt,!near,false);
}
else if(near)flight.Move(Position,dock.PointVelocity(Position),ship.Controller.WorldMatrix.Forward,ship.Controller.WorldMatrix.Up,0,.3,false,dt);
else flight.Hold(dt);
stableSince=-1;progressAt=P.Now;reason=L.F(L.MinerTrafficYield,flight.TrafficPeer);
if(flight.TrafficAvoid && flight.Problem.Length>0)reason+=" / "+flight.Problem;
return true;
}
void DockedTick(double dt)
{
ship.ParkThrusters(true);
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.ServiceFlags>=0?L.Service(resources.ServiceFlags):resources.DepartureWaitReason;return;}
if(!calibrated) {reason=AutoCalibrate?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:Data.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(WatchdogEnabled && (!watchdogReady || P.Now-watchdogAt>3)) {reason=L.MinerRunWatchdogArm;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();
if(!flight.CanSupport(ship.Controller.WorldMatrix.Forward,ship.Controller.WorldMatrix.Up,0.3)) {ship.ParkThrusters(true);reason=flight.Problem;return;}
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(Data.Max(DockDistance,dock.LaneClearance+ship.Radius+2),out f,out u,false);
flight.SetDockContact(dock.GridId,dock.Position,Data.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 Data.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-Data.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=Data.LengthSquared(gravity)>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,Data.Length(ship.Controller.GetShipVelocities().LinearVelocity),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*Data.Min(job.Depth,Data.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=Data.Min(entryNearest,height);entryFarthest=Data.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=Data.Max(x,Math.Abs(Data.Dot(corner,local.Right)));y=Data.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=Data.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 ores=new List<string>();foreach(var item in resources.Ores)
{
double previous;sample.TryGetValue(item.Key,out previous);
if(item.Value-previous>=P.Config.Number("Mining","DetectionAmount",0.01,0.000001,1000))ores.Add(item.Key);
}
ores.Sort();var found=string.Join(",",ores);
if(found.Length>0)
{
sampleConfirm=sampleOre==found?sampleConfirm+1:1;sampleOre=found;
if(sampleConfirm<2)return;
var fresh=new List<string>();foreach(var ore in ores)if(discovered.Add(ore))fresh.Add(ore);
if(fresh.Count>0)Report("Discovery",string.Join(",",fresh),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;}
var end=job.Entry+job.Direction*job.Depth;
var attitude=ship.CameraAttitude(end,job.Up);
if(!flight.CanSupport(attitude.Forward,attitude.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,attitude.Forward,attitude.Up,0,0.5,false,dt))
{reason=flight.Problem.Length>0?flight.Problem:L.MinerWaitSurveyScan;return;}
if(P.Now<nextSample)return;nextSample=P.Now+0.25;
foreach(var camera in ship.Cameras)
{
if(!camera.IsWorking || camera.CubeGrid!=P.Me.CubeGrid || !camera.CanScan(end))continue;
var hit=camera.Raycast(end);if(!hit.IsEmpty() && hit.EntityId==P.Me.CubeGrid.EntityId)continue;
if(!hit.IsEmpty() && hit.HitPosition.HasValue && (hit.Type==MyDetectedEntityType.Asteroid || hit.Type==MyDetectedEntityType.Planet))
{
var report=new JobReport {Scan=ScanRecord.Capture(camera.GetPosition(),end,hit),JobId=job.Id,Outcome="SurveyHit",Position=hit.HitPosition.Value,Direction=Data.Unit(end-camera.GetPosition(),job.Direction),Up=job.Up,EntityId=hit.EntityId,Message=L.MinerSurfaceHitUnconfirmed};
CompleteJob(report);return;
}
var result=MakeReport(hit.IsEmpty()?"SurveyEmpty":"Blocked","",hit.IsEmpty()?L.MinerRayNoSurface:L.MinerScanNonVoxelBlocked);
result.Scan=ScanRecord.Capture(camera.GetPosition(),end,hit);CompleteJob(result);return;
}
reason=L.MinerWaitSurveyScan;
}
void BeginReturn(string text)
{
if(Idle || state==FlightState.Fault || state==FlightState.Manual)ship.PrepareFlight();
continuing=false;
reason=text;pausedAfterRetreat=false;
if(InHole) {Set(FlightState.Retreat,text);return;}
if(state==FlightState.Departing) {AbortDock(text);return;}
if(ReturningToDock)return;
retry=0;
ship.SetDrills(false);ship.SetSampling(false);returnIndex=breadcrumbs.Count-1;Set(FlightState.Returning,text);
}
void FinishBlocked(string text)
{
if(InHole) {completion="Blocked";BeginReturn(text);return;}
if(job!=null && job.Kind==JobKind.Manual) {manualStop=true;running=false;Report("Paused","",text);Set(FlightState.Paused,text);return;}
if(job!=null)CompleteJob(MakeReport("Blocked","",text));else BeginReturn(text);
}
void CompleteJob(JobReport report)
{
P.Bus.Send("RESULT",P.Config.BaseId,report.ToIni(),true);
nextReport=report;job=null;leaseConfirmed=false;discovered.Clear();
continuing=running && !manualStop && !canceling;nextReportAt=P.Now+2;
string why;if(continuing && resources.MustReturn(out why)) {BeginReturn(why);return;}
if(continuing) {Set(FlightState.Survey,L.MinerWaitNextTask);flight.ResetRoute();Publish();}
else BeginReturn(report.Message);
}
void RecordBreadcrumb()
{
if(breadcrumbs.Count==0 || Data.Distance(Position,breadcrumbs[breadcrumbs.Count-1])>10)
{
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=Data.Max(2,Data.Length(ship.Controller.GetShipVelocities().LinearVelocity)*1.5);
if(Data.Length(leg)<look && (Data.Length(leg)<0.1 || Data.Dot(Data.Unit(leg,Data.Forward),Data.Unit(next,Vector3D.Backward))>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,Data.LengthSquared(gravity)>0.001?-gravity:ship.Controller.WorldMatrix.Up);
}
void Waiting(double dt)
{
Request("DOCK_REQUEST");
if(dock==null || P.Now-dock.ReceivedAt>MaxDockAge)
{stableSince=-1;flight.Hold(dt);reason=dock==null?L.MinerWaitBerthPoint:L.F(L.MinerDockPoseStale,Data.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=Data.LengthSquared(dock.WaitingLocal)>1?dock.WaitingLocal:new Vector3D(0,ship.Radius*2,-Data.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=Data.Length(FlightController.PointVelocity(motion.LinearVelocity,motion.AngularVelocity,Position,ship.Controller.CenterOfMass)-dock.PointVelocity(target));
if(waitingAligning && distance>4) {waitingAligning=false;stableSince=-1;flight.ResetRoute();}
if(!waitingAligning && distance<=2 && relativeSpeed<=0.3) {waitingAligning=true;flight.ResetRoute();}
var openLeg=!waitingAligning;
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=Data.Max(0,P.Now-dock.ReceivedAt);
if(age>MaxDockAge)return L.F(L.MinerDockPoseStale,age);
if(lane.Length==0 || dock.Token.Length==0)return dock.Problem.Length>0?dock.Problem: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,Data.Length(dock.PointVelocity(dock.Position)),Data.Length(dock.AngularVelocity)*180/Math.PI,Data.Length(dock.Acceleration));
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=Data.Transpose(other.WorldMatrix);
Vector3D position=Data.TransformNormal(Position-other.GetPosition(),inverse);
Vector3D forward=Data.TransformNormal(ship.Controller.WorldMatrix.Forward,inverse),up=Data.TransformNormal(ship.Controller.WorldMatrix.Up,inverse);
if(!Data.Finite(Data.LengthSquared(position))||!Data.Finite(Data.LengthSquared(forward))||!Data.Finite(Data.LengthSquared(up)))return false;
if(Math.Abs(Data.Length(forward)-1)>0.05||Math.Abs(Data.Length(up)-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=Data.TransformNormal(calibratedForward,frame);u=Data.TransformNormal(calibratedUp,frame);
if(!calibrated) {f=ship.Controller.WorldMatrix.Forward;u=ship.Controller.WorldMatrix.Up;}
if(mating && Data.LengthSquared(ship.ConnectorForwardLocal)>0.5 && Data.LengthSquared(ship.ConnectorUpLocal)>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?Data.Max(0,-(calibratedPosition+Data.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 && !AutoCalibrate) {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 && Data.Length(lateral)<Data.Max(0.5,Data.Min(5,depth*0.08));
if(onAxis && depth<=DockDistance+1)
{
Set(FlightState.DockApproach,L.MinerDockFollowAxis);DockApproach(dt);return;
}
// An on-axis ship scans its way to the entry plane without braking to
// a stop there. Off-axis arrivals first join the guide point normally.
var target=onAxis?contact:contact+normal*DockDistance;
// Before entry, even an axial route may be behind the mining cameras.
// Travel facing the route; DockApproach acquires the mating pose at entry.
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))
{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(metric<lastProgress-0.2) {lastProgress=metric;progressAt=P.Now;}
if(P.Now-progressAt>60)
{
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),Data.Length(velocity-dock.PointVelocity(target)),flight.AttitudeError*180/Math.PI);
return flight.Problem.Length>0?flight.Problem+" / "+detail:detail;
}
void DockApproach(double dt)
{
if(TryConnect())return;
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+Data.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=Data.Length(relative-normal*Data.Dot(relative,normal));
double angular=Data.Length(velocity.AngularVelocity-dock.AngularVelocity)*180/Math.PI,depth=Data.Dot(error,normal);
var aligned=Data.Length(lateral)<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 && Data.Length(lateral)<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:Data.Max(6,ship.Radius*2);
var synchronizing=angle>=2 || upAngle>=2 || (depth<=captureDistance && (stableSince<0 || P.Now-stableSince<2));
if(synchronizing)target+=normal*Data.Max(0,depth);
// Keep the pose-acquisition distance; only the measured final metre
// needs constant capture speed. First contact stays conservative.
var slowDistance=calibrated?1:captureDistance;
var finalSpeed=Data.Max(.5,P.Config.Number("Dock","FinalSpeed",.5,.05,1));
var speed=Data.Min(10,Math.Sqrt(finalSpeed*finalSpeed+0.5*Data.Max(0,depth-slowDistance)));
flight.SetDockContact(dock.GridId,dock.Predicted(P.Now).Translation,Data.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,closing && !synchronizing?finalSpeed:0);
reason=DockNavigationReason(synchronizing?L.MinerDockSyncPose:depth>slowDistance?L.MinerDockFollowAxis:L.MinerSlowRelativeApproach,contact)+" / "+L.F(L.MinerDockPrecision,Data.Length(lateral),angular);
if(state==FlightState.DockApproach)CheckDockProgress(contact,L.MinerDockTimeout);
}
bool TryConnect()
{
if(ship.Connector==null || ship.Connector.Status!=MyShipConnectorStatus.Connectable)return false;
// Magnetic capture is authoritative. Do not fight it with geometric
// corrections, or wait for stale approach telemetry before locking.
flight.Release();ship.Connector.Connect();reason=L.MinerDockLockPending;
if(Connected)
{
ship.ParkThrusters(true);ReleaseLane();retry=0;
var other=ship.Connector.OtherConnector;
if(dock!=null && other!=null && other.EntityId!=dock.ConnectorId)
{running=false;Set(FlightState.Fault,L.MinerWrongDock);}
else Set(FlightState.Servicing,L.MinerDockConfirmedService);
Publish();
}
return true;
}
void AbortDock(string why)
{
if(Connected)return;
var outward=dock==null?ship.Controller.WorldMatrix.Backward:dock.Predicted(P.Now).Forward;
abortPoint=Position+outward*Data.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*Data.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.Now<nextRequest)return;nextRequest=P.Now+2;P.Bus.Send(kind,P.Config.BaseId,Snapshot().ToIni(),true);
}
void ReleaseLane()
{
if(lane.Length>0) {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.ServiceFlags=resources.ServiceFlags;
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=Data.Length(ship.Controller.GetNaturalGravity());}
t.BrakeResponse=1/Data.Min(1.6,P.Config.Number("Flight","VelocityGain",1.6,.2,5));
if(ship.Connector!=null) {t.ConnectorId=ship.Connector.EntityId;if(ship.Connector.OtherConnector!=null)t.PeerConnectorId=ship.Connector.OtherConnector.EntityId;}
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;
}
void Publish() {P.Bus.Send(home==null?"HELLO":"TELEMETRY",P.Config.BaseId,Snapshot().ToIni());}
void Heartbeat()
{
if(!WatchdogEnabled)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",WatchdogId,i);
}
public override void Receive(Packet packet)
{
if(packet.Kind=="WATCH_READY" || packet.Kind=="WATCH_TRIPPED")
{
if(packet.From!=WatchdogId || 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.Epoch<fleetEpoch)return;
if(packet.Epoch>fleetEpoch)
{
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=Data.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(!ReturningToDock)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<Telemetry> GetTelemetry() {view.Clear();view.Add(Snapshot());return view;}
public override double MeasuredDepartureMass => ship.AutoDepartureMass;
public override List<Job> GetJobs() {var result=new List<Job>();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(Snapshot()),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;n<breadcrumbs.Count;n++)i.Set("path",n.ToString(),Data.Vector(breadcrumbs[n]));
resources.Save(i);flight.Save(i);
}
public override void Load(MyIni i)
{
fleetEpoch=Data.ReadLong(i,"miner","FleetEpoch");if(fleetEpoch<0 || fleetEpoch>DateTime.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(Data.Length(calibratedForward)-1)>0.05 || Math.Abs(Data.Length(calibratedUp)-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;n<count;n++)breadcrumbs.Add(Data.ReadVector(i,"path",n.ToString(),Data.Zero));
resources.Load(i);flight.Load(i);leaseConfirmed=false;lane="";dock=null;home=null;ship.Release();
// Never replay old actuator values or dock/world targets after reload.
Set(Connected?FlightState.Servicing:FlightState.Paused,L.MinerRecoverySelfTest);
if(!Connected)
{
var previous=(FlightState)Data.ReadLong(i,"miner","State");
if(previous<FlightState.Boot || previous>FlightState.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;
}
}
}
}
}
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<Vector3D> breadcrumbs=new List<Vector3D>();
readonly Dictionary<string,double> sample=new Dictionary<string,double>();
readonly HashSet<string> discovered=new HashSet<string>();
readonly List<Telemetry> view=new List<Telemetry>();
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;
bool AutoCalibrate => P.Config.Flag("Hardware","AutoCalibrate",true);
bool WatchdogEnabled => P.Config.Flag("Watchdog","Enabled",false);
string WatchdogId => P.Config.Text("Watchdog","Id",P.Config.Id+"-watchdog");
double MaxDockAge => P.Config.Number("Dock","MaxTelemetryAge",0.5,0.1,2);
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:Data.Max(HomeDistance,time*speed);
}
}
double Approach => P.Config.Number("Mining","ApproachDistance",20,5,200)+ship.Radius;
double DockDistance => Data.Max(dock!=null && dock.ApproachDistance>0?dock.ApproachDistance: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;
bool ReturningToDock => state==FlightState.Returning || state==FlightState.Holding || state==FlightState.DockAlign || state==FlightState.DockApproach || state==FlightState.DockRetreat;
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+(state==FlightState.DockAlign || state==FlightState.DockApproach?.2: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;ship.ParkThrusters(false);}
else if(!dockInitialized && AutoCalibrate)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) {if(Connected)ship.ParkThrusters(true);return;}
if(Connected) {DockedTick(dt);return;}
if(state==FlightState.Fault) {flight.Hold(dt);return;}
if(!manualStop && Idle && (calibrated || AutoCalibrate))BeginReturn(L.MinerAutomaticReturn);
if(canceling && state==FlightState.Paused && !manualStop)BeginReturn(L.MinerTaskCancelledReturn);
if(WatchdogEnabled && (!watchdogReady || P.Now-watchdogAt>3) && running)
{
BeginReturn(L.MinerWatchdogHandshakeMissing);
}
if(Idle)
{flight.Hold(dt);return;}
if(ReturningToDock && TryConnect())return;
string returnReason;
if(!ReturningToDock && 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);
}
if(((ReturningToDock && state!=FlightState.DockRetreat) || state==FlightState.Transit) && YieldAtPort(dt))return;
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;
}
}
bool YieldAtPort(double dt)
{
if(P.Now-flight.TrafficAt>1.5 || (!flight.TrafficWait && !flight.TrafficAvoid))return false;
var range=dock==null?double.MaxValue:Data.Distance(Position,dock.Position);
var near=range<ship.Radius*3 && P.Now-dock.ReceivedAt<3;
if(flight.TrafficAvoid)
{
if(state==FlightState.DockApproach)Set(FlightState.DockAlign,L.F(L.MinerTrafficYield,flight.TrafficPeer));
flight.ClearDockContact();var point=flight.TrafficPoint;
if(range<Data.Max(30,ship.Radius*6))
{
var pose=dock.Predicted(P.Now);var normal=pose.Forward;var step=point-Position;
if(Data.Length(step)>2)step=Data.Unit(step,normal)*2;
step+=normal*Data.Max(0,Data.Min(0,ship.Radius-Data.Dot(Position-pose.Translation,normal))-Data.Dot(step,normal));
if(Data.LengthSquared(step)<.01)step=Data.Perpendicular(normal,ship.Controller.WorldMatrix.Up)*2;
point=Position+step;
}
var f=near?ship.Controller.WorldMatrix.Forward:Data.Unit(point-Position,ship.Controller.WorldMatrix.Forward);
flight.Move(point,near?dock.PointVelocity(point):Data.Zero,f,near?ship.Controller.WorldMatrix.Up:TravelUp(f),1.5,.3,true,dt,!near,false);
}
else if(near)flight.Move(Position,dock.PointVelocity(Position),ship.Controller.WorldMatrix.Forward,ship.Controller.WorldMatrix.Up,0,.3,false,dt);
else flight.Hold(dt);
stableSince=-1;progressAt=P.Now;reason=L.F(L.MinerTrafficYield,flight.TrafficPeer);
if(flight.TrafficAvoid && flight.Problem.Length>0)reason+=" / "+flight.Problem;
return true;
}
void DockedTick(double dt)
{
ship.ParkThrusters(true);
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.ServiceFlags>=0?L.Service(resources.ServiceFlags):resources.DepartureWaitReason;return;}
if(!calibrated) {reason=AutoCalibrate?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:Data.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(WatchdogEnabled && (!watchdogReady || P.Now-watchdogAt>3)) {reason=L.MinerRunWatchdogArm;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();
if(!flight.CanSupport(ship.Controller.WorldMatrix.Forward,ship.Controller.WorldMatrix.Up,0.3)) {ship.ParkThrusters(true);reason=flight.Problem;return;}
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(Data.Max(DockDistance,dock.LaneClearance+ship.Radius+2),out f,out u,false);
flight.SetDockContact(dock.GridId,dock.Position,Data.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 Data.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-Data.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=Data.LengthSquared(gravity)>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,Data.Length(ship.Controller.GetShipVelocities().LinearVelocity),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*Data.Min(job.Depth,Data.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=Data.Min(entryNearest,height);entryFarthest=Data.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=Data.Max(x,Math.Abs(Data.Dot(corner,local.Right)));y=Data.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=Data.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 ores=new List<string>();foreach(var item in resources.Ores)
{
double previous;sample.TryGetValue(item.Key,out previous);
if(item.Value-previous>=P.Config.Number("Mining","DetectionAmount",0.01,0.000001,1000))ores.Add(item.Key);
}
ores.Sort();var found=string.Join(",",ores);
if(found.Length>0)
{
sampleConfirm=sampleOre==found?sampleConfirm+1:1;sampleOre=found;
if(sampleConfirm<2)return;
var fresh=new List<string>();foreach(var ore in ores)if(discovered.Add(ore))fresh.Add(ore);
if(fresh.Count>0)Report("Discovery",string.Join(",",fresh),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;}
var end=job.Entry+job.Direction*job.Depth;
var attitude=ship.CameraAttitude(end,job.Up);
if(!flight.CanSupport(attitude.Forward,attitude.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,attitude.Forward,attitude.Up,0,0.5,false,dt))
{reason=flight.Problem.Length>0?flight.Problem:L.MinerWaitSurveyScan;return;}
if(P.Now<nextSample)return;nextSample=P.Now+0.25;
foreach(var camera in ship.Cameras)
{
if(!camera.IsWorking || camera.CubeGrid!=P.Me.CubeGrid || !camera.CanScan(end))continue;
var hit=camera.Raycast(end);if(!hit.IsEmpty() && hit.EntityId==P.Me.CubeGrid.EntityId)continue;
if(!hit.IsEmpty() && hit.HitPosition.HasValue && (hit.Type==MyDetectedEntityType.Asteroid || hit.Type==MyDetectedEntityType.Planet))
{
var report=new JobReport {Scan=ScanRecord.Capture(camera.GetPosition(),end,hit),JobId=job.Id,Outcome="SurveyHit",Position=hit.HitPosition.Value,Direction=Data.Unit(end-camera.GetPosition(),job.Direction),Up=job.Up,EntityId=hit.EntityId,Message=L.MinerSurfaceHitUnconfirmed};
CompleteJob(report);return;
}
var result=MakeReport(hit.IsEmpty()?"SurveyEmpty":"Blocked","",hit.IsEmpty()?L.MinerRayNoSurface:L.MinerScanNonVoxelBlocked);
result.Scan=ScanRecord.Capture(camera.GetPosition(),end,hit);CompleteJob(result);return;
}
reason=L.MinerWaitSurveyScan;
}
void BeginReturn(string text)
{
if(Idle || state==FlightState.Fault || state==FlightState.Manual)ship.PrepareFlight();
continuing=false;
reason=text;pausedAfterRetreat=false;
if(InHole) {Set(FlightState.Retreat,text);return;}
if(state==FlightState.Departing) {AbortDock(text);return;}
if(ReturningToDock)return;
retry=0;
ship.SetDrills(false);ship.SetSampling(false);returnIndex=breadcrumbs.Count-1;Set(FlightState.Returning,text);
}
void FinishBlocked(string text)
{
if(InHole) {completion="Blocked";BeginReturn(text);return;}
if(job!=null && job.Kind==JobKind.Manual) {manualStop=true;running=false;Report("Paused","",text);Set(FlightState.Paused,text);return;}
if(job!=null)CompleteJob(MakeReport("Blocked","",text));else BeginReturn(text);
}
void CompleteJob(JobReport report)
{
P.Bus.Send("RESULT",P.Config.BaseId,report.ToIni(),true);
nextReport=report;job=null;leaseConfirmed=false;discovered.Clear();
continuing=running && !manualStop && !canceling;nextReportAt=P.Now+2;
string why;if(continuing && resources.MustReturn(out why)) {BeginReturn(why);return;}
if(continuing) {Set(FlightState.Survey,L.MinerWaitNextTask);flight.ResetRoute();Publish();}
else BeginReturn(report.Message);
}
void RecordBreadcrumb()
{
if(breadcrumbs.Count==0 || Data.Distance(Position,breadcrumbs[breadcrumbs.Count-1])>10)
{
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=Data.Max(2,Data.Length(ship.Controller.GetShipVelocities().LinearVelocity)*1.5);
if(Data.Length(leg)<look && (Data.Length(leg)<0.1 || Data.Dot(Data.Unit(leg,Data.Forward),Data.Unit(next,Vector3D.Backward))>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,Data.LengthSquared(gravity)>0.001?-gravity:ship.Controller.WorldMatrix.Up);
}
void Waiting(double dt)
{
Request("DOCK_REQUEST");
if(dock==null || P.Now-dock.ReceivedAt>MaxDockAge)
{stableSince=-1;flight.Hold(dt);reason=dock==null?L.MinerWaitBerthPoint:L.F(L.MinerDockPoseStale,Data.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=Data.LengthSquared(dock.WaitingLocal)>1?dock.WaitingLocal:new Vector3D(0,ship.Radius*2,-Data.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=Data.Length(FlightController.PointVelocity(motion.LinearVelocity,motion.AngularVelocity,Position,ship.Controller.CenterOfMass)-dock.PointVelocity(target));
if(waitingAligning && distance>4) {waitingAligning=false;stableSince=-1;flight.ResetRoute();}
if(!waitingAligning && distance<=2 && relativeSpeed<=0.3) {waitingAligning=true;flight.ResetRoute();}
var openLeg=!waitingAligning;
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=Data.Max(0,P.Now-dock.ReceivedAt);
if(age>MaxDockAge)return L.F(L.MinerDockPoseStale,age);
if(lane.Length==0 || dock.Token.Length==0)return dock.Problem.Length>0?dock.Problem: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,Data.Length(dock.PointVelocity(dock.Position)),Data.Length(dock.AngularVelocity)*180/Math.PI,Data.Length(dock.Acceleration));
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=Data.Transpose(other.WorldMatrix);
Vector3D position=Data.TransformNormal(Position-other.GetPosition(),inverse);
Vector3D forward=Data.TransformNormal(ship.Controller.WorldMatrix.Forward,inverse),up=Data.TransformNormal(ship.Controller.WorldMatrix.Up,inverse);
if(!Data.Finite(Data.LengthSquared(position))||!Data.Finite(Data.LengthSquared(forward))||!Data.Finite(Data.LengthSquared(up)))return false;
if(Math.Abs(Data.Length(forward)-1)>0.05||Math.Abs(Data.Length(up)-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=Data.TransformNormal(calibratedForward,frame);u=Data.TransformNormal(calibratedUp,frame);
if(!calibrated) {f=ship.Controller.WorldMatrix.Forward;u=ship.Controller.WorldMatrix.Up;}
if(mating && Data.LengthSquared(ship.ConnectorForwardLocal)>0.5 && Data.LengthSquared(ship.ConnectorUpLocal)>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?Data.Max(0,-(calibratedPosition+Data.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 && !AutoCalibrate) {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 && Data.Length(lateral)<Data.Max(0.5,Data.Min(5,depth*0.08));
if(onAxis && depth<=DockDistance+1)
{
Set(FlightState.DockApproach,L.MinerDockFollowAxis);DockApproach(dt);return;
}
// An on-axis ship scans its way to the entry plane without braking to
// a stop there. Off-axis arrivals first join the guide point normally.
var target=onAxis?contact:contact+normal*DockDistance;
// Before entry, even an axial route may be behind the mining cameras.
// Travel facing the route; DockApproach acquires the mating pose at entry.
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))
{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(metric<lastProgress-0.2) {lastProgress=metric;progressAt=P.Now;}
if(P.Now-progressAt>60)
{
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),Data.Length(velocity-dock.PointVelocity(target)),flight.AttitudeError*180/Math.PI);
return flight.Problem.Length>0?flight.Problem+" / "+detail:detail;
}
void DockApproach(double dt)
{
if(TryConnect())return;
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+Data.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=Data.Length(relative-normal*Data.Dot(relative,normal));
double angular=Data.Length(velocity.AngularVelocity-dock.AngularVelocity)*180/Math.PI,depth=Data.Dot(error,normal);
var aligned=Data.Length(lateral)<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 && Data.Length(lateral)<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:Data.Max(6,ship.Radius*2);
var synchronizing=angle>=2 || upAngle>=2 || (depth<=captureDistance && (stableSince<0 || P.Now-stableSince<2));
if(synchronizing)target+=normal*Data.Max(0,depth);
// Keep the pose-acquisition distance; only the measured final metre
// needs constant capture speed. First contact stays conservative.
var slowDistance=calibrated?1:captureDistance;
var finalSpeed=Data.Max(.5,P.Config.Number("Dock","FinalSpeed",.5,.05,1));
var speed=Data.Min(10,Math.Sqrt(finalSpeed*finalSpeed+0.5*Data.Max(0,depth-slowDistance)));
flight.SetDockContact(dock.GridId,dock.Predicted(P.Now).Translation,Data.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,closing && !synchronizing?finalSpeed:0);
reason=DockNavigationReason(synchronizing?L.MinerDockSyncPose:depth>slowDistance?L.MinerDockFollowAxis:L.MinerSlowRelativeApproach,contact)+" / "+L.F(L.MinerDockPrecision,Data.Length(lateral),angular);
if(state==FlightState.DockApproach)CheckDockProgress(contact,L.MinerDockTimeout);
}
bool TryConnect()
{
if(ship.Connector==null || ship.Connector.Status!=MyShipConnectorStatus.Connectable)return false;
// Magnetic capture is authoritative. Do not fight it with geometric
// corrections, or wait for stale approach telemetry before locking.
flight.Release();ship.Connector.Connect();reason=L.MinerDockLockPending;
if(Connected)
{
ship.ParkThrusters(true);ReleaseLane();retry=0;
var other=ship.Connector.OtherConnector;
if(dock!=null && other!=null && other.EntityId!=dock.ConnectorId)
{running=false;Set(FlightState.Fault,L.MinerWrongDock);}
else Set(FlightState.Servicing,L.MinerDockConfirmedService);
Publish();
}
return true;
}
void AbortDock(string why)
{
if(Connected)return;
var outward=dock==null?ship.Controller.WorldMatrix.Backward:dock.Predicted(P.Now).Forward;
abortPoint=Position+outward*Data.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*Data.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.Now<nextRequest)return;nextRequest=P.Now+2;P.Bus.Send(kind,P.Config.BaseId,Snapshot().ToIni(),true);
}
void ReleaseLane()
{
if(lane.Length>0) {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.ServiceFlags=resources.ServiceFlags;
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=Data.Length(ship.Controller.GetNaturalGravity());}
t.BrakeResponse=1/Data.Min(1.6,P.Config.Number("Flight","VelocityGain",1.6,.2,5));
if(ship.Connector!=null) {t.ConnectorId=ship.Connector.EntityId;if(ship.Connector.OtherConnector!=null)t.PeerConnectorId=ship.Connector.OtherConnector.EntityId;}
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;
}
void Publish() {P.Bus.Send(home==null?"HELLO":"TELEMETRY",P.Config.BaseId,Snapshot().ToIni());}
void Heartbeat()
{
if(!WatchdogEnabled)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",WatchdogId,i);
}
public override void Receive(Packet packet)
{
if(packet.Kind=="WATCH_READY" || packet.Kind=="WATCH_TRIPPED")
{
if(packet.From!=WatchdogId || 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.Epoch<fleetEpoch)return;
if(packet.Epoch>fleetEpoch)
{
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=Data.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(!ReturningToDock)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<Telemetry> GetTelemetry() {view.Clear();view.Add(Snapshot());return view;}
public override double MeasuredDepartureMass => ship.AutoDepartureMass;
public override List<Job> GetJobs() {var result=new List<Job>();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(Snapshot()),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;n<breadcrumbs.Count;n++)i.Set("path",n.ToString(),Data.Vector(breadcrumbs[n]));
resources.Save(i);flight.Save(i);
}
public override void Load(MyIni i)
{
fleetEpoch=Data.ReadLong(i,"miner","FleetEpoch");if(fleetEpoch<0 || fleetEpoch>DateTime.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(Data.Length(calibratedForward)-1)>0.05 || Math.Abs(Data.Length(calibratedUp)-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;n<count;n++)breadcrumbs.Add(Data.ReadVector(i,"path",n.ToString(),Data.Zero));
resources.Load(i);flight.Load(i);leaseConfirmed=false;lane="";dock=null;home=null;ship.Release();
// Never replay old actuator values or dock/world targets after reload.
Set(Connected?FlightState.Servicing:FlightState.Paused,L.MinerRecoverySelfTest);
if(!Connected)
{
var previous=(FlightState)Data.ReadLong(i,"miner","State");
if(previous<FlightState.Boot || previous>FlightState.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;
}
}
}
}
}