Skip to content

Instantly share code, notes, and snippets.

@Chase-san
Last active December 25, 2020 14:22
Show Gist options
  • Save Chase-san/635c21f7774b6c4fbcb9ff22fb7e70e3 to your computer and use it in GitHub Desktop.
Save Chase-san/635c21f7774b6c4fbcb9ff22fb7e70e3 to your computer and use it in GitHub Desktop.
Space Engineers

Jackrabbit Industries Automation Controller (JRIAC)

Version 1.1

Example Airlock

Blocks Internal sliding door named "[Automated] Door A1" External sliding door named "[Automated] Door A2" Vent inside airlock named "[Automated] Vent A"

"[Automated] Door A1" Custom Data (inner door)

OnClose 0 TurnOff
OnClose 0 Depressurize Vent A
OnClose 2 TurnOn Door A2
OnClose 2 Open Door A2

"[Automated] Door A2" Custom Data (outer door)

OnClose 0 TurnOff
OnClose 0 Pressurize Vent A
OnClose 2 TurnOn Door A1
OnClose 2 Open Door A1

Place a button that simply closes the opposite door to have a "Cycle Airlock" button, never waste oxygen again! You can have any number of airlocks and this single block can handle them all (assuming they are named differently).

Keep in mind that none of these are case sensative, so you could use code that looks like

"[aUtOmAtEd] dOoR a2" Custom Data (outer door)

oncloSE 0 TURNoff
ONCLOSE 0 pREsSUrize vent a
oncLOSE 2 turnON doOR a1
ONclose 2 open DOOR A1

and it would work exactly the same

Command Syntax

It's rather simple actually.

EVENT TIME COMMAND TARGET

EVENT means "when this happens", there are several events available in the automation script.

  • OnOpen- When the door becomes open
  • OnClose- When the door becomes closed
  • OnOpening- When the door starts opening
  • OnClosing- When the door starts closing
  • OnDetect- When the sensor becomes active
  • OnLost- When the sensor becomes inactive

TIME is simply the time in seconds, you can use decimals if you wnat, so "7" or "3.2213" are both fine. Keep in mind that the time may not be exactly what you set, as it depends on how quickly the script runs, your PC/Server and the script load.

COMMAND means "DO THIS", there are currently 6 commands. The normal block rules apply to these, if a door is not enabled, then you cannot open or close it, you also cannot close a light, or pressurize a door, the controller will simply ignore nonsense commands like those.

  • Open- Opens a door
  • Close- Closes a door
  • Pressurize- Sets a vent to pressurize a room
  • Depressurize- Sets a vent to depressurize a room
  • TurnOn- Toggles the power of a block on
  • TurnOff- Toggles the power of a block off
  • ToggleDoor- Switches the open/closed state of a door (opening is treated as open and closing as closed)
  • TogglePower- Switches the on/off state of a block
  • TogglePressure- Switches the pressurize/depressurize state of a vent

TARGET is what you want to run your command on, simply type it out, spaces and all, the controller is fine with it (mostly because it's the last paramater). For this parameter you omit the "[Automated]" prefix when typing it, this is for ease of use. Unlike other parameters this one is OPTIONAL, that means you can leave it out. If you don't use this parameter, the controller assumes you want to apply the command to whatever block the event is attached to.

Change Log

Version 1.1

  • Added additional door state events OnClosing and OnOpening
  • Added sensor events OnDetect and OnLost
  • Changed ToggleOn and ToggleOff commands to TurnOn and TurnOff
  • Added 3 new commands, TogglePower, ToggleDoor, and TogglePressure for changing the current state to the opposite in all blocks, doors and vents respectively.
  • Added a check to see if run, so you can bind a button to "run" the Programmable Block the script is in to force it to reload all blocks. Also works from the terminal. This is useful as it allows it to load new automation tasks without breaking currently running automation tasks.
  • Converted readme to markdown.
/**
* Jackrabbit Industries Automation Controller (JRIAC)
* Version 1.1
*/
// CONSTANTS
const string BlockPrefix = "[Automated]";
//const string GroupPrefix = "[Automated]"; //TODO
// Set Programmable Block screen to 0.4 font size, recommend Monospace font.
const int displayBufferX = 64;
const int displayBufferY = 27;
public void DebugPrint (string str) {
if (str.Length > displayBufferX - 1) {
str = str.Substring (0, displayBufferX - 1);
}
displayBuffer.Add (str + "\n");
if (displayBuffer.Count > displayBufferY) {
displayBuffer.RemoveAt (0);
}
Me.GetSurface (0).WriteText (String.Concat (displayBuffer));
}
public enum EventType {
None,
OnClose,
OnClosing,
OnOpen,
OnOpening,
OnDetect,
OnLost
}
string[] EventTypeNames = Enum.GetNames (typeof (EventType));
public enum CommandType {
None,
Close,
Open,
Pressurize,
Depressurize,
TurnOn,
TurnOff,
ToggleDoor,
TogglePower,
TogglePressure
}
string[] CommandTypeNames = Enum.GetNames (typeof (CommandType));
public sealed class Event {
public EventType eventType;
public long time;
public CommandType commandType;
public long targetId;
public Event Copy () {
return (Event) this.MemberwiseClone ();
}
public override string ToString () {
return "[" + eventType + "," + time + "," + commandType + "," + targetId + "]";
}
}
public sealed class BlockState {
public BlockState (IMyTerminalBlock block) {
this.block = block;
this.doorStatus = DoorStatus.Closed;
this.sensorActive = false;
UpdateState ();
}
public IMyTerminalBlock block;
public DoorStatus doorStatus;
public bool sensorActive;
public EventType UpdateState () {
if (this.block is IMyDoor) {
IMyDoor door = this.block as IMyDoor;
if (door.Status != this.doorStatus) {
this.doorStatus = door.Status;
switch (door.Status) {
case DoorStatus.Open:
return EventType.OnOpen;
case DoorStatus.Opening:
return EventType.OnOpening;
case DoorStatus.Closed:
return EventType.OnClose;
case DoorStatus.Closing:
return EventType.OnClosing;
}
}
} else if (this.block is IMySensorBlock) {
IMySensorBlock sensor = this.block as IMySensorBlock;
if (sensor.IsActive != this.sensorActive) {
this.sensorActive = sensor.IsActive;
if (this.sensorActive) {
return EventType.OnDetect;
}
return EventType.OnLost;
}
}
return EventType.None;
}
public void DoOpen (bool? value) {
if (this.block is IMyDoor) {
IMyDoor door = this.block as IMyDoor;
if (value == true) {
door.OpenDoor ();
} else if (value == false) {
door.CloseDoor ();
} else if (door.Status == DoorStatus.Closed ||
door.Status == DoorStatus.Closing) {
door.OpenDoor ();
} else {
door.CloseDoor ();
}
}
}
public void DoPressurize (bool? value) {
if (this.block is IMyAirVent) {
IMyAirVent vent = this.block as IMyAirVent;
if (value == true) {
vent.Depressurize = false;
} else if (value == false) {
vent.Depressurize = true;
} else {
vent.Depressurize = !vent.Depressurize;
}
}
}
public void DoTogglePower (bool? value) {
if (this.block is IMyFunctionalBlock) {
IMyFunctionalBlock block = this.block as IMyFunctionalBlock;
if (value == true) {
block.Enabled = true;
} else if (value == false) {
block.Enabled = false;
} else {
block.Enabled = !block.Enabled;
}
}
}
}
List<string> displayBuffer = new List<String> ();
Dictionary<long, Event[]> EventMap = new Dictionary<long, Event[]> ();
Dictionary<long, BlockState> StateMap = new Dictionary<long, BlockState> ();
List<Event> TimedEventQueue = new List<Event> ();
public Program () {
Runtime.UpdateFrequency = UpdateFrequency.Update10;
DebugPrint ("Starting up....");
LoadBlocks ();
}
public void Main (string argument, UpdateType type) {
if (type == UpdateType.Terminal || type == UpdateType.Trigger) {
//reload blocks without restarting
LoadBlocks ();
}
long time = DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond;
// check for state changes
foreach (long id in EventMap.Keys) {
BlockState state = StateMap[id];
EventType eventType = state.UpdateState ();
if (eventType != EventType.None) {
DebugPrint ("(" + id + ") " + eventType);
foreach (Event e in EventMap[id]) {
if (e.eventType == eventType) {
Event timedEvent = e.Copy ();
timedEvent.time += time;
TimedEventQueue.Add (timedEvent);
}
}
}
}
//fire event commands if time is right
int index = 0;
while (index < TimedEventQueue.Count) {
Event e = TimedEventQueue[index];
if (e.time <= time) {
DebugPrint ("Event: " + e);
FireEvent (e.commandType, e.targetId);
TimedEventQueue.RemoveAt (index);
continue;
}
++index;
}
}
public void FireEvent (CommandType cmd, long target) {
switch (cmd) {
case CommandType.Close:
StateMap[target].DoOpen (false);
break;
case CommandType.Open:
StateMap[target].DoOpen (true);
break;
case CommandType.ToggleDoor:
StateMap[target].DoOpen (null);
break;
case CommandType.Pressurize:
StateMap[target].DoPressurize (true);
break;
case CommandType.Depressurize:
StateMap[target].DoPressurize (false);
break;
case CommandType.TogglePressure:
StateMap[target].DoPressurize (null);
break;
case CommandType.TurnOn:
StateMap[target].DoTogglePower (true);
break;
case CommandType.TurnOff:
StateMap[target].DoTogglePower (false);
break;
case CommandType.TogglePower:
StateMap[target].DoTogglePower (null);
break;
}
}
public void LoadBlocks () {
DebugPrint ("Loading Block Data");
Dictionary<string, long> lookupMap = new Dictionary<string, long> ();
EventMap.Clear ();
//populate list
List<IMyTerminalBlock> blockList = new List<IMyTerminalBlock> ();
GridTerminalSystem.SearchBlocksOfName (BlockPrefix, blockList);
foreach (IMyTerminalBlock block in blockList) {
string name = block.CustomName;
if (!name.StartsWith (BlockPrefix)) {
continue;
}
long id = block.EntityId;
name = name.Substring (BlockPrefix.Length).Trim ().ToLower ();
if (name.Length > 0) {
lookupMap[name] = id;
}
if (!StateMap.ContainsKey (id)) {
StateMap.Add (id, new BlockState (block));
}
EventMap[id] = null;
}
List<long> keys = new List<long> (EventMap.Keys);
for (int index = 0; index < keys.Count; ++index) {
long id = keys[index];
IMyTerminalBlock block = StateMap[id].block;
DebugPrint ("Processing events for " + StateMap[id].block.CustomName);
EventMap[id] = CreateBlockEventList (block, ref lookupMap);
}
}
public Event[] CreateBlockEventList (IMyTerminalBlock block, ref Dictionary<string, long> lookupMap) {
string data = block.CustomData;
string[] lines = data.Split (new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
Event[] events = new Event[lines.Length];
//TODO handle invalid events better?
for (int index = 0; index < lines.Length; ++index) {
int line = index + 1;
events[index] = new Event ();
events[index].eventType = EventType.None;
events[index].commandType = CommandType.None;
events[index].targetId = block.EntityId;
lines[index] = lines[index].Trim ();
if (lines[index].Length == 0 || lines[index].StartsWith ("#")) {
continue;
}
string[] tokens = lines[index].Split (new char[] { ' ' }, 4);
//check token count or fail
if (tokens.Length < 3) {
DebugPrint ("Line " + line + ": too few parameters.");
continue;
}
//parse time or fail
float time = 0;
if (!Single.TryParse (tokens[1], out time)) {
events[index].eventType = EventType.None;
DebugPrint ("Line " + line + ": Failed to parse time.");
continue;
}
events[index].time = (long) (time * 1000);
//parse event type or fail
for (int j = 0; j < EventTypeNames.Length; ++j) {
if (EventTypeNames[j].ToLower () == tokens[0].ToLower ()) {
events[index].eventType = (EventType) j;
break;
}
}
if (events[index].eventType == EventType.None) {
DebugPrint ("Line " + line + ": Invalid event type: " + tokens[0]);
continue;
}
//parse command type of fail
for (int j = 0; j < CommandTypeNames.Length; ++j) {
if (CommandTypeNames[j].ToLower () == tokens[2].ToLower ()) {
events[index].commandType = (CommandType) j;
break;
}
}
if (events[index].commandType == CommandType.None) {
events[index].eventType = EventType.None;
DebugPrint ("Line " + line + ": Invalid command type: " + tokens[2]);
continue;
}
//lookup target block (if needed) or fail
if (tokens.Length == 4) {
string name = tokens[3].Trim ().ToLower ();
if (!lookupMap.ContainsKey (name)) {
events[index].eventType = EventType.None;
DebugPrint ("Line " + line + ": Invalid target.");
continue;
}
events[index].targetId = lookupMap[name];
}
}
return events;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment