Created
August 15, 2015 16:53
-
-
Save ZerothAngel/651d09f7fc0d44f7d30d to your computer and use it in GitHub Desktop.
Space Engineers Generic Solar-Powered Satellite Controller
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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):