Convert airlock into a multiplex controller and use MDK.

This commit is contained in:
Anna Rose 2025-02-07 18:09:19 -05:00
parent d2952a3111
commit 446aa29742
9 changed files with 489 additions and 258 deletions

305
Airlock/Airlock.cs Normal file
View File

@ -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<IMyLightingBlock> 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<IMyDoor> onlyDoors = new List<IMyDoor>();
_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<IMyAirVent> onlyFans = new List<IMyAirVent>();
_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<IMyLightingBlock>();
List<IMyLightingBlock> allLights = new List<IMyLightingBlock>();
_p.GridTerminalSystem.GetBlocksOfType(allLights);
foreach (IMyLightingBlock light in allLights)
{
if (!light.CustomName.StartsWith(_name)) continue;
lights.Add(light);
}
}
private void initOxygen()
{
List<IMyGasTank> allTanks = new List<IMyGasTank>();
_p.GridTerminalSystem.GetBlocksOfType(allTanks);
foreach (IMyGasTank tank in allTanks)
{
if (!tank.CustomName.StartsWith(_name)) continue;
oxygenTank = tank;
return;
}
Functional = false;
}
public IEnumerator<bool> 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);
}
}
}

26
Airlock/Airlock.csproj Normal file
View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netframework48</TargetFramework>
<RootNamespace>IngameScript</RootNamespace>
<LangVersion>6</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<Configurations>Release;Debug</Configurations>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mal.Mdk2.PbAnalyzers" Version="2.1.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Mal.Mdk2.PbPackager" Version="2.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Mal.Mdk2.References" Version="2.2.4" />
</ItemGroup>
<ItemGroup>
<None Remove="Instructions.readme" />
<AdditionalFiles Include="Instructions.readme" />
<AdditionalFiles Include="thumb.png" />
</ItemGroup>
</Project>

22
Airlock/Airlock.mdk.ini Normal file
View File

@ -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

View File

@ -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

25
Airlock/Airlock.sln Normal file
View File

@ -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

View File

@ -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.

92
Airlock/Program.cs Normal file
View File

@ -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<string, Airlock> _airlocks;
private List<IEnumerator<bool>> _jobs;
private int _tickCount = 0;
private MyCommandLine _cli;
public Program()
{
_cli = new MyCommandLine();
_jobs = new List<IEnumerator<bool>>();
_airlocks = new Dictionary<string, Airlock>();
List<IMyDoor> doors = new List<IMyDoor>();
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<bool> job = _jobs[i];
if (!job.MoveNext())
{
job.Dispose();
_jobs.Remove(job);
i--;
Echo("Airlock Cycling Complete.");
}
}
if (_jobs.Count == 0) Runtime.UpdateFrequency = UpdateFrequency.None;
}
}
}

BIN
Airlock/thumb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

View File

@ -1,258 +0,0 @@
enum AirlockState {
Off,
DoorsClosing,
Cycling,
DoorOpening,
Cooldown,
}
enum AirlockLightState {
Off,
Cycling,
Cooldown,
Error,
}
string airlockName;
IEnumerator<bool> state;
float targetOxygenLevel = 0.0F;
// Blocks we need to track
IMyDoor innerDoor;
IMyDoor outerDoor;
List<IMyLightingBlock> 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<IMyDoor> onlyDoors = new List<IMyDoor>();
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<IMyAirVent> onlyFans = new List<IMyAirVent>();
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<IMyLightingBlock>();
List<IMyLightingBlock> allLights = new List<IMyLightingBlock>();
GridTerminalSystem.GetBlocksOfType(allLights);
foreach (IMyLightingBlock light in allLights) {
if (light.CustomName.StartsWith(airlockName)) {
lights.Add(light);
}
}
}
private void InitOxygen() {
List<IMyGasTank> allTanks = new List<IMyGasTank>();
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<bool> 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);
}