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;
}
}
}
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;
}
}
}