Skip to content

Instantly share code, notes, and snippets.

@ZerothAngel
Created August 15, 2015 16:53
Show Gist options
  • Save ZerothAngel/651d09f7fc0d44f7d30d to your computer and use it in GitHub Desktop.
Save ZerothAngel/651d09f7fc0d44f7d30d to your computer and use it in GitHub Desktop.
Space Engineers Generic Solar-Powered Satellite Controller
// Generated from ZerothAngel's SEScripts version 11998d7043c3 tip
// Modules: satellitecontroller, batterymanager, solargyrocontroller, gyrocontrol, thrustcontrol, shiporientation, shipcontrol, eventdriver, commons
const string BATTERY_MANAGER_DISCHARGE_INTERVAL = "00:00:57"; // HH:MM[:SS]
const string BATTERY_MANAGER_RECHARGE_INTERVAL = "00:00:03"; // HH:MM[:SS]
// BatteryManager
const uint BATTERY_MANAGER_DRAIN_CHECK_TICKS = 120;
const uint BATTERY_MANAGER_DRAIN_CHECK_THRESHOLD = 100;
// SolarGyroController
const float SOLAR_GYRO_VELOCITY = 0.05f;
// ZACommons
const string STANDARD_LOOP_TIMER_BLOCK_NAME = "1 second loop";
public class SatellitePowerDrainHandler : BatteryManager.PowerDrainHandler
{
private const string Message = "HELP! NET POWER LOSS!";
private string OldAntennaName;
public void PowerDrainStarted(ZACommons commons)
{
// Just change the name of the first active antenna
for (var e = ZACommons.GetBlocksOfType<IMyRadioAntenna>(commons.Blocks).GetEnumerator(); e.MoveNext();)
{
var antenna = e.Current;
if (antenna.IsFunctional && antenna.IsWorking)
{
OldAntennaName = antenna.CustomName;
antenna.SetCustomName(Message);
break;
}
}
}
public void PowerDrainEnded(ZACommons commons)
{
// Scan for the antenna with the message, change it back
for (var e = ZACommons.GetBlocksOfType<IMyRadioAntenna>(commons.Blocks).GetEnumerator(); e.MoveNext();)
{
var antenna = e.Current;
if (antenna.CustomName == Message)
{
antenna.SetCustomName(OldAntennaName);
break;
}
}
}
}
public readonly EventDriver eventDriver = new EventDriver(timerName: STANDARD_LOOP_TIMER_BLOCK_NAME);
public readonly BatteryManager batteryManager = new BatteryManager(new SatellitePowerDrainHandler());
public readonly SolarGyroController solarGyroController = new SolarGyroController(
// GyroControl.Yaw,
GyroControl.Pitch,
GyroControl.Roll
);
private readonly ShipOrientation shipOrientation = new ShipOrientation();
private bool FirstRun = true;
void Main(string argument)
{
var commons = new ShipControlCommons(this, shipOrientation);
if (FirstRun)
{
FirstRun = false;
shipOrientation.SetShipReference<IMyShipController>(commons.Blocks);
eventDriver.Schedule(0.0);
}
batteryManager.HandleCommand(commons, argument);
solarGyroController.HandleCommand(commons, argument);
eventDriver.Tick(commons, () =>
{
batteryManager.Run(commons);
solarGyroController.Run(commons);
eventDriver.Schedule(1.0);
});
}
// Assumptions: No reactor, only batteries and solar panels
// Solar panels can power entire system no problem
// Thus batteries only serve as backup. They should almost always be discharging
// (because output from solar panels will be prioritized anyway).
public class BatteryManager
{
public struct AggregateBatteryDetails
{
public float CurrentPowerOutput;
public float MaxPowerOutput;
public float CurrentStoredPower;
public float MaxStoredPower;
public AggregateBatteryDetails(IEnumerable<IMyBatteryBlock> batteries)
{
CurrentPowerOutput = 0.0f;
MaxPowerOutput = 0.0f;
CurrentStoredPower = 0.0f;
MaxStoredPower = 0.0f;
for (var e = batteries.GetEnumerator(); e.MoveNext();)
{
var battery = e.Current;
CurrentPowerOutput += battery.CurrentPowerOutput;
MaxPowerOutput += battery.MaxPowerOutput;
CurrentStoredPower += battery.CurrentStoredPower;
MaxStoredPower += battery.MaxStoredPower;
}
}
}
public interface PowerDrainHandler
{
void PowerDrainStarted(ZACommons commons);
void PowerDrainEnded(ZACommons commons);
}
private const int STATE_DISABLED = -1;
private const int STATE_NORMAL = 0;
private const int STATE_RECHARGE = 1;
private readonly TimeSpan DischargeInterval = TimeSpan.Parse(BATTERY_MANAGER_DISCHARGE_INTERVAL);
private readonly TimeSpan RechargeInterval = TimeSpan.Parse(BATTERY_MANAGER_RECHARGE_INTERVAL);
private int? CurrentState = null;
private TimeSpan SinceLastStateChange;
private bool Active = true;
private PowerDrainHandler powerDrainHandler;
private bool Draining = false;
private uint DrainCounts = 0;
private readonly LinkedList<bool> DrainData = new LinkedList<bool>();
public BatteryManager(PowerDrainHandler powerDrainHandler = null)
{
this.powerDrainHandler = powerDrainHandler;
}
private bool AddDrainData(bool draining)
{
DrainData.AddLast(draining);
if (draining) DrainCounts++;
while (DrainData.Count > BATTERY_MANAGER_DRAIN_CHECK_TICKS)
{
// Forget about oldest value, adjust count if necessary
if (DrainData.First.Value) DrainCounts--;
DrainData.RemoveFirst();
}
return DrainCounts >= BATTERY_MANAGER_DRAIN_CHECK_THRESHOLD;
}
private List<IMyBatteryBlock> GetBatteries(ZACommons commons)
{
return ZACommons.GetBlocksOfType<IMyBatteryBlock>(commons.Blocks, battery => battery.IsFunctional && battery.Enabled);
}
public void Run(ZACommons commons)
{
var batteries = GetBatteries(commons);
if (CurrentState == null)
{
// First time run, get to known state and return
CurrentState = STATE_NORMAL;
SinceLastStateChange = TimeSpan.FromSeconds(0);
ZACommons.SetBatteryRecharge(batteries, false);
return;
}
SinceLastStateChange += commons.Program.ElapsedTime;
var aggregateDetails = new AggregateBatteryDetails(batteries);
string stateStr = "Unknown";
switch (CurrentState)
{
case STATE_NORMAL:
if (SinceLastStateChange >= DischargeInterval)
{
// Don't check again until next interval, regardless of whether we
// change state
SinceLastStateChange = TimeSpan.FromSeconds(0);
// Only recharge if there is available power, e.g. the batteries have no load,
// and there is need to
if (aggregateDetails.CurrentPowerOutput == 0.0f &&
aggregateDetails.CurrentStoredPower < aggregateDetails.MaxStoredPower &&
Active)
{
CurrentState = STATE_RECHARGE;
ZACommons.SetBatteryRecharge(batteries, true);
}
else
{
// Force discharge, just in case
ZACommons.SetBatteryRecharge(batteries, false);
}
}
stateStr = Active ? "Normal" : "Paused";
break;
case STATE_RECHARGE:
// Too bad we don't have access to battery input (w/o parsing DetailInfo)
// Then we could figure out non-battery load and cancel recharge pre-emptively
// when needed
if (SinceLastStateChange >= RechargeInterval)
{
CurrentState = STATE_NORMAL;
SinceLastStateChange = TimeSpan.FromSeconds(0);
ZACommons.SetBatteryRecharge(batteries, false);
}
stateStr = "Recharging";
break;
case STATE_DISABLED:
// Switch back to auto if full
if (aggregateDetails.CurrentStoredPower >= aggregateDetails.MaxStoredPower)
{
CurrentState = STATE_NORMAL;
SinceLastStateChange = TimeSpan.FromSeconds(0);
ZACommons.SetBatteryRecharge(batteries, false);
}
stateStr = "Disabled";
break;
}
// See if we have a net power loss
var newDraining = AddDrainData(aggregateDetails.CurrentPowerOutput > 0.0f);
if (powerDrainHandler != null)
{
if (!Draining && newDraining)
{
powerDrainHandler.PowerDrainStarted(commons);
}
else if (Draining && !newDraining)
{
powerDrainHandler.PowerDrainEnded(commons);
}
}
Draining = newDraining;
commons.Echo(string.Format("Battery Manager: {0}", stateStr));
commons.Echo(string.Format("Total Stored Power: {0}h", ZACommons.FormatPower(aggregateDetails.CurrentStoredPower)));
commons.Echo(string.Format("Max Stored Power: {0}h", ZACommons.FormatPower(aggregateDetails.MaxStoredPower)));
if (Draining) commons.Echo("Net power loss!");
}
public void HandleCommand(ZACommons commons, string argument)
{
argument = argument.Trim().ToLower();
if (argument == "forcerecharge")
{
var batteries = GetBatteries(commons);
CurrentState = STATE_DISABLED;
ZACommons.SetBatteryRecharge(batteries, true);
return;
}
else if (argument == "pause")
{
CurrentState = null;
Active = false;
}
else if (argument == "resume")
{
CurrentState = null;
Active = true;
}
}
}
public class SolarGyroController
{
public struct SolarPanelDetails
{
public float MaxPowerOutput;
public float DefinedPowerOutput;
public SolarPanelDetails(IEnumerable<IMyTerminalBlock> blocks)
{
MaxPowerOutput = 0.0f;
DefinedPowerOutput = 0.0f;
for (var e = ZACommons.GetBlocksOfType<IMySolarPanel>(blocks).GetEnumerator(); e.MoveNext();)
{
var panel = e.Current;
if (panel.IsFunctional && panel.IsWorking)
{
MaxPowerOutput += panel.MaxPowerOutput;
DefinedPowerOutput += panel.DefinedPowerOutput;
}
}
}
}
private readonly int[] AllowedAxes;
private readonly float[] LastVelocities;
private readonly TimeSpan AxisTimeout = TimeSpan.FromSeconds(15);
private float? MaxPower = null;
private int AxisIndex = 0;
private bool Active = true;
private TimeSpan TimeOnAxis;
public SolarGyroController(params int[] allowedAxes)
{
// Weird things happening with array constants
AllowedAxes = (int[])allowedAxes.Clone();
LastVelocities = new float[AllowedAxes.Length];
for (int i = 0; i < LastVelocities.Length; i++)
{
LastVelocities[i] = SOLAR_GYRO_VELOCITY;
}
}
public void Run(ShipControlCommons commons)
{
if (!Active)
{
commons.Echo("Solar Max Power: Paused");
return;
}
var gyroControl = commons.GyroControl;
var currentAxis = AllowedAxes[AxisIndex];
if (MaxPower == null)
{
MaxPower = -100.0f; // Start with something absurdly low to kick things off
gyroControl.Reset();
gyroControl.EnableOverride(true);
gyroControl.SetAxisVelocity(currentAxis, LastVelocities[AxisIndex]);
TimeOnAxis = TimeSpan.FromSeconds(0);
}
var solarPanelDetails = new SolarPanelDetails(commons.Blocks);
var currentMaxPower = solarPanelDetails.MaxPowerOutput;
var minError = solarPanelDetails.DefinedPowerOutput * 0.005f; // From experimentation
var delta = currentMaxPower - MaxPower;
MaxPower = currentMaxPower;
if (delta > minError)
{
// Keep going
gyroControl.EnableOverride(true);
}
else if (delta < -minError)
{
// Back up
gyroControl.EnableOverride(true);
LastVelocities[AxisIndex] = -LastVelocities[AxisIndex];
gyroControl.SetAxisVelocity(currentAxis, LastVelocities[AxisIndex]);
}
else
{
// Hold still
gyroControl.EnableOverride(false);
}
TimeOnAxis += commons.Program.ElapsedTime;
if (TimeOnAxis > AxisTimeout)
{
// Time out, try next axis
AxisIndex++;
AxisIndex %= AllowedAxes.Length;
gyroControl.Reset();
gyroControl.EnableOverride(true);
gyroControl.SetAxisVelocity(AllowedAxes[AxisIndex], LastVelocities[AxisIndex]);
TimeOnAxis = TimeSpan.FromSeconds(0);
}
commons.Echo(string.Format("Solar Max Power: {0}", ZACommons.FormatPower(currentMaxPower)));
}
public void HandleCommand(ShipControlCommons commons, string argument)
{
// Handle commands
argument = argument.Trim().ToLower();
if (argument == "pause")
{
Active = false;
commons.GyroControl.EnableOverride(false);
}
else if (argument == "resume")
{
Active = true;
MaxPower = null; // Use first-run initialization
}
}
}
public class GyroControl
{
public const int Yaw = 0;
public const int Pitch = 1;
public const int Roll = 2;
public readonly string[] AxisNames = new string[] { "Yaw", "Pitch", "Roll" };
public struct GyroAxisDetails
{
public int LocalAxis;
public int Sign;
public GyroAxisDetails(int localAxis, int sign)
{
LocalAxis = localAxis;
Sign = sign;
}
}
public struct GyroDetails
{
public IMyGyro Gyro;
public GyroAxisDetails[] AxisDetails;
public GyroDetails(IMyGyro gyro, Base6Directions.Direction shipUp,
Base6Directions.Direction shipForward)
{
Gyro = gyro;
AxisDetails = new GyroAxisDetails[3];
var shipLeft = Base6Directions.GetLeft(shipUp, shipForward);
// Determine yaw axis
switch (gyro.Orientation.TransformDirectionInverse(shipUp))
{
case Base6Directions.Direction.Up:
AxisDetails[Yaw] = new GyroAxisDetails(Yaw, -1);
break;
case Base6Directions.Direction.Down:
AxisDetails[Yaw] = new GyroAxisDetails(Yaw, 1);
break;
case Base6Directions.Direction.Left:
AxisDetails[Yaw] = new GyroAxisDetails(Pitch, 1);
break;
case Base6Directions.Direction.Right:
AxisDetails[Yaw] = new GyroAxisDetails(Pitch, -1);
break;
case Base6Directions.Direction.Forward:
AxisDetails[Yaw] = new GyroAxisDetails(Roll, 1);
break;
case Base6Directions.Direction.Backward:
AxisDetails[Yaw] = new GyroAxisDetails(Roll, -1);
break;
}
// Determine pitch axis
switch (gyro.Orientation.TransformDirectionInverse(shipLeft))
{
case Base6Directions.Direction.Up:
AxisDetails[Pitch] = new GyroAxisDetails(Yaw, -1);
break;
case Base6Directions.Direction.Down:
AxisDetails[Pitch] = new GyroAxisDetails(Yaw, 1);
break;
case Base6Directions.Direction.Left:
AxisDetails[Pitch] = new GyroAxisDetails(Pitch, -1);
break;
case Base6Directions.Direction.Right:
AxisDetails[Pitch] = new GyroAxisDetails(Pitch, 1);
break;
case Base6Directions.Direction.Forward:
AxisDetails[Pitch] = new GyroAxisDetails(Roll, 1);
break;
case Base6Directions.Direction.Backward:
AxisDetails[Pitch] = new GyroAxisDetails(Roll, -1);
break;
}
// Determine roll axis
switch (gyro.Orientation.TransformDirectionInverse(shipForward))
{
case Base6Directions.Direction.Up:
AxisDetails[Roll] = new GyroAxisDetails(Yaw, -1);
break;
case Base6Directions.Direction.Down:
AxisDetails[Roll] = new GyroAxisDetails(Yaw, 1);
break;
case Base6Directions.Direction.Left:
AxisDetails[Roll] = new GyroAxisDetails(Pitch, -1);
break;
case Base6Directions.Direction.Right:
AxisDetails[Roll] = new GyroAxisDetails(Pitch, 1);
break;
case Base6Directions.Direction.Forward:
AxisDetails[Roll] = new GyroAxisDetails(Roll, 1);
break;
case Base6Directions.Direction.Backward:
AxisDetails[Roll] = new GyroAxisDetails(Roll, -1);
break;
}
}
}
private readonly List<GyroDetails> gyros = new List<GyroDetails>();
public void Init(IEnumerable<IMyTerminalBlock> blocks,
Func<IMyGyro, bool> collect = null,
Base6Directions.Direction shipUp = Base6Directions.Direction.Up,
Base6Directions.Direction shipForward = Base6Directions.Direction.Forward)
{
gyros.Clear();
for (var e = blocks.GetEnumerator(); e.MoveNext();)
{
var gyro = e.Current as IMyGyro;
if (gyro != null &&
gyro.IsFunctional && gyro.IsWorking && gyro.Enabled &&
(collect == null || collect(gyro)))
{
var details = new GyroDetails(gyro, shipUp, shipForward);
gyros.Add(details);
}
}
}
public void EnableOverride(bool enable)
{
gyros.ForEach(gyro => gyro.Gyro.SetValue<bool>("Override", enable));
}
public void SetAxisVelocity(int axis, float velocity)
{
gyros.ForEach(gyro => gyro.Gyro.SetValue<float>(AxisNames[gyro.AxisDetails[axis].LocalAxis], gyro.AxisDetails[axis].Sign * velocity));
}
public void SetAxisVelocityRPM(int axis, float rpmVelocity)
{
SetAxisVelocity(axis, rpmVelocity * MathHelper.RPMToRadiansPerSecond);
}
public void Reset()
{
gyros.ForEach(gyro => {
gyro.Gyro.SetValue<float>("Yaw", 0.0f);
gyro.Gyro.SetValue<float>("Pitch", 0.0f);
gyro.Gyro.SetValue<float>("Roll", 0.0f);
});
}
}
public class ThrustControl
{
private readonly Dictionary<Base6Directions.Direction, List<IMyThrust>> thrusters = new Dictionary<Base6Directions.Direction, List<IMyThrust>>();
private void AddThruster(Base6Directions.Direction direction, IMyThrust thruster)
{
var thrusterList = GetThrusters(direction);
thrusterList.Add(thruster);
}
public void Init(IEnumerable<IMyTerminalBlock> blocks,
Func<IMyThrust, bool> collect = null,
Base6Directions.Direction shipUp = Base6Directions.Direction.Up,
Base6Directions.Direction shipForward = Base6Directions.Direction.Forward)
{
MyBlockOrientation shipOrientation = new MyBlockOrientation(shipForward, shipUp);
thrusters.Clear();
for (var e = blocks.GetEnumerator(); e.MoveNext();)
{
var thruster = e.Current as IMyThrust;
if (thruster != null && thruster.IsFunctional &&
(collect == null || collect(thruster)))
{
var facing = thruster.Orientation.TransformDirection(Base6Directions.Direction.Forward); // Exhaust goes this way
var thrustDirection = Base6Directions.GetFlippedDirection(facing);
var shipDirection = shipOrientation.TransformDirectionInverse(thrustDirection);
AddThruster(shipDirection, thruster);
}
}
}
public List<IMyThrust> GetThrusters(Base6Directions.Direction direction)
{
List<IMyThrust> thrusterList;
if (!thrusters.TryGetValue(direction, out thrusterList))
{
thrusterList = new List<IMyThrust>();
thrusters.Add(direction, thrusterList);
}
return thrusterList;
}
public void SetOverride(Base6Directions.Direction direction, bool enable = true)
{
var thrusterList = GetThrusters(direction);
thrusterList.ForEach(thruster =>
thruster.SetValue<float>("Override", enable ?
thruster.GetMaximum<float>("Override") :
0.0f));
}
public void SetOverride(Base6Directions.Direction direction, double percent)
{
percent = Math.Max(percent, 0.0);
percent = Math.Min(percent, 1.0);
var thrusterList = GetThrusters(direction);
thrusterList.ForEach(thruster =>
thruster.SetValue<float>("Override",
(float)(thruster.GetMaximum<float>("Override") * percent)));
}
public void SetOverrideNewtons(Base6Directions.Direction direction, double force)
{
var thrusterList = GetThrusters(direction);
var maxForce = 0.0;
thrusterList.ForEach(thruster =>
maxForce += thruster.GetMaximum<float>("Override"));
// Constrain
force = Math.Max(force, 0.0);
force = Math.Min(force, maxForce);
// Each thruster outputs its own share
var fraction = force / maxForce;
thrusterList.ForEach(thruster =>
thruster.SetValue<float>("Override",
(float)(fraction * thruster.GetMaximum<float>("Override"))));
}
public void Enable(Base6Directions.Direction direction, bool enable)
{
var thrusterList = GetThrusters(direction);
thrusterList.ForEach(thruster => thruster.SetValue<bool>("OnOff", enable));
}
public void Enable(bool enable)
{
for (var e = thrusters.Values.GetEnumerator(); e.MoveNext();)
{
var thrusterList = e.Current;
thrusterList.ForEach(thruster => thruster.SetValue<bool>("OnOff", enable));
}
}
public void Reset()
{
for (var e = thrusters.Values.GetEnumerator(); e.MoveNext();)
{
var thrusterList = e.Current;
thrusterList.ForEach(thruster => thruster.SetValue<float>("Override", 0.0f));
}
}
}
public class ShipOrientation
{
public Base6Directions.Direction ShipUp { get; private set; }
public Base6Directions.Direction ShipForward { get; private set; }
public ShipOrientation()
{
// Defaults
ShipUp = Base6Directions.Direction.Up;
ShipForward = Base6Directions.Direction.Forward;
}
// Use orientation of given block
public void SetShipReference(IMyCubeBlock reference)
{
ShipUp = reference.Orientation.TransformDirection(Base6Directions.Direction.Up);
ShipForward = reference.Orientation.TransformDirection(Base6Directions.Direction.Forward);
}
// Use orientation of a block in the given group
public void SetShipReference(ZACommons commons, string groupName,
Func<IMyTerminalBlock, bool> condition = null)
{
var group = commons.GetBlockGroupWithName(groupName);
if (group != null)
{
for (var e = group.Blocks.GetEnumerator(); e.MoveNext();)
{
var block = e.Current;
if (block.CubeGrid == commons.Me.CubeGrid &&
(condition == null || condition(block)))
{
SetShipReference(block);
return;
}
}
}
// Default to grid up/forward
ShipUp = Base6Directions.Direction.Up;
ShipForward = Base6Directions.Direction.Forward;
}
// Use orientation of a block of the given type
public void SetShipReference<T>(IEnumerable<IMyTerminalBlock> blocks,
Func<T, bool> condition = null)
where T : IMyCubeBlock
{
var references = ZACommons.GetBlocksOfType<T>(blocks, condition);
if (references.Count > 0)
{
SetShipReference(references[0]);
}
else
{
// Default to grid up/forward
ShipUp = Base6Directions.Direction.Up;
ShipForward = Base6Directions.Direction.Forward;
}
}
}
// Do not hold a reference to an instance of this class between runs
public class ShipControlCommons : ZACommons
{
private readonly ShipOrientation shipOrientation;
public Base6Directions.Direction ShipUp
{
get { return shipOrientation.ShipUp; }
}
public Base6Directions.Direction ShipForward
{
get { return shipOrientation.ShipForward; }
}
public ShipControlCommons(MyGridProgram program,
ShipOrientation shipOrientation,
string shipGroup = null)
: base(program, shipGroup: shipGroup)
{
this.shipOrientation = shipOrientation;
// Use own programmable block as reference point
Reference = program.Me;
ReferencePoint = Reference.GetPosition();
}
// GyroControl
public GyroControl GyroControl
{
get
{
if (m_gyroControl == null)
{
m_gyroControl = new GyroControl();
m_gyroControl.Init(Blocks,
shipUp: shipOrientation.ShipUp,
shipForward: shipOrientation.ShipForward);
}
return m_gyroControl;
}
}
private GyroControl m_gyroControl = null;
// ThrustControl
public ThrustControl ThrustControl
{
get
{
if (m_thrustControl == null)
{
m_thrustControl = new ThrustControl();
m_thrustControl.Init(Blocks,
shipUp: shipOrientation.ShipUp,
shipForward: shipOrientation.ShipForward);
}
return m_thrustControl;
}
}
private ThrustControl m_thrustControl = null;
// Reference vectors (i.e. orientation in world coordinates)
public IMyCubeBlock Reference { get; private set; }
public Vector3D ReferencePoint { get; private set; }
public Vector3D ReferenceUp
{
get
{
if (m_referenceUp == null)
{
m_referenceUp = GetReferenceVector(shipOrientation.ShipUp);
}
return (Vector3D)m_referenceUp;
}
}
private Vector3D? m_referenceUp = null;
public Vector3D ReferenceForward
{
get
{
if (m_referenceForward == null)
{
m_referenceForward = GetReferenceVector(shipOrientation.ShipForward);
}
return (Vector3D)m_referenceForward;
}
}
private Vector3D? m_referenceForward = null;
public Vector3D ReferenceLeft
{
get
{
if (m_referenceLeft == null)
{
m_referenceLeft = GetReferenceVector(Base6Directions.GetLeft(shipOrientation.ShipUp, shipOrientation.ShipForward));
}
return (Vector3D)m_referenceLeft;
}
}
private Vector3D? m_referenceLeft = null;
private Vector3D GetReferenceVector(Base6Directions.Direction direction)
{
var offset = Reference.Position + Base6Directions.GetIntVector(direction);
return Vector3D.Normalize(Reference.CubeGrid.GridIntegerToWorld(offset) - ReferencePoint);
}
}
public class EventDriver
{
public struct FutureTickAction : IComparable<FutureTickAction>
{
public ulong When;
public Action<ZACommons, EventDriver> Action;
public FutureTickAction(ulong when, Action<ZACommons, EventDriver> action = null)
{
When = when;
Action = action;
}
public int CompareTo(FutureTickAction other)
{
return When.CompareTo(other.When);
}
}
public struct FutureTimeAction : IComparable<FutureTimeAction>
{
public TimeSpan When;
public Action<ZACommons, EventDriver> Action;
public FutureTimeAction(TimeSpan when, Action<ZACommons, EventDriver> action = null)
{
When = when;
Action = action;
}
public int CompareTo(FutureTimeAction other)
{
return When.CompareTo(other.When);
}
}
private const ulong TicksPerSecond = 60;
// Why is there no standard priority queue implementation?
private readonly LinkedList<FutureTickAction> TickQueue = new LinkedList<FutureTickAction>();
private readonly LinkedList<FutureTimeAction> TimeQueue = new LinkedList<FutureTimeAction>();
private readonly string TimerName, TimerGroup;
private ulong Ticks; // Not a reliable measure of time because of variable timer delay.
public TimeSpan TimeSinceStart
{
get { return m_timeSinceStart; }
private set { m_timeSinceStart = value; }
}
private TimeSpan m_timeSinceStart = TimeSpan.FromSeconds(0);
// If neither timerName nor timerGroup are given, it's assumed the timer will kick itself
public EventDriver(string timerName = null, string timerGroup = null)
{
TimerName = timerName;
TimerGroup = timerGroup;
}
private void KickTimer(ZACommons commons)
{
IMyTimerBlock timer = null;
// Name takes priority over group
if (TimerName != null)
{
var blocks = ZACommons.SearchBlocksOfName(commons.Blocks, TimerName,
block => block is IMyTimerBlock);
if (blocks.Count > 0)
{
timer = blocks[0] as IMyTimerBlock;
}
}
if (timer == null && TimerGroup != null)
{
var group = commons.GetBlockGroupWithName(TimerGroup);
if (group != null)
{
var blocks = ZACommons.GetBlocksOfType<IMyTimerBlock>(group.Blocks,
block => block.CubeGrid == commons.Me.CubeGrid);
timer = blocks.Count > 0 ? blocks[0] : null;
}
}
if (timer != null)
{
// Rules are simple. If we have something in the tick queue, trigger now.
// Otherwise, set timer delay appropriately (minimum 1 second) and kick.
// If you want sub-second accuracy, always use ticks.
if (TickQueue.First != null)
{
timer.GetActionWithName("TriggerNow").Apply(timer);
}
else if (TimeQueue.First != null)
{
var next = (float)(TimeQueue.First.Value.When.TotalSeconds - TimeSinceStart.TotalSeconds);
// Constrain appropriately (not sure if this will be done for us or if it
// will just throw). Just do it to be safe.
next = Math.Max(next, timer.GetMininum<float>("TriggerDelay"));
next = Math.Min(next, timer.GetMaximum<float>("TriggerDelay"));
timer.SetValue<float>("TriggerDelay", next);
timer.GetActionWithName("Start").Apply(timer);
}
// NB If both queues are empty, we stop running
}
}
public void Tick(ZACommons commons, Action mainAction = null,
Action preAction = null,
Action postAction = null)
{
Ticks++;
TimeSinceStart += commons.Program.ElapsedTime;
bool runMain = false;
if (preAction != null) preAction();
// Process each queue independently
while (TickQueue.First != null &&
TickQueue.First.Value.When <= Ticks)
{
var action = TickQueue.First.Value.Action;
TickQueue.RemoveFirst();
if (action != null)
{
action(commons, this);
}
else
{
runMain = true;
}
}
while (TimeQueue.First != null &&
TimeQueue.First.Value.When <= TimeSinceStart)
{
var action = TimeQueue.First.Value.Action;
TimeQueue.RemoveFirst();
if (action != null)
{
action(commons, this);
}
else
{
runMain = true;
}
}
if (runMain && mainAction != null) mainAction();
if (postAction != null) postAction();
KickTimer(commons);
}
public void Schedule(ulong delay, Action<ZACommons, EventDriver> action = null)
{
var future = new FutureTickAction(Ticks + delay, action);
for (var current = TickQueue.First;
current != null;
current = current.Next)
{
if (future.CompareTo(current.Value) < 0)
{
// Insert before this one
TickQueue.AddBefore(current, future);
return;
}
}
// Just add at the end
TickQueue.AddLast(future);
}
public void Schedule(double seconds, Action<ZACommons, EventDriver> action = null)
{
var delay = Math.Max(seconds, 0.0);
var future = new FutureTimeAction(TimeSinceStart + TimeSpan.FromSeconds(delay), action);
for (var current = TimeQueue.First;
current != null;
current = current.Next)
{
if (future.CompareTo(current.Value) < 0)
{
// Insert before this one
TimeQueue.AddBefore(current, future);
return;
}
}
// Just add at the end
TimeQueue.AddLast(future);
}
}
// Do NOT hold on to an instance of this class. Always instantiate a new one
// each run.
public class ZACommons
{
public const StringComparison IGNORE_CASE = StringComparison.CurrentCultureIgnoreCase;
public readonly MyGridProgram Program;
private readonly string ShipGroupName;
// All accessible blocks
public List<IMyTerminalBlock> AllBlocks
{
get
{
// No multithreading makes lazy init so much easier
if (m_allBlocks == null)
{
m_allBlocks = new List<IMyTerminalBlock>();
Program.GridTerminalSystem.GetBlocks(m_allBlocks);
}
return m_allBlocks;
}
}
private List<IMyTerminalBlock> m_allBlocks = null;
// Blocks on the same grid only
public List<IMyTerminalBlock> Blocks
{
get
{
if (m_blocks == null)
{
// Try group first, if it exists
if (ShipGroupName != null)
{
var group = GetBlockGroupWithName(ShipGroupName);
if (group != null) m_blocks = group.Blocks;
}
// Otherwise fetch all blocks on the same grid
if (m_blocks == null)
{
m_blocks = new List<IMyTerminalBlock>();
for (var e = AllBlocks.GetEnumerator(); e.MoveNext();)
{
var block = e.Current;
if (block.CubeGrid == Program.Me.CubeGrid) m_blocks.Add(block);
}
}
}
return m_blocks;
}
}
private List<IMyTerminalBlock> m_blocks = null;
public List<IMyBlockGroup> Groups
{
get
{
if (m_groups == null)
{
m_groups = new List<IMyBlockGroup>();
Program.GridTerminalSystem.GetBlockGroups(m_groups);
}
return m_groups;
}
}
private List<IMyBlockGroup> m_groups = null;
// NB Names are actually lowercased
public Dictionary<string, IMyBlockGroup> GroupsByName
{
get
{
if (m_groupsByName == null)
{
m_groupsByName = new Dictionary<string, IMyBlockGroup>();
for (var e = Groups.GetEnumerator(); e.MoveNext();)
{
var group = e.Current;
m_groupsByName.Add(group.Name.ToLower(), group);
}
}
return m_groupsByName;
}
}
private Dictionary<string, IMyBlockGroup> m_groupsByName = null;
public ZACommons(MyGridProgram program, string shipGroup = null)
{
Program = program;
ShipGroupName = shipGroup;
}
// Groups
public IMyBlockGroup GetBlockGroupWithName(string name)
{
IMyBlockGroup group;
if (GroupsByName.TryGetValue(name.ToLower(), out group))
{
return group;
}
return null;
}
public List<IMyBlockGroup> GetBlockGroupsWithPrefix(string prefix)
{
var result = new List<IMyBlockGroup>();
for (var e = Groups.GetEnumerator(); e.MoveNext();)
{
var group = e.Current;
if (group.Name.StartsWith(prefix, IGNORE_CASE)) result.Add(group);
}
return result;
}
// Blocks
public static List<T> GetBlocksOfType<T>(IEnumerable<IMyTerminalBlock> blocks,
Func<T, bool> collect = null)
{
List<T> list = new List<T>();
for (var e = blocks.GetEnumerator(); e.MoveNext();)
{
var block = e.Current;
if (block is T && (collect == null || collect((T)block))) list.Add((T)block);
}
return list;
}
public static T GetBlockWithName<T>(IEnumerable<IMyTerminalBlock> blocks, string name)
where T : IMyTerminalBlock
{
for (var e = blocks.GetEnumerator(); e.MoveNext();)
{
var block = e.Current;
if(block is T && block.CustomName.Equals(name, IGNORE_CASE)) return (T)block;
}
return default(T);
}
public static List<IMyTerminalBlock> SearchBlocksOfName(IEnumerable<IMyTerminalBlock> blocks, string name, Func<IMyTerminalBlock, bool> collect = null)
{
var result = new List<IMyTerminalBlock>();
for (var e = blocks.GetEnumerator(); e.MoveNext();)
{
var block = e.Current;
if (block.CustomName.IndexOf(name, IGNORE_CASE) >= 0 &&
(collect == null || collect(block)))
{
result.Add(block);
}
}
return result;
}
public static void ForEachBlockOfType<T>(IEnumerable<IMyTerminalBlock> blocks, Action<T> action)
{
for (var e = blocks.GetEnumerator(); e.MoveNext();)
{
var block = e.Current;
if (block is T)
{
action((T)block);
}
}
}
public static void EnableBlocks(IEnumerable<IMyTerminalBlock> blocks, bool enabled)
{
for (var e = blocks.GetEnumerator(); e.MoveNext();)
{
var block = e.Current;
// Not all blocks will implement IMyFunctionalBlock, so can't checked Enabled
block.GetActionWithName(enabled ? "OnOff_On" : "OnOff_Off").Apply(block);
}
}
public IMyProgrammableBlock Me
{
get { return Program.Me; }
}
// Batteries
public static bool IsBatteryRecharging(IMyBatteryBlock battery)
{
return !battery.ProductionEnabled;
}
public static void SetBatteryRecharge(IMyBatteryBlock battery, bool recharge)
{
var recharging = IsBatteryRecharging(battery);
if ((recharging && !recharge) || (!recharging && recharge))
{
battery.GetActionWithName("Recharge").Apply(battery);
}
}
public static void SetBatteryRecharge(IEnumerable<IMyBatteryBlock> batteries, bool recharge)
{
for (var e = batteries.GetEnumerator(); e.MoveNext();)
{
var battery = e.Current;
SetBatteryRecharge(battery, recharge);
}
}
// Display
public Action<string> Echo
{
get { return Program.Echo; }
}
public static string FormatPower(float value)
{
if (value >= 1.0f)
{
return string.Format("{0:F2} MW", value);
}
else if (value >= 0.001)
{
return string.Format("{0:F2} kW", value * 1000f);
}
else
{
return string.Format("{0:F2} W", value * 1000000f);
}
}
// Misc
public static bool StartTimerBlockWithName(IEnumerable<IMyTerminalBlock> blocks, string name,
Func<IMyTimerBlock, bool> condition = null)
{
var timer = GetBlockWithName<IMyTimerBlock>(blocks, name);
if (timer != null && timer.Enabled && !timer.IsCountingDown &&
(condition == null || condition(timer)))
{
timer.GetActionWithName("Start").Apply(timer);
return true;
}
return false;
}
public static bool IsConnectedAnywhere(IEnumerable<IMyShipConnector> connectors)
{
for (var e = connectors.GetEnumerator(); e.MoveNext();)
{
var connector = e.Current;
if (connector.IsLocked && connector.IsConnected)
{
return true;
}
}
return false;
}
public static bool IsConnectedAnywhere(IEnumerable<IMyTerminalBlock> blocks)
{
return IsConnectedAnywhere(GetBlocksOfType<IMyShipConnector>(blocks));
}
}
@ZerothAngel
Copy link
Author

If you want to start timer blocks instead of changing the antenna name (and if you're somewhat familiar with C#), use the following PowerDrainHandler instead (and pass an instance to BatteryManager's constructor):

public class TimerBlockPowerDrainHandler : BatteryManager.PowerDrainHandler
{
    public void PowerDrainStarted(ZACommons commons)
    {
        ZACommons.StartTimerBlockWithName(commons.Blocks, POWER_DRAIN_START_NAME);
    }

    public void PowerDrainEnded(ZACommons commons)
    {
        ZACommons.StartTimerBlockWithName(commons.Blocks, POWER_DRAIN_END_NAME);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment