Last active
July 5, 2023 07:56
-
-
Save fresc81/2292720070a4e7bd59cbec76939d1025 to your computer and use it in GitHub Desktop.
Space Engineers Airlock Script
This file contains 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
| |
/* 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