Skip to content

Instantly share code, notes, and snippets.

@fresc81
Last active July 5, 2023 07:56
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fresc81/2292720070a4e7bd59cbec76939d1025 to your computer and use it in GitHub Desktop.
Save fresc81/2292720070a4e7bd59cbec76939d1025 to your computer and use it in GitHub Desktop.
Space Engineers Airlock Script

/* adjust block names to fit your needs */
// name of the inner door
const string INNER_DOOR_NAME = "Sliding Door Inner";
// name of the outer door
const string OUTER_DOOR_NAME = "Sliding Door Outer";
// name of the vent inside the airlock
const string AIR_VENT_NAME = "Air Vent Outer";
// name of the timer that defines how long the doors stay open
const string DOOR_IDLE_TIMER_NAME = "Timer Block Door Idle";
// name of the airlock status panel
const string AIRLOCK_STATUS_PANEL_NAME = "LCD Panel Airlock Status";
// name of the block group that contains all airlock buttons
const string BUTTON_GROUP_NAME = "Airlock Buttons";
/**
* state machine states
*/
public enum AirlockState {
START, // the starting state
DEPRESSURIZING, // depressurizing the room
OUTER_DOOR_OPENING, // opening the outer door
OUTER_DOOR_OPEN, // the outer door is open
OUTER_DOOR_CLOSING, // closing the outer door
DEPRESSURIZED_IDLE, // both doors are closed and the room is depressurized
PRESSURIZING, // pressurizing the room
INNER_DOOR_OPENING, // opening the inner door
INNER_DOOR_OPEN, // the inner door is open
INNER_DOOR_CLOSING, // closing the inner door
PRESSURIZED_IDLE, // both doors are closed and the room is pressurized
}
/**
* state machine events
*/
public enum AirlockEvent {
INITIALIZE, // initialization event - reset the doors and depressurize the room
REQUEST_OUTER_DOOR_OPEN, // a button was pressed - request the outer door to be opened
REQUEST_INNER_DOOR_OPEN, // a button was pressed - request the inner door to be opened
INNER_DOOR_OPENED, // a door state has changed - the inner door has been opened
INNER_DOOR_CLOSED, // a door state has changed - the inner door has been closed
OUTER_DOOR_OPENED, // a door state has changed - the outer door has been opened
OUTER_DOOR_CLOSED, // a door state has changed - the outer door has been closed
DEPRESSURIZED, // the room's oxidation level changed - the room has been depressurized
PRESSURIZED, // the room's oxidation level changed - the room has been pressurized
DOOR_IDLE_TIMER, // the door's idle-open timer fired
}
/**
* controller encapsules all interaction with Space Engineers
*/
private AirlockController controller;
/**
* airlock state machine encapsules the airlock behaviour
*/
private AirlockStatemachine airlock;
/**
* script constructor
* initializes the controller and state machine
* sets the script update interval
*/
public Program () {
// create controller and statemachine
controller = new AirlockController(this, Me);
airlock = new AirlockStatemachine(controller);
// run the startup transition
airlock.Consume (AirlockEvent.INITIALIZE);
// setup how often Space Engineers calls the Main-method
Runtime.UpdateFrequency = UpdateFrequency.Update10;
}
/**
* generates events for the statemachine
* clears log for each processed event
* can be called from the outside world to fire events in the state machine
*/
public void Main (string argument, UpdateType updateSource) {
AirlockEvent evt;
if (Enum.TryParse<AirlockEvent> (argument, out evt)) {
// successfully parsed the run argument as an AirlockEvent
controller.Log("event " + argument);
airlock.Consume (evt);
// otherwise poll the Check-methods to generate events...
} else if (controller.CheckPressurized()) {
controller.Log("event PRESSURIZED");
airlock.Consume (AirlockEvent.PRESSURIZED);
} else if (controller.CheckDepressurized()) {
controller.Log("event DEPRESSURIZED");
airlock.Consume (AirlockEvent.DEPRESSURIZED);
} else if (controller.CheckInnerDoorOpened()) {
controller.Log("event INNER_DOOR_OPENED");
airlock.Consume (AirlockEvent.INNER_DOOR_OPENED);
} else if (controller.CheckInnerDoorClosed()) {
controller.Log("event INNER_DOOR_CLOSED");
airlock.Consume (AirlockEvent.INNER_DOOR_CLOSED);
} else if (controller.CheckOuterDoorOpened()) {
controller.Log("event OUTER_DOOR_OPENED");
airlock.Consume (AirlockEvent.OUTER_DOOR_OPENED);
} else if (controller.CheckOuterDoorClosed()) {
controller.Log("event OUTER_DOOR_CLOSED");
airlock.Consume (AirlockEvent.OUTER_DOOR_CLOSED);
}
}
/**
* Implementation of the Airlock statemachine.
*/
public class AirlockStatemachine : Statemachine<AirlockState, AirlockEvent> {
// constructor
public AirlockStatemachine (AirlockController controller) {
// startup state
State = AirlockState.START;
// list of transitions...
Transitions = new Transition[] {
// startup transition
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.INITIALIZE
}),
Source = AirlockState.START,
Target = AirlockState.DEPRESSURIZED_IDLE,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.CloseInnerDoor ();
controller.CloseOuterDoor ();
controller.Depressurize ();
controller.DisableInnerDoor ();
controller.DisableOuterDoor ();
controller.EnableButtons ();
}
},
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.DEPRESSURIZED
}),
Source = AirlockState.DEPRESSURIZING,
Target = AirlockState.OUTER_DOOR_OPENING,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.OpenOuterDoor ();
}
},
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.OUTER_DOOR_OPENED
}),
Source = AirlockState.OUTER_DOOR_OPENING,
Target = AirlockState.OUTER_DOOR_OPEN,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.ScheduleDoorIdle ();
}
},
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.DOOR_IDLE_TIMER
}),
Source = AirlockState.OUTER_DOOR_OPEN,
Target = AirlockState.OUTER_DOOR_CLOSING,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.CloseOuterDoor ();
}
},
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.OUTER_DOOR_CLOSED
}),
Source = AirlockState.OUTER_DOOR_CLOSING,
Target = AirlockState.DEPRESSURIZED_IDLE,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.EnableButtons ();
}
},
// inner door open request
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.REQUEST_INNER_DOOR_OPEN
}),
Source = AirlockState.DEPRESSURIZED_IDLE,
Target = AirlockState.PRESSURIZING,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.DisableButtons ();
controller.Pressurize ();
}
},
// outer door open request
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.REQUEST_OUTER_DOOR_OPEN
}),
Source = AirlockState.DEPRESSURIZED_IDLE,
Target = AirlockState.OUTER_DOOR_OPENING,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.DisableButtons ();
controller.OpenOuterDoor ();
}
},
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.PRESSURIZED
}),
Source = AirlockState.PRESSURIZING,
Target = AirlockState.INNER_DOOR_OPENING,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.OpenInnerDoor ();
}
},
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.INNER_DOOR_OPENED
}),
Source = AirlockState.INNER_DOOR_OPENING,
Target = AirlockState.INNER_DOOR_OPEN,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.ScheduleDoorIdle();
}
},
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.DOOR_IDLE_TIMER
}),
Source = AirlockState.INNER_DOOR_OPEN,
Target = AirlockState.INNER_DOOR_CLOSING,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.CloseInnerDoor ();
}
},
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.INNER_DOOR_CLOSED
}),
Source = AirlockState.INNER_DOOR_CLOSING,
Target = AirlockState.PRESSURIZED_IDLE,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.EnableButtons ();
}
},
// inner door open request
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.REQUEST_INNER_DOOR_OPEN
}),
Source = AirlockState.PRESSURIZED_IDLE,
Target = AirlockState.INNER_DOOR_OPENING,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.DisableButtons ();
controller.OpenInnerDoor ();
}
},
// outer door open request
new Transition {
Filter = MatchEvents (new AirlockEvent[] {
AirlockEvent.REQUEST_OUTER_DOOR_OPEN
}),
Source = AirlockState.PRESSURIZED_IDLE,
Target = AirlockState.DEPRESSURIZING,
Transit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
controller.DisableButtons ();
controller.Depressurize ();
}
},
};
// action to be done after each transit
AfterTransit = (AirlockState src, AirlockState dst, AirlockEvent evt) => {
// log the current transition
controller.Log ("state " + src + " -> " + dst, true);
};
}
}
/**
* encapsules interaction with the world
*/
public class AirlockController {
private Program m_program;
private IMyProgrammableBlock m_block;
private bool m_innerDoorState = false;
private bool m_outerDoorState = false;
private bool m_depressurizerState = true;
public IMyDoor InnerDoor {
get {
return m_program.GridTerminalSystem.GetBlockWithName (INNER_DOOR_NAME) as IMyDoor;
}
}
public IMyDoor OuterDoor {
get {
return m_program.GridTerminalSystem.GetBlockWithName (OUTER_DOOR_NAME) as IMyDoor;
}
}
public IMyAirVent AirVent {
get {
return m_program.GridTerminalSystem.GetBlockWithName (AIR_VENT_NAME) as IMyAirVent;
}
}
public IMyTimerBlock DoorIdleTimer {
get {
return m_program.GridTerminalSystem.GetBlockWithName (DOOR_IDLE_TIMER_NAME) as IMyTimerBlock;
}
}
public IMyTextPanel StatusPanel {
get {
return m_program.GridTerminalSystem.GetBlockWithName (AIRLOCK_STATUS_PANEL_NAME) as IMyTextPanel;
}
}
public IMyBlockGroup ButtonsBlockGroup {
get {
return m_program.GridTerminalSystem.GetBlockGroupWithName (BUTTON_GROUP_NAME);
}
}
/**
* constructor
* implicitly clears the log
*/
public AirlockController (Program program, IMyProgrammableBlock block) {
m_program = program;
m_block = block;
ClearLog ();
}
/**
* Empties the log.
*/
public void ClearLog() {
m_block.CustomData = "";
StatusPanel.WritePublicText ("");
}
/**
* Creates a line in the log. With append being set to true the previous content of the log is preserved.
*/
public void Log(string message, bool append = false) {
message += "\n";
if (append) {
m_block.CustomData += message;
} else {
m_block.CustomData = message;
}
StatusPanel.WritePublicText (message, append);
}
/**
* Disable all IMyButtonPanel objects in the block group that contains the buttons.
*/
public void DisableButtons () {
List<IMyButtonPanel> buttons = new List<IMyButtonPanel> ();
ButtonsBlockGroup.GetBlocksOfType<IMyButtonPanel> (buttons);
foreach (IMyButtonPanel button in buttons) {
try {
button.ApplyAction ("OnOff_Off");
} catch {
Log ("Cannot Disable Button: " + button.CustomName, true);
}
}
}
/**
* Enable all IMyButtonPanel objects in the block group that contains the buttons.
*/
public void EnableButtons () {
List<IMyButtonPanel> buttons = new List<IMyButtonPanel>();
ButtonsBlockGroup.GetBlocksOfType<IMyButtonPanel>(buttons);
foreach (IMyButtonPanel button in buttons) {
try {
button.ApplyAction ("OnOff_On");
} catch {
Log ("Cannot Enable Button: " + button.CustomName, true);
}
}
}
/**
* Disables the inner door.
*/
public void DisableInnerDoor () {
try {
InnerDoor.ApplyAction ("OnOff_Off");
} catch {
Log ("Cannot Disable Inner Door", true);
}
}
/**
* Enables the inner door.
*/
public void EnableInnerDoor () {
try {
InnerDoor.ApplyAction ("OnOff_On");
} catch {
Log ("Cannot Enable Inner Door", true);
}
}
/**
* Disables the outer door.
*/
public void DisableOuterDoor () {
try {
OuterDoor.ApplyAction ("OnOff_Off");
} catch {
Log ("Cannot Disable Outer Door", true);
}
}
/**
* Enables the outer door.
*/
public void EnableOuterDoor () {
try {
OuterDoor.ApplyAction ("OnOff_On");
} catch {
Log ("Cannot Enable Outer Door", true);
}
}
/**
* Starts opening the inner door.
*/
public void OpenInnerDoor () {
m_innerDoorState = false;
EnableInnerDoor ();
try {
InnerDoor.ApplyAction ("Open_On");
} catch {
Log ("Cannot Open Inner Door", true);
}
}
/**
* Polls the inner door status and returns true if it has been fully opened.
*/
public bool CheckInnerDoorOpened () {
if ((m_innerDoorState == false) && (InnerDoor.OpenRatio == 1.0)) {
m_innerDoorState = true;
DisableInnerDoor ();
return true;
}
return false;
}
/**
* Starts closing the inner door.
*/
public void CloseInnerDoor () {
m_innerDoorState = true;
EnableInnerDoor ();
try {
InnerDoor.ApplyAction ("Open_Off");
} catch {
Log ("Cannot Close Inner Door", true);
}
}
/**
* Polls the inner door status and returns true if it has been fully closed.
*/
public bool CheckInnerDoorClosed () {
if ((m_innerDoorState == true) && (InnerDoor.OpenRatio == 0.0)) {
m_innerDoorState = false;
DisableInnerDoor ();
return true;
}
return false;
}
/**
* Starts opening the outer door.
*/
public void OpenOuterDoor () {
m_outerDoorState = false;
EnableOuterDoor ();
try {
OuterDoor.ApplyAction ("Open_On");
} catch {
Log ("Cannot Open Outer Door", true);
}
}
/**
* Polls the outer door status and returns true if it has been fully opened.
*/
public bool CheckOuterDoorOpened () {
if ((m_outerDoorState == false) && (OuterDoor.OpenRatio == 1.0)) {
m_outerDoorState = true;
DisableOuterDoor ();
return true;
}
return false;
}
/**
* Starts closing the outer door.
*/
public void CloseOuterDoor () {
m_outerDoorState = true;
EnableOuterDoor ();
try {
OuterDoor.ApplyAction ("Open_Off");
} catch {
Log ("Cannot Close Outer Door", true);
}
}
/**
* Polls the outer door status and returns true if it has been fully closed.
*/
public bool CheckOuterDoorClosed () {
if ((m_outerDoorState == true) && (OuterDoor.OpenRatio == 0.0)) {
m_outerDoorState = false;
DisableOuterDoor ();
return true;
}
return false;
}
/**
* Starts pressurizing the room.
*/
public void Pressurize () {
m_depressurizerState = true;
try {
AirVent.ApplyAction ("Depressurize_Off");
} catch {
Log ("Cannot Pressurize", true);
}
}
/**
* Polls the oxygen level of the room and returns true if it has been pressurized.
*/
public bool CheckPressurized () {
if ((m_depressurizerState == true) && (AirVent.GetOxygenLevel () >= 0.999)) {
m_depressurizerState = false;
return true;
}
return false;
}
/**
* Starts depressurizing the room.
*/
public void Depressurize () {
m_depressurizerState = false;
try {
AirVent.ApplyAction ("Depressurize_On");
} catch {
Log ("Cannot Depressurize", true);
}
}
/**
* Polls the oxygen level of the room and returns true if it has been depressurized.
*/
public bool CheckDepressurized () {
if ((m_depressurizerState == false) && (AirVent.GetOxygenLevel () <= 0.001)) {
m_depressurizerState = true;
return true;
}
return false;
}
/**
* Starts the door open idle timer.
*/
public void ScheduleDoorIdle () {
try {
DoorIdleTimer.ApplyAction ("Start");
} catch {
Log ("Cannot Start Timer", true);
}
}
}
/**
* a simple finite statemachine
*/
public class Statemachine<TState, TEvent>
where TState : struct, IComparable // the state datatype, must be a value that implements IComparable
where TEvent : struct, IComparable // the event datatype, must be a value that implements IComparable
{
/**
* Filter function to select matching events.
*/
public delegate bool EventFilter (TEvent evt);
/**
* Callback function that is called if a transition is applied.
*/
public delegate void TransitCallback (TState src, TState dst, TEvent evt);
/**
* Defines a data structure that stores a possible transition.
*/
public struct Transition {
// selects events that trigger this transition
public EventFilter Filter;
// the state before transition
public TState Source;
// the state after transition
public TState Target;
// a callback that is called if this transition has been applied
// can be null if not needed
public TransitCallback Transit;
}
// the current state of the statemachine
public TState State;
// the transitions this statemachine defines
public Transition[] Transitions;
// a callback that is called before transitions
// use it to define actions that must be performed when leaving a source state
// can be null if not needed
public TransitCallback BeforeTransit;
// a callback that is called after transitions
// use it to define actions that must be performed when entering a destination state
// can be null if not needed
public TransitCallback AfterTransit;
/**
* Builds a new EventFilter function that matches the given events.
*/
public static EventFilter MatchEvents (TEvent[] events) {
return (TEvent evt) => {
foreach (TEvent item in events) {
if (evt.Equals (item)) {
return true;
}
}
return false;
};
}
/**
* Builds a new TransitCallback function that is composed of calling the given callbacks in order.
*/
public static TransitCallback CallbacksInSeries (TransitCallback[] callbacks) {
return (TState src, TState dst, TEvent evt) => {
foreach (TransitCallback callback in callbacks) {
callback (src, dst, evt);
}
};
}
/**
* Let the statemachine consume the given event.
* Returned value indicates that a transition has been applied.
*/
public bool Consume (TEvent evt) {
foreach (Transition transition in Transitions) {
if (transition.Source.Equals (State) && transition.Filter (evt)) {
if (BeforeTransit != null) {
BeforeTransit (transition.Source, transition.Target, evt);
}
State = transition.Target;
if (transition.Transit != null) {
transition.Transit (transition.Source, transition.Target, evt);
}
if (AfterTransit != null) {
AfterTransit (transition.Source, transition.Target, evt);
}
return true;
}
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment