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;
using VRage.Game.ModAPI.Ingame;
using VRage.Game.ModAPI.Ingame.Utilities;

namespace AutoMiningScript
{
    public partial class Program
    {
        public sealed class Resources
        {
            readonly Program p;
            readonly ShipHardware h;
            readonly List<MyInventoryItem> items = Data.CreateList<MyInventoryItem>();
            readonly Dictionary<string, double> pendingOres = Data.CreateDictionary<string, double>();
            public readonly Dictionary<string, double> Ores = Data.CreateDictionary<string, double>();
            public double Battery, StoredMWh, CapacityMWh, NetMW, Hydrogen = 1, Cargo, DrillCargo, OreAmount;
            public double ReturnBudgetMWh, SortieBudgetMWh, EnduranceSeconds;
            public double InputMW, OutputMW, HydrogenLitres, HydrogenCapacity, HydrogenLitresPerSecond;
            public double CruiseMW, HoverMW, DrillingMW, ReturnMW;
            public bool NeedsCharge, NeedsHydrogen, Empty, SamplingValid;
            public bool DepartureReady;
            public int ServiceFlags => !inventorySnapshotValid || ScanProblem.Length > 0 ? -1 : (OreAmount >= 0.000001 ? 1 : 0) | (NeedsCharge || NeedsHydrogen ? 2 : 0);
            public readonly double DepartureFill;
            public string ScanProblem = "";
            double previousHydrogen = -1, returnFuel, sortieFuel, lastProgress;
            double previousServiceOre = -1, previousServiceEnergy = -1, previousServiceFuel = -1, previousServiceCargo = -1;
            double lastNotice = -1000;
            double lastInventoryAt = -100;
            int emptyScans, departureScans;
            bool wasServicing, sampleInvalid, inventorySnapshotValid;
            bool Connected => h.Connector != null && h.Connector.Status == MyShipConnectorStatus.Connected;

            public Resources(Program program, ShipHardware hardware)
            {
                p = program; h = hardware;
                var returnFill = CargoReturnFill();
                DepartureFill = p.Config.Number("Cargo", "DepartureFill", 0.05, 0, returnFill);
                if (DepartureFill >= returnFill) throw Data.Invalid(L.MinerDepartureFillInvalid);
                CruiseMW = HoverMW = DrillingMW = ReturnMW = p.Config.Number("Energy", "InitialMW", 0.5, 0.01, 10000);
            }

            public void Update(double dt, FlightState state, double distanceHome, double remainingDepth)
            {
                ValidateDeparturePermit();
                DepartureReady = false;
                try { UpdateCore(dt, state, distanceHome, remainingDepth); }
                catch
                {
                    ClearDeparturePermit(); emptyScans = 0; Empty = false; inventorySnapshotValid = false;
                    h.SetInventoryMass(0, false); throw;
                }
            }

            void UpdateCore(double dt, FlightState state, double distanceHome, double remainingDepth)
            {
                dt = Data.Max(0.01, Data.Min(5, dt));
                StoredMWh = CapacityMWh = InputMW = OutputMW = 0;
                for (int i = 0; i < h.Batteries.Count; i++)
                {
                    var battery = h.Batteries[i];
                    if (battery.CubeGrid != p.Me.CubeGrid || !battery.IsFunctional) continue;
                    double stored = battery.CurrentStoredPower, capacity = battery.MaxStoredPower, input = battery.CurrentInput, output = battery.CurrentOutput;
                    Require(NonnegativeFinite(stored) && NonnegativeFinite(capacity) && NonnegativeFinite(input) && NonnegativeFinite(output),L.MinerDepartureEnergyInvalid);
                    StoredMWh += stored; CapacityMWh += capacity; InputMW += input; OutputMW += output;
                }
                Battery = StoredFraction(StoredMWh, CapacityMWh);
                NetMW = OutputMW - InputMW;
                var connected = Connected;
                double power = Data.Max(0.01, NetMW), alpha = 1 - Math.Exp(-dt / 20);
                if (!connected)
                {
                    if (state == FlightState.Drilling) DrillingMW = Learn(DrillingMW, power, alpha);
                    else if (state == FlightState.Returning || state == FlightState.DockAlign || state == FlightState.DockApproach) ReturnMW = Learn(ReturnMW, power, alpha);
                    else if (state == FlightState.Transit || state == FlightState.Departing || state == FlightState.Survey) CruiseMW = Learn(CruiseMW, power, alpha);
                    else HoverMW = Learn(HoverMW, power, alpha);
                }
                EnduranceSeconds = NetMW > 0.001 ? StoredMWh / NetMW * 3600 : -1;
                HydrogenLitres = HydrogenCapacity = 0;
                for (int i = 0; i < h.Tanks.Count; i++)
                {
                    var tank = h.Tanks[i];
                    if (tank.CubeGrid != p.Me.CubeGrid || !tank.IsFunctional) continue;
                    double capacity = tank.Capacity, ratio = tank.FilledRatio;
                    Require(NonnegativeFinite(capacity) && NonnegativeFinite(ratio) && ratio <= 1,L.MinerDepartureEnergyInvalid);
                    HydrogenCapacity += capacity; HydrogenLitres += capacity * ratio;
                }
                Hydrogen = HydrogenCapacity > 0 ? StoredFraction(HydrogenLitres, HydrogenCapacity) : h.UsesHydrogen ? 0 : 1;
                if (!connected && previousHydrogen >= 0 && previousHydrogen >= HydrogenLitres)
                {
                    var measured = (previousHydrogen - HydrogenLitres) / dt;
                    if (measured > 0.001) HydrogenLitresPerSecond = Learn(HydrogenLitresPerSecond, measured, alpha);
                }
                previousHydrogen = HydrogenLitres;
                if (h.UsesHydrogen && HydrogenLitresPerSecond <= 0)
                    HydrogenLitresPerSecond = HydrogenCapacity / p.Config.Number("Energy", "InitialHydrogenEndurance", 1200, 60, 86400);
                ReadInventories(state == FlightState.Drilling);
                if (ScanProblem.Length == 0 && connected && OreAmount < 0.000001) emptyScans = Data.Min(2, emptyScans + 1); else emptyScans = 0;
                Empty = emptyScans >= 2;
                var speed = p.Config.Number("Flight", "CruiseSpeed", 15, 0.5, 95);
                var retreat = Data.Max(0, remainingDepth) / p.Config.Number("Flight", "RetreatSpeed", 1, 0.1, 5);
                var travel = Data.Max(0, distanceHome) / speed;
                var wait = DockReserveSeconds;
                var factor = ReserveFactor;
                var peak = Data.Max(CruiseMW, ReturnMW);
                ReturnBudgetMWh = (peak * travel + Data.Max(DrillingMW, HoverMW) * retreat + HoverMW * wait) / 3600 * factor;
                ReturnBudgetMWh += EmergencyMWh;
                returnFuel = (travel + retreat + wait) * HydrogenLitresPerSecond * factor + HydrogenCapacity * 0.1;
                if (Battery <= LowBattery() || StoredMWh <= ReturnBudgetMWh) NeedsCharge = true;
                if (h.UsesHydrogen && (Hydrogen <= LowHydrogen() || HydrogenLitres <= returnFuel)) NeedsHydrogen = true;
                if (connected && inventorySnapshotValid && ScanProblem.Length == 0 && DrillCargo < 0.98 &&
                    (DepartureFill == 0 ? OreAmount < 0.000001 : Cargo <= DepartureFill)) departureScans = Data.Min(2, departureScans + 1);
                else departureScans = 0;
                DepartureReady = departureScans >= 2;
            }

            static double Learn(double average, double value, double alpha)
            => value > average ? average + (value - average) * Data.Max(0.35, alpha) : average + (value - average) * alpha;

            void ReadInventories(bool sampling)
            {
                // Only publish a complete bounded snapshot. A failed scan must never leave
                // a partial mineral sample or an old "empty" permit available to departure.
                Empty = SamplingValid = DepartureReady = inventorySnapshotValid = false;
                if (ScanProblem.Length > 0) return;
                pendingOres.Clear();
                double volume = 0, capacity = 0, inventoryMass = 0;
                double drillCargo = 0, oreAmount = 0;
                bool massValid = true, samplingValid = h.SamplingIsolated && !sampleInvalid;
                int inventories = 0, stacks = 0;
                h.SetInventoryMass(0, false);
                try
                {
                    Require(h.InventoryBlocks.Count <= 512,L.MinerInventoryScanLimit);
                    for (int b = 0; b < h.InventoryBlocks.Count; b++)
                    {
                        var block = h.InventoryBlocks[b];
                        if (block.CubeGrid != p.Me.CubeGrid) { samplingValid = false; continue; }
                        Require(block.HasInventory && !block.Closed,L.MinerDepartureInventoryInvalid);
                        bool cargoInventory = Data.CargoInventory(block);
                        for (int j = 0; j < block.InventoryCount; j++)
                        {
                            Require(++inventories <= 128,L.MinerInventoryScanLimit);
                            var inventory = block.GetInventory(j);
                            var currentMass = (double)inventory.CurrentMass;
                            if (!Data.Finite(currentMass) || currentMass < 0) massValid = false;
                            else inventoryMass += currentMass;
                            // Fuel and production supplies still weigh down the ship, but
                            // only transport inventories contribute to ore and cargo fill.
                            if (!cargoInventory) continue;
                            Require(inventory.ItemCount <= 512 - stacks,L.MinerInventoryScanLimit);
                            double v = (double)inventory.CurrentVolume, c = (double)inventory.MaxVolume;
                            Require(NonnegativeFinite(v) && NonnegativeFinite(c),L.MinerDepartureInventoryInvalid);
                            volume += v; capacity += c;
                            if (block is IMyShipDrill && c > 0) drillCargo = Data.Max(drillCargo, v / c);
                            items.Clear(); inventory.GetItems(items); stacks += items.Count;
                            Require(stacks <= 512,L.MinerInventoryScanLimit);
                            for (int k = 0; k < items.Count; k++)
                            {
                                var item = items[k];
                                if (item.Type.TypeId != "MyObjectBuilder_Ore") continue;
                                var ore = item.Type.SubtypeId;
                                double amount = (double)item.Amount, old;
                                oreAmount += amount;
                                pendingOres.TryGetValue(ore, out old); pendingOres[ore] = old + amount;
                            }
                        }
                    }
                    if (sampling)
                    {
                        h.SetSampling(true);
                        samplingValid = samplingValid && h.SamplingIsolated;
                        foreach (var pair in Ores)
                        {
                            double amount; pendingOres.TryGetValue(pair.Key, out amount);
                            if (amount + 0.000001 < pair.Value) { samplingValid = false; break; }
                        }
                        if (!samplingValid) sampleInvalid = true;
                    }
                    Ores.Clear();
                    foreach (var pair in pendingOres) Ores[pair.Key] = pair.Value;
                    OreAmount = oreAmount; DrillCargo = drillCargo; SamplingValid = samplingValid;
                    h.SetInventoryMass(inventoryMass, massValid);
                    Cargo = capacity > 0 ? Data.Min(1, volume / capacity) : 1;
                    double multiplier = p.World == null ? 1 : p.World.InventoryMultiplier;
                    inventorySnapshotValid = massValid && Data.Finite(multiplier) && multiplier > 0 && (DepartureFill == 0 || capacity > 0);
                    lastInventoryAt = p.Now;
                }
                catch (Exception error)
                {
                    items.Clear(); pendingOres.Clear(); emptyScans = 0; ClearDeparturePermit(); sampleInvalid = true;
                    ScanProblem = error.Message;
                }
            }

            public void ResetInventoryScan()
            {
                ScanProblem = ""; Empty = SamplingValid = inventorySnapshotValid = false; emptyScans = 0; lastInventoryAt = -100; ClearDeparturePermit();
                items.Clear(); pendingOres.Clear(); h.SetInventoryMass(0, false);
            }

            public void ResetSampleValidation() { sampleInvalid = false; SamplingValid = h.SamplingIsolated; }
            public static double StoredFraction(double stored, double capacity)
            => capacity > 0 ? Data.Max(0, Data.Min(1, stored / capacity)) : 0;
            public static double TripEnergy(double cruiseMW, double returnMW, double drillMW, double hoverMW,
                double travelSeconds, double drillSeconds, double retreatSeconds, double waitSeconds, double reserveFactor, double emergencyMWh)
            => ((cruiseMW + returnMW) * travelSeconds * 0.5 + drillMW * drillSeconds + Data.Max(cruiseMW, hoverMW) * retreatSeconds + hoverMW * waitSeconds) / 3600 * reserveFactor + emergencyMWh;

            double LowBattery() => p.Config.Number("Energy", "ReturnBattery", p.Config.Number("Energy", "LowBattery", 0.25, 0.05, 0.8), 0.05, 0.8);
            double LowHydrogen() => p.Config.Number("Energy", "LowHydrogen", 0.25, 0.05, 0.8);
            double CargoReturnFill() => p.Config.Number("Cargo", "ReturnFill", p.Config.Number("Mining", "CargoReturn", 0.95, 0.2, 0.99), 0.2, 0.99);
            double DockReserveSeconds => p.Config.Number("Energy", "DockReserveSeconds", 120, 30, 3600);
            double ReserveFactor => p.Config.Number("Energy", "ReserveFactor", 1.5, 1.1, 5);
            double EmergencyMWh => CapacityMWh * p.Config.Number("Energy", "EmergencyReserve", 0.1, 0.02, 0.4);

            void ClearDeparturePermit() { DepartureReady = false; departureScans = 0; }
            void ValidateDeparturePermit()
            {
                if (!Connected || ScanProblem.Length > 0 ||
                    !inventorySnapshotValid || p.Now - lastInventoryAt > 0.75 || p.Now < lastInventoryAt || DrillCargo >= 0.98 ||
                    (DepartureFill == 0 ? OreAmount >= 0.000001 : Cargo > DepartureFill)) ClearDeparturePermit();
            }
            public string DepartureWaitReason
            {
                get
                {
                    ValidateDeparturePermit();
                    if (DepartureReady) return "";
                    if (ScanProblem.Length > 0) return ScanProblem;
                    if (!Connected) return L.MinerDepartureDisconnected;
                    if (!inventorySnapshotValid) return L.MinerDepartureInventoryInvalid;
                    if (DrillCargo >= 0.98) return L.MinerDrillBlocked;
                    if (DepartureFill == 0 && OreAmount >= 0.000001) return L.MinerWaitWholeShipUnload;
                    if (DepartureFill > 0 && Cargo > DepartureFill) return L.F(L.MinerWaitDepartureFill, Cargo, DepartureFill);
                    return L.MinerWaitDepartureScans;
                }
            }

            bool CargoReady(out string reason)
            {
                if (ScanProblem.Length > 0) { reason = ScanProblem; return false; }
                if (Cargo >= CargoReturnFill()) { reason = L.MinerCargoFull; return false; }
                reason="";return true;
            }
            public bool MustReturn(out string reason)
            {
                if(!CargoReady(out reason))return true;
                if (DrillCargo >= 0.98) { reason = L.MinerDrillBlocked; return true; }
                if (Battery <= LowBattery()) { NeedsCharge = true; reason = L.MinerBatteryLow; return true; }
                if (StoredMWh <= ReturnBudgetMWh) { NeedsCharge = true; reason = L.MinerReturnEnergyLow; return true; }
                if (h.UsesHydrogen && (Hydrogen <= LowHydrogen() || HydrogenLitres <= returnFuel))
                { NeedsHydrogen = true; reason = L.MinerReturnHydrogenLow; return true; }
                reason = ""; return false;
            }

            public bool ReadyForSortie(double distance, double depth, out string reason, bool airborne = false)
            {
                ValidateDeparturePermit();
                // Even strict ore-only unloading must not launch a ship already above
                // its return threshold because of retained supplies.
                if(!CargoReady(out reason))return false;
                var speed = p.Config.Number("Flight", "CruiseSpeed", 15, 0.5, 95);
                var drillSpeed = p.Config.Number("Mining", "DrillSpeed", 0.5, 0.05, 2);
                var retreatSpeed = p.Config.Number("Flight", "RetreatSpeed", 1, 0.1, 5);
                var travelSeconds = Data.Max(0, distance) * 2 / speed;
                var drillSeconds = Data.Max(0, depth) / drillSpeed;
                var retreatSeconds = Data.Max(0, depth) / retreatSpeed;
                var wait = DockReserveSeconds;
                var factor = ReserveFactor;
                SortieBudgetMWh = TripEnergy(CruiseMW, ReturnMW, DrillingMW, HoverMW, travelSeconds, drillSeconds, retreatSeconds,
                    wait, factor, EmergencyMWh);
                sortieFuel = (travelSeconds + drillSeconds + retreatSeconds + wait) * HydrogenLitresPerSecond * factor + HydrogenCapacity * 0.1;
                if (CapacityMWh <= 0 || SortieBudgetMWh > CapacityMWh * 0.99 || (h.UsesHydrogen && sortieFuel > HydrogenCapacity * 0.99))
                { reason = L.MinerSortieOutOfRange; return false; }
                if (airborne)
                {
                    if (MustReturn(out reason)) return false;
                    if (StoredMWh < SortieBudgetMWh) { reason = L.MinerReturnEnergyLow; return false; }
                    if (h.UsesHydrogen && HydrogenLitres < sortieFuel) { reason = L.MinerReturnHydrogenLow; return false; }
                    reason = ""; return true;
                }
                if (StoredMWh < SortieBudgetMWh || Battery <= LowBattery()) NeedsCharge = true;
                if (h.UsesHydrogen && (HydrogenLitres < sortieFuel || Hydrogen <= LowHydrogen())) NeedsHydrogen = true;
                FinishCharging();
                if (!DepartureReady) { reason = DepartureWaitReason; return false; }
                if (NeedsCharge) { reason = L.MinerWaitChargeTarget; return false; }
                if (NeedsHydrogen) { reason = L.MinerWaitHydrogenTarget; return false; }
                reason = ""; return true;
            }

