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

AutoMiningScript

【SpaceEngineer】全自动挖矿脚本

公开
关注 0 Fork 0 Star 0
UTF-8
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;
                }
            }
        }
    }
}