diff --git a/Airlock/Airlock.cs b/Airlock/Airlock.cs new file mode 100644 index 0000000..e24fb61 --- /dev/null +++ b/Airlock/Airlock.cs @@ -0,0 +1,305 @@ +using Sandbox.ModAPI.Ingame; +using SpaceEngineers.Game.ModAPI.Ingame; +using System.Collections.Generic; +using VRageMath; + +namespace IngameScript +{ + public class Airlock + { + enum AirlockLightState + { + Off, + Cycling, + Cooldown, + Error, + } + + public bool DoorsClosed + { + get + { + return innerDoor.Status == DoorStatus.Closed && outerDoor.Status == DoorStatus.Closed; + } + } + + // TODO: this should check for depressurizing to the *reference* amount + public bool PressureStabilized + { + get + { + return ((airVent.Depressurize && airVent.GetOxygenLevel() <= targetOxygenLevel) || + (!airVent.Depressurize && airVent.Status == VentStatus.Pressurized)); + } + } + + public bool OxygenBalanced + { + get { return oxygenTank.FilledRatio > 0.25; } + } + + public bool Functional { get; private set; } = true; + + public bool Cycling { get; private set; } = false; + + private bool DoorOpened + { + get + { + return innerDoor.Status == DoorStatus.Open || outerDoor.Status == DoorStatus.Open; + } + } + + // Returns false if we are in a state where we can't or don't need to balance + private bool balanceOxygen() + { + if (innerDoor.Status == DoorStatus.Closed || outerDoor.Status == DoorStatus.Open || OxygenBalanced) { return false; } + + _p.Echo("DEBUG: Balancing Oxygen Tank"); + + // Configure the vent to suck in Oxygen. + airVent.Depressurize = true; + airVent.Enabled = true; + return true; + } + + private string _name; + private MyGridProgram _p; + private float targetOxygenLevel = 0.0F; + + private IMyDoor innerDoor; + private IMyDoor outerDoor; + private List lights; + private IMyGasTank oxygenTank; + private IMyAirVent airVent; + private IMyAirVent airSensor; + private const int CooldownTicks = 120; + + public Airlock(MyGridProgram p, string name) + { + _p = p; + _name = name; + + // Find the appropriate blocks given the airlock name + initDoors(); + initLights(); + initVents(); + initOxygen(); + } + + private void initDoors() + { + List onlyDoors = new List(); + _p.GridTerminalSystem.GetBlocksOfType(onlyDoors); + foreach (IMyDoor door in onlyDoors) + { + if (!door.CustomName.StartsWith(_name)) continue; + + if (door.CustomName.Contains("Inner")) + { + innerDoor = door; + } + else if (door.CustomName.Contains("Outer")) + { + outerDoor = door; + } + + if (innerDoor != null && outerDoor != null) + { + return; + } + } + + Functional = false; + } + + private void initVents() + { + List onlyFans = new List(); + _p.GridTerminalSystem.GetBlocksOfType(onlyFans); + foreach (IMyAirVent vent in onlyFans) + { + if (!vent.CustomName.StartsWith(_name)) continue; + + if (vent.CustomName.StartsWith(_name)) + { + if (vent.CustomName.Contains("Main")) + { + airVent = vent; + continue; + } + else if (vent.CustomName.Contains("Reference")) + { + airSensor = vent; + continue; + } + } + + // A global reference vent will be used if we don't have one specific to our airlock. + // A specific vent found later will overwrite this assignment. + if (vent.CustomName.StartsWith("Airlock Reference") && airSensor == null) + { + airSensor = vent; + } + } + + if (airVent == null) Functional = false; + } + + private void initLights() + { + lights = new List(); + List allLights = new List(); + _p.GridTerminalSystem.GetBlocksOfType(allLights); + foreach (IMyLightingBlock light in allLights) + { + if (!light.CustomName.StartsWith(_name)) continue; + lights.Add(light); + } + } + + private void initOxygen() + { + List allTanks = new List(); + _p.GridTerminalSystem.GetBlocksOfType(allTanks); + foreach (IMyGasTank tank in allTanks) + { + if (!tank.CustomName.StartsWith(_name)) continue; + oxygenTank = tank; + return; + } + Functional = false; + } + + public IEnumerator CycleAirlock() + { + Cycling = true; + setLights(AirlockLightState.Cycling); + + closeDoors(); + while (!DoorsClosed) { yield return true; } + lockDoors(); + + if (!airVent.CanPressurize) + { + error("Airlock is not airtight."); + yield return false; + } + + pressurizeDepressurize(); + while (!PressureStabilized) { yield return true; } + airVent.Enabled = false; + + openDoor(); + while (!DoorOpened) { yield return true; } + lockDoors(); + + // Balance oxygen storage + setLights(AirlockLightState.Cooldown); + if (balanceOxygen()) + { + while (!OxygenBalanced) { yield return true; } + } + airVent.Enabled = false; + + // Cooldown period + int cooldown = 0; + while(cooldown < CooldownTicks) { + cooldown++; + yield return true; + } + + setLights(AirlockLightState.Off); + } + + private void closeDoors() + { + _p.Echo("DEBUG: Closing Doors"); + + // close the doors + innerDoor.Enabled = true; + outerDoor.Enabled = true; + innerDoor.CloseDoor(); + outerDoor.CloseDoor(); + } + + private void pressurizeDepressurize() + { + _p.Echo("DEBUG: Cycling"); + + // toggle the current state + airVent.Depressurize = !airVent.Depressurize; + airVent.Enabled = true; + + // When depressurizing, check the external pressure and only depressurize to that value. + // TODO: test this for floating point errors + if (airVent.Depressurize && airSensor != null) + { + targetOxygenLevel = airSensor.GetOxygenLevel(); + _p.Echo($"Set depressurization target to {targetOxygenLevel}"); + } + } + + // Open the appropriate door based on pressure state. + private void openDoor() + { + _p.Echo("DEBUG: Opening Door"); + + if (airVent.Status == VentStatus.Pressurized) + { + innerDoor.Enabled = true; + innerDoor.OpenDoor(); + } + else + { + outerDoor.Enabled = true; + outerDoor.OpenDoor(); + } + } + + // TODO: blinkenlights are unsatisfying right now... + private void setLights(AirlockLightState lightState) + { + float blinkLength = 1.0F; + float blinkInterval = 0.0F; + Color color = Color.Red; + + switch (lightState) + { + case AirlockLightState.Off: + color = Color.Green; + break; + case AirlockLightState.Cycling: + blinkInterval = 1.0F; + blinkLength = 0.75F; + break; + case AirlockLightState.Cooldown: + color = Color.Yellow; + break; + case AirlockLightState.Error: + // the defaults already set this correctly + break; + } + + foreach (IMyLightingBlock light in lights) + { + light.Enabled = true; + light.BlinkIntervalSeconds = blinkInterval; + light.BlinkLength = blinkLength; + light.Color = color; + } + } + + private void lockDoors() + { + innerDoor.Enabled = false; + outerDoor.Enabled = false; + } + + private void error(string error) + { + _p.Echo(error); + setLights(AirlockLightState.Error); + } + } +} \ No newline at end of file diff --git a/Airlock/Airlock.csproj b/Airlock/Airlock.csproj new file mode 100644 index 0000000..f2006f4 --- /dev/null +++ b/Airlock/Airlock.csproj @@ -0,0 +1,26 @@ + + + netframework48 + IngameScript + 6 + false + Release;Debug + x64 + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + \ No newline at end of file diff --git a/Airlock/Airlock.mdk.ini b/Airlock/Airlock.mdk.ini new file mode 100644 index 0000000..5add8f4 --- /dev/null +++ b/Airlock/Airlock.mdk.ini @@ -0,0 +1,22 @@ +; This file is project specific and should be checked in to source control. + +[mdk] +; This is a programmable block script project. +; You should not change this. +type=programmableblock + +; Toggle trace (on|off) (verbose output) +trace=off + +; What type of minification to use (none|trim|stripcomments|lite|full) +; none: No minification +; trim: Removes unused types (NOT members). +; stripcomments: trim + removes comments. +; lite: stripcomments + removes leading/trailing whitespace. +; full: lite + renames identifiers to shorter names. +minify=none + +; A list of files and folder to ignore when creating the script. +; This is a comma separated list of glob patterns. +; See https://code.visualstudio.com/docs/editor/glob-patterns +ignores=obj/**/*,MDK/**/*,**/*.debug.cs diff --git a/Airlock/Airlock.mdk.local.ini b/Airlock/Airlock.mdk.local.ini new file mode 100644 index 0000000..4b66820 --- /dev/null +++ b/Airlock/Airlock.mdk.local.ini @@ -0,0 +1,7 @@ +; This file is _local_ to your machine and should not be checked in to source control. + +[mdk] +; Where to output the script to (auto|specific path) +output=auto +; Override the default binary path (auto|specific path) +binarypath=auto \ No newline at end of file diff --git a/Airlock/Airlock.sln b/Airlock/Airlock.sln new file mode 100644 index 0000000..158ac94 --- /dev/null +++ b/Airlock/Airlock.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Airlock", "Airlock.csproj", "{B6B9BE35-0DAE-4066-AAE0-699803B5EFDB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B6B9BE35-0DAE-4066-AAE0-699803B5EFDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6B9BE35-0DAE-4066-AAE0-699803B5EFDB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6B9BE35-0DAE-4066-AAE0-699803B5EFDB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6B9BE35-0DAE-4066-AAE0-699803B5EFDB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9AE24D08-32D5-4BAA-819E-BC436D40C89D} + EndGlobalSection +EndGlobal diff --git a/Airlock/Instructions.readme b/Airlock/Instructions.readme new file mode 100644 index 0000000..00d32d7 --- /dev/null +++ b/Airlock/Instructions.readme @@ -0,0 +1,12 @@ +A script for controlling airlocks, designed so that a single Programmable Block can handle +every airlock on the grid. + +Airlocks are configured via block naming. Each airlock block should have a name starting with +"AirlockX Function", where "X" is an arbitrary identifier and "Function" specifies the specific +function for that block. + +Possible functions vary by block type: + +Doors should be named "Inner" for the pressurized side of the door, and "Outer" for the vacuum side. +Vents are "Main" (for the vent that actually (de)pressurizes the lock) and "Reference" (optional, see below). +Lights and Oxygen tanks do not require function specification. \ No newline at end of file diff --git a/Airlock/Program.cs b/Airlock/Program.cs new file mode 100644 index 0000000..f7bd865 --- /dev/null +++ b/Airlock/Program.cs @@ -0,0 +1,92 @@ +using Sandbox.Game.EntityComponents; +using Sandbox.ModAPI.Ingame; +using Sandbox.ModAPI.Interfaces; +using SpaceEngineers.Game.ModAPI.Ingame; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using VRage; +using VRage.Collections; +using VRage.Game; +using VRage.Game.Components; +using VRage.Game.GUI.TextPanel; +using VRage.Game.ModAPI.Ingame; +using VRage.Game.ModAPI.Ingame.Utilities; +using VRage.Game.ObjectBuilders.Definitions; +using VRageMath; + +namespace IngameScript +{ + public partial class Program : MyGridProgram + { + private Dictionary _airlocks; + private List> _jobs; + + private int _tickCount = 0; + private MyCommandLine _cli; + + public Program() + { + _cli = new MyCommandLine(); + _jobs = new List>(); + _airlocks = new Dictionary(); + + List doors = new List(); + GridTerminalSystem.GetBlocksOfType(doors); + foreach (IMyDoor door in doors) + { + if (!door.CustomName.StartsWith("Airlock")) continue; + string airlockName = door.CustomName.Split(' ')[0]; + if (_airlocks.ContainsKey(airlockName)) continue; + Airlock newAirlock = new Airlock(this, airlockName); + if (!newAirlock.Functional) + { + Echo($"{airlockName} is missing one or more required blocks."); + continue; + } + _airlocks[airlockName] = newAirlock; + } + } + + public void Main(string argument, UpdateType updateSource) + { + Echo($"index: {_tickCount++}"); + + if (updateSource == UpdateType.Trigger || updateSource == UpdateType.Terminal) + { + _cli.TryParse(argument); + if (_cli.ArgumentCount == 0) { Echo("You must provide an airlock ID."); } + else + { + string airlockName = $"Airlock{_cli.Argument(0)}"; + if (!_airlocks.ContainsKey(airlockName)) + { + Echo($"Invalid airlock ID {_cli.Argument(0)}"); + } + else + { + _jobs.Add(_airlocks[airlockName].CycleAirlock()); + Runtime.UpdateFrequency |= UpdateFrequency.Update1; + } + } + } + + for (int i = 0; i < _jobs.Count; i++) + { + IEnumerator job = _jobs[i]; + if (!job.MoveNext()) + { + job.Dispose(); + _jobs.Remove(job); + i--; + Echo("Airlock Cycling Complete."); + } + } + + if (_jobs.Count == 0) Runtime.UpdateFrequency = UpdateFrequency.None; + } + } +} diff --git a/Airlock/thumb.png b/Airlock/thumb.png new file mode 100644 index 0000000..a8dc2ef Binary files /dev/null and b/Airlock/thumb.png differ diff --git a/airlock.cs b/airlock.cs deleted file mode 100644 index 82dc200..0000000 --- a/airlock.cs +++ /dev/null @@ -1,258 +0,0 @@ -enum AirlockState { - Off, - DoorsClosing, - Cycling, - DoorOpening, - Cooldown, -} - -enum AirlockLightState { - Off, - Cycling, - Cooldown, - Error, -} - -string airlockName; -IEnumerator state; -float targetOxygenLevel = 0.0F; - -// Blocks we need to track -IMyDoor innerDoor; -IMyDoor outerDoor; -List lights; -IMyAirVent airVent; -IMyAirVent airSensor; // TODO: we don't use this yet -IMyGasTank oxygenTank; - -public Program() -{ - airlockName = Me.CustomName.Split(' ')[0]; - state = null; - - // Find the correct objects - InitDoors(); - InitVents(); - InitLights(); - InitOxygen(); -} - -private void InitDoors() { - List onlyDoors = new List(); - GridTerminalSystem.GetBlocksOfType(onlyDoors); - foreach (IMyDoor door in onlyDoors) { - if (door.CustomName.StartsWith(airlockName)) { - if (door.CustomName.Contains("Inner")) { - innerDoor = door; - } - else if (door.CustomName.Contains("Outer")) { - outerDoor = door; - } - } - - if (innerDoor != null && outerDoor != null) { - break; - } - } -} - -private void InitVents() { - List onlyFans = new List(); - GridTerminalSystem.GetBlocksOfType(onlyFans); - foreach (IMyAirVent vent in onlyFans) { - if (vent.CustomName.StartsWith(airlockName)) { - if (vent.CustomName.Contains("Main")) { - airVent = vent; - } - else if (vent.CustomName.Contains("Reference")) { - airSensor = vent; - } - } - - // A global reference vent will be used if we don't have one specific to our airlock. - // A specific vent found later will overwrite this assignment. - if(vent.CustomName.StartsWith("Airlock Reference") && airSensor == null) { - airSensor = vent; - } - } -} - -private void InitLights() { - lights = new List(); - List allLights = new List(); - GridTerminalSystem.GetBlocksOfType(allLights); - foreach (IMyLightingBlock light in allLights) { - if (light.CustomName.StartsWith(airlockName)) { - lights.Add(light); - } - } -} - -private void InitOxygen() { - List allTanks = new List(); - GridTerminalSystem.GetBlocksOfType(allTanks); - foreach (IMyGasTank tank in allTanks) { - if (tank.CustomName.StartsWith(airlockName)) { - oxygenTank = tank; - break; - } - } -} - -public void Main(string argument, UpdateType updateSource) -{ - if (state == null) { - state = CycleAirlock(); - Runtime.UpdateFrequency = UpdateFrequency.Update1; - return; - } - - if (!state.MoveNext()) { - state.Dispose(); - state = null; - Runtime.UpdateFrequency = UpdateFrequency.None; - } -} - -private IEnumerator CycleAirlock() { - SetLights(AirlockLightState.Cycling); - - CloseDoors(); - while(!DoorsClosed()) { yield return true; } - LockDoors(); - - if (!airVent.CanPressurize) { - Error("Airlock is not airtight."); - yield return false; - } - - PressurizeDepressurize(); - while(!PressureStabilized()) { yield return true; } - airVent.Enabled = false; - - OpenDoor(); - while(!DoorOpened()) { yield return true; } - LockDoors(); - - // Balance oxygen storage - SetLights(AirlockLightState.Cooldown); - if (BalanceOxygen()) { - while(!OxygenBalanced()) { yield return true; } - } - airVent.Enabled = false; - - // Cooldown period - Runtime.UpdateFrequency = UpdateFrequency.Update100; - yield return true; - Runtime.UpdateFrequency = UpdateFrequency.Update1; - - SetLights(AirlockLightState.Off); -} - -private void CloseDoors() { - Echo("DEBUG: Closing Doors"); - - // close the doors - innerDoor.Enabled = true; - outerDoor.Enabled = true; - innerDoor.CloseDoor(); - outerDoor.CloseDoor(); -} - -private bool DoorsClosed() { - return innerDoor.Status == DoorStatus.Closed && outerDoor.Status == DoorStatus.Closed; -} - -private void PressurizeDepressurize() { - Echo("DEBUG: Cycling"); - - // toggle the current state - airVent.Depressurize = !airVent.Depressurize; - airVent.Enabled = true; - - // When depressurizing, check the external pressure and only depressurize to that value. - // TODO: test this for floating point errors - if (airVent.Depressurize && airSensor != null) { - targetOxygenLevel = airSensor.GetOxygenLevel(); - Echo($"Set depressurization target to {targetOxygenLevel}"); - } -} - -// TODO: this should check for depressurizing to the *reference* amount -private bool PressureStabilized() { - return ((airVent.Depressurize && airVent.GetOxygenLevel() <= targetOxygenLevel) || - (!airVent.Depressurize && airVent.Status == VentStatus.Pressurized)); -} - -// Open the appropriate door based on pressure state. -private void OpenDoor() { - Echo("DEBUG: Opening Door"); - - if (airVent.Status == VentStatus.Pressurized) { - innerDoor.Enabled = true; - innerDoor.OpenDoor(); - } else { - outerDoor.Enabled = true; - outerDoor.OpenDoor(); - } -} - -private bool DoorOpened() { - return innerDoor.Status == DoorStatus.Open || outerDoor.Status == DoorStatus.Open; -} - -// Returns false if we are in a state where we can't or don't need to balance -private bool BalanceOxygen() { - if (innerDoor.Status == DoorStatus.Closed || outerDoor.Status == DoorStatus.Open || OxygenBalanced()) { return false; } - - Echo("DEBUG: Balancing Oxygen Tank"); - - // Configure the vent to suck in Oxygen. - airVent.Depressurize = true; - airVent.Enabled = true; - return true; -} - -private bool OxygenBalanced() { - return oxygenTank.FilledRatio > 0.25; -} - -// TODO: blinkenlights are unsatisfying right now... -private void SetLights(AirlockLightState lightState) { - float blinkLength = 1.0F; - float blinkInterval = 0.0F; - Color color = Color.Red; - - switch(lightState) { - case AirlockLightState.Off: - color = Color.Green; - break; - case AirlockLightState.Cycling: - blinkInterval = 1.0F; - blinkLength = 0.75F; - break; - case AirlockLightState.Cooldown: - color = Color.Yellow; - break; - case AirlockLightState.Error: - // the defaults already set this correctly - break; - } - - foreach (IMyLightingBlock light in lights) { - light.Enabled = true; - light.BlinkIntervalSeconds = blinkInterval; - light.BlinkLength = blinkLength; - light.Color = color; - } -} - -private void LockDoors() { - innerDoor.Enabled = false; - outerDoor.Enabled = false; -} - -private void Error(string error) { - Echo(error); - SetLights(AirlockLightState.Error); -} \ No newline at end of file