            void FinishCharging()
            {
                if (Battery >= p.Config.Number("Energy", "ChargeTarget", 0.9, 0.5, 1) && StoredMWh >= Data.Min(CapacityMWh, SortieBudgetMWh)) NeedsCharge = false;
                if (Hydrogen >= p.Config.Number("Energy", "HydrogenTarget", 0.9, 0.5, 1) && HydrogenLitres >= Data.Min(HydrogenCapacity, sortieFuel)) NeedsHydrogen = false;
            }
            public void Service(double dt)
            {
                ValidateDeparturePermit();
                if (!Connected)
                { wasServicing = false; h.SetCharging(false); h.SetRefilling(false); return; }
                h.SetDrills(false); h.SetSampling(false);
                // Finish replenishing even when no next job calls ReadyForSortie.
                // A new sortie still validates its own complete energy budget.
                FinishCharging();
                // Auto still accepts surplus base power. Do not force local battery-to-battery
                // recharge when the connected base cannot supply the ship's existing load.
                h.SetCharging(NeedsCharge && InputMW > OutputMW + 0.001); h.SetRefilling(h.UsesHydrogen && NeedsHydrogen);
                if (!wasServicing || OreAmount < previousServiceOre - 0.0001 || Cargo < previousServiceCargo - 0.000001 || StoredMWh > previousServiceEnergy + 0.000001 || HydrogenLitres > previousServiceFuel + 0.01)
                { lastProgress = p.Now; previousServiceOre = OreAmount; previousServiceEnergy = StoredMWh; previousServiceFuel = HydrogenLitres; previousServiceCargo = Cargo; }
                wasServicing = true;
                if ((!DepartureReady || NeedsCharge || NeedsHydrogen) && p.Now - lastProgress > 120 && p.Now - lastNotice > 120)
                {
                    p.Log(!DepartureReady ? L.MinerUnloadStalled : NeedsCharge ? L.MinerChargeStalled : L.MinerHydrogenStalled);
                    lastNotice = p.Now;
                }
            }

            public void Save(MyIni ini)
            {
                var section="EnergyState";
                ini.Set(section, "NeedsCharge", NeedsCharge); ini.Set(section, "NeedsHydrogen", NeedsHydrogen);
                ini.Set(section, "CruiseMW", CruiseMW); ini.Set(section, "HoverMW", HoverMW);
                ini.Set(section, "DrillingMW", DrillingMW); ini.Set(section, "ReturnMW", ReturnMW);
                ini.Set(section, "HydrogenRate", HydrogenLitresPerSecond);
            }
            public void Load(MyIni ini)
            {
                ClearDeparturePermit(); inventorySnapshotValid = false; lastInventoryAt = -100;
                NeedsCharge = Data.Flag(ini,"EnergyState", "NeedsCharge");
                NeedsHydrogen = Data.Flag(ini,"EnergyState", "NeedsHydrogen");
                CruiseMW = RestoreEnergy(ini,"CruiseMW",CruiseMW);
                HoverMW = RestoreEnergy(ini,"HoverMW",HoverMW);
                DrillingMW = RestoreEnergy(ini,"DrillingMW",DrillingMW);
                ReturnMW = RestoreEnergy(ini,"ReturnMW",ReturnMW);
                HydrogenLitresPerSecond = RestoreEnergy(ini,"HydrogenRate",0);
                emptyScans = 0; Empty = false;
            }
            static double RestoreEnergy(MyIni ini,string key,double fallback)
            {
                var value=ini.Get("EnergyState",key).ToDouble(fallback);
                return !NonnegativeFinite(value) || value > 1e9 ? fallback : value;
            }
            static void Require(bool valid,string message) {if(!valid)throw new InvalidOperationException(message);}
            static bool NonnegativeFinite(double value) => Data.Finite(value) && value >= 0;
        }
    }
}