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 items = Data.CreateList(); readonly Dictionary pendingOres = Data.CreateDictionary(); public readonly Dictionary Ores = Data.CreateDictionary(); 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; } } }