Pressure Relief Valve (#36708)

* initial system (this math is probably WRONG)

* General code cleanup and OnExamined support
(holy moly this code sucks)

* UICode and related events foundation
TODO:
- Actually write the XAML UI and the underlying system
- Un-shitcode the entire thing
- Actually test everything...

* Working UI code
TODO: Make predicted, as this certainly isn't predicted. Even though I said it was. It isn't.

* Remove one TODO for unshitcoding the examine code

* Add reminder
yea

* Make predicted (defenitely isn't)
(also defenitely isn't a copypaste from pressure pump code)

* It's predicted!
TODO:
- Give it snazzy predicted visuals!
- Have a different field for pressure entry, lest it gets bulldozed every UI update.

* Improve gas pressure relief valve UI
TODO: Reminder to reduce amount of dirties using deltafields

* Implement DirtyField prediction

* Entity<T> cleanup
A lot of Entity<T> conversions and lukewarm cleanup.

Also got caught copy pasting code in 4K UHD but it's not like you couldn't tell.

* More cleanup and comments

* Remove TODO comment on bulldozing window title

* """refactoring"""
- Move appearance out of shared and finally fix it. Pointless to predict appearance in this instance.
- More Entity<T> conversions because I like them.
- Move UI creation handling over entirely to the ActivatableUI system.
- Fix a hardcoded locale string (why????).

* Add visuals

* Revert debugging variable replacememt
yea

* Revert skissue

* Remove unused using directives and remove TODO

* Localize, cleanup, document

* Fix adminlogging discrepancy

* Add ability to construct, add guidebook entry

* Clear up comment

* Add guidebook tooltip to valve

* Convert GasPressureReliefValveBoundUserInterface declaration into primary constructor

* Adds more input handling and adds autofill on open

* Un-deepfry input validator shitcode
Genuinely what was I smoking

* improve visuals logic

* Refactor again
- Update math to the correct implementation
- Moved code that could be re-used in the future into a helper method under AtmosphereSystem.Gases.cs

* I'm sorry but I hate warnings

* Remove unused using directive in AtmosphereSystem.Gases.cs

* Review and cleanup

* Lukewarm UI glossup

* Maintainer for the upstream project btw

* Remove redundant state sets and messy logic

* Unduplicate valve updater code

* Redo UI (im sorry Slarti)

* run tests

* Test refactored UI messaging

* Second round of UI improvements
- God please find a way to improve this system. Feels bad.

* Update loop implementation

* Further predict UI

* Clear up SetToCurrentThreshold

* cleanup

* Update to master + pipe layers and bug fixes
want to run tests

* fixes

* Deploy rename pipebomb

* Documentation and requested changes

* Rename the method that wiggled away

* Undo rounding changes

* Fix comment

* Rename and cleanup

* Apply suggestions from code review

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
ArtisticRoomba
2025-07-03 09:00:34 -07:00
committed by GitHub
parent 1bc3d37d40
commit f874459092
32 changed files with 1173 additions and 80 deletions

View File

@@ -0,0 +1,31 @@
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Piping.Binary.Components;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// Represents the client system responsible for managing and updating the gas pressure regulator interface.
/// Inherits from the shared system <see cref="SharedGasPressureRegulatorSystem"/>.
/// </summary>
public sealed partial class GasPressureRegulatorSystem : SharedGasPressureRegulatorSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasPressureRegulatorComponent, AfterAutoHandleStateEvent>(OnValveUpdate);
}
private void OnValveUpdate(Entity<GasPressureRegulatorComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateUi(ent);
}
protected override void UpdateUi(Entity<GasPressureRegulatorComponent> ent)
{
if (UserInterfaceSystem.TryGetOpenUi(ent.Owner, GasPressureRegulatorUiKey.Key, out var bui))
{
bui.Update();
}
}
}

View File

@@ -0,0 +1,58 @@
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Localizations;
using Robust.Client.UserInterface;
namespace Content.Client.Atmos.UI;
public sealed class GasPressureRegulatorBoundUserInterface(EntityUid owner, Enum uiKey)
: BoundUserInterface(owner, uiKey)
{
private GasPressureRegulatorWindow? _window;
protected override void Open()
{
base.Open();
_window = this.CreateWindow<GasPressureRegulatorWindow>();
_window.SetEntity(Owner);
_window.ThresholdPressureChanged += OnThresholdChanged;
if (EntMan.TryGetComponent(Owner, out GasPressureRegulatorComponent? comp))
_window.SetThresholdPressureInput(comp.Threshold);
Update();
}
public override void Update()
{
if (_window == null)
return;
_window.Title = Identity.Name(Owner, EntMan);
if (!EntMan.TryGetComponent(Owner, out GasPressureRegulatorComponent? comp))
return;
_window.SetThresholdPressureLabel(comp.Threshold);
_window.UpdateInfo(comp.InletPressure, comp.OutletPressure, comp.FlowRate);
}
private void OnThresholdChanged(string newThreshold)
{
var sentThreshold = 0f;
if (UserInputParser.TryFloat(newThreshold, out var parsedNewThreshold) && parsedNewThreshold >= 0 &&
!float.IsInfinity(parsedNewThreshold))
{
sentThreshold = parsedNewThreshold;
}
// Autofill to zero if the user inputs an invalid value.
_window?.SetThresholdPressureInput(sentThreshold);
SendPredictedMessage(new GasPressureRegulatorChangeThresholdMessage(sentThreshold));
}
}

View File

@@ -0,0 +1,96 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="345 380"
MinSize="345 380"
Title="{Loc gas-pressure-regulator-ui-title}"
Resizable="False">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical" Margin="0 10 0 10">
<BoxContainer Orientation="Vertical" Align="Center">
<Label Text="{Loc gas-pressure-regulator-ui-outlet}" Align="Center" StyleClasses="LabelKeyText" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center">
<Label Name="OutletPressureLabel" Text="N/A" Margin="0 0 4 0" />
<Label Text="{Loc gas-pressure-regulator-ui-pressure-unit}" />
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Align="Center">
<BoxContainer Orientation="Vertical" Align="Center" HorizontalExpand="True">
<Label Text="{Loc gas-pressure-regulator-ui-target}" Align="Right" StyleClasses="LabelKeyText" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Right">
<Label Name="TargetPressureLabel" Margin="0 0 4 0" />
<Label Text="{Loc gas-pressure-regulator-ui-pressure-unit}" />
</BoxContainer>
</BoxContainer>
<ProgressBar Name="ToTargetBar" MaxValue="1" SetSize="5 75" Margin="10" Vertical="True" />
<SpriteView Name="EntityView" SetSize="64 64" Scale="3 3" OverrideDirection="North" Margin="0" />
<ProgressBar Name="FlowRateBar" MaxValue="1" SetSize="5 75" Margin="10" Vertical="True" />
<BoxContainer Orientation="Vertical" Align="Center" HorizontalExpand="True">
<Label Text="{Loc gas-pressure-regulator-ui-flow}" StyleClasses="LabelKeyText" />
<BoxContainer Orientation="Horizontal">
<Label Name="CurrentFlowLabel" Text="N/A" Margin="0 0 4 0" />
<Label Text="{Loc gas-pressure-regulator-ui-flow-rate-unit}" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Vertical" Align="Center" Margin="1">
<Label Text="{Loc gas-pressure-regulator-ui-inlet}" Align="Center" StyleClasses="LabelKeyText" />
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Center">
<Label Name="InletPressureLabel" Text="N/A" Margin="0 0 4 0" />
<Label Text="{Loc gas-pressure-regulator-ui-pressure-unit}" />
</BoxContainer>
</BoxContainer>
</BoxContainer>
<!-- Controls to Set Pressure -->
<controls:StripeBack Name="SetPressureStripeBack" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="10 10 10 10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<LineEdit Name="ThresholdInput" HorizontalExpand="True" MinSize="70 0" />
<Button Name="SetThresholdButton" Text="{Loc gas-pressure-regulator-ui-set-threshold}"
Disabled="True" Margin="5 0 0 0" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 5 0 0">
<Button Name="Subtract1000Button" Text="{Loc gas-pressure-regulator-ui-subtract-1000}"
HorizontalExpand="True" Margin="0 2 2 0"
StyleClasses="OpenBoth" />
<Button Name="Subtract100Button" Text="{Loc gas-pressure-regulator-ui-subtract-100}"
HorizontalExpand="True" Margin="0 2 2 0"
StyleClasses="OpenBoth" />
<Button Name="Subtract10Button" Text="{Loc gas-pressure-regulator-ui-subtract-10}"
HorizontalExpand="True" Margin="0 2 2 0"
StyleClasses="OpenBoth" />
<Button Name="Add10Button" Text="{Loc gas-pressure-regulator-ui-add-10}" HorizontalExpand="True"
Margin="0 2 2 0"
StyleClasses="OpenBoth" />
<Button Name="Add100Button" Text="{Loc gas-pressure-regulator-ui-add-100}"
HorizontalExpand="True" Margin="0 2 2 0"
StyleClasses="OpenBoth" />
<Button Name="Add1000Button" Text="{Loc gas-pressure-regulator-ui-add-1000}"
HorizontalExpand="True" Margin="0 2 2 0"
StyleClasses="OpenBoth" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 5 0 0">
<Button Name="ZeroThresholdButton" Text="{Loc gas-pressure-regulator-ui-zero-threshold}"
HorizontalExpand="True" Margin="0 0 5 0" />
<Button Name="SetToCurrentPressureButton"
Text="{Loc gas-pressure-regulator-ui-set-to-current-pressure}" HorizontalExpand="True" />
</BoxContainer>
</BoxContainer>
</controls:StripeBack>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,129 @@
using System.Globalization;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Atmos.UI;
/// <summary>
/// Client-side UI for controlling a pressure regulator.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class GasPressureRegulatorWindow : FancyWindow
{
private float _flowRate;
public GasPressureRegulatorWindow()
{
RobustXamlLoader.Load(this);
ThresholdInput.OnTextChanged += _ => SetThresholdButton.Disabled = false;
SetThresholdButton.OnPressed += _ =>
{
ThresholdPressureChanged?.Invoke(ThresholdInput.Text ??= "");
SetThresholdButton.Disabled = true;
};
SetToCurrentPressureButton.OnPressed += _ =>
{
if (InletPressureLabel.Text != null)
{
ThresholdInput.Text = InletPressureLabel.Text;
}
SetThresholdButton.Disabled = false;
};
ZeroThresholdButton.OnPressed += _ =>
{
ThresholdInput.Text = "0";
SetThresholdButton.Disabled = false;
};
Add1000Button.OnPressed += _ => AdjustThreshold(1000);
Add100Button.OnPressed += _ => AdjustThreshold(100);
Add10Button.OnPressed += _ => AdjustThreshold(10);
Subtract10Button.OnPressed += _ => AdjustThreshold(-10);
Subtract100Button.OnPressed += _ => AdjustThreshold(-100);
Subtract1000Button.OnPressed += _ => AdjustThreshold(-1000);
return;
void AdjustThreshold(float adjustment)
{
if (float.TryParse(ThresholdInput.Text, out var currentValue))
{
ThresholdInput.Text = (currentValue + adjustment).ToString(CultureInfo.CurrentCulture);
SetThresholdButton.Disabled = false;
}
}
}
public event Action<string>? ThresholdPressureChanged;
/// <summary>
/// Sets the current threshold pressure label. This is not setting the threshold input box.
/// </summary>
/// <param name="threshold"> Threshold to set.</param>
public void SetThresholdPressureLabel(float threshold)
{
TargetPressureLabel.Text = threshold.ToString(CultureInfo.CurrentCulture);
}
/// <summary>
/// Sets the threshold pressure input field with the given value.
/// When the client opens the UI the field will be autofilled with the current threshold pressure.
/// </summary>
/// <param name="input">The threshold pressure value to autofill into the input field.</param>
public void SetThresholdPressureInput(float input)
{
ThresholdInput.Text = input.ToString(CultureInfo.CurrentCulture);
}
/// <summary>
/// Sets the entity to be visible in the UI.
/// </summary>
/// <param name="entity"></param>
public void SetEntity(EntityUid entity)
{
EntityView.SetEntity(entity);
}
/// <summary>
/// Updates the UI for the labels.
/// </summary>
/// <param name="inletPressure">The current pressure at the valve's inlet.</param>
/// <param name="outletPressure">The current pressure at the valve's outlet.</param>
/// <param name="flowRate">The current flow rate through the valve.</param>
public void UpdateInfo(float inletPressure, float outletPressure, float flowRate)
{
if (float.TryParse(TargetPressureLabel.Text, out var parsedfloat))
ToTargetBar.Value = inletPressure / parsedfloat;
InletPressureLabel.Text = float.Round(inletPressure).ToString(CultureInfo.CurrentCulture);
OutletPressureLabel.Text = float.Round(outletPressure).ToString(CultureInfo.CurrentCulture);
CurrentFlowLabel.Text = float.IsNaN(flowRate) ? "0" : flowRate.ToString(CultureInfo.CurrentCulture);
_flowRate = flowRate;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
// Defines the flow rate at which the progress bar fills in one second.
// If the flow rate is >50 L/s, the bar will take <1 second to fill.
// If the flow rate is <50 L/s, the bar will take >1 second to fill.
const int barFillPerSecond = 50;
var maxValue = FlowRateBar.MaxValue;
// Increment the progress bar value based on elapsed time
FlowRateBar.Value += (_flowRate / barFillPerSecond) * args.DeltaSeconds;
// Reset the progress bar when it is fully filled
if (FlowRateBar.Value >= maxValue)
{
FlowRateBar.Value = 0f;
}
}
}

View File

@@ -252,6 +252,128 @@ namespace Content.Server.Atmos.EntitySystems
Merge(destination, buffer);
}
/// <summary>
/// Calculates the dimensionless fraction of gas required to equalize pressure between two gas mixtures.
/// </summary>
/// <param name="gasMixture1">The first gas mixture involved in the pressure equalization.
/// This mixture should be the one you always expect to be the highest pressure.</param>
/// <param name="gasMixture2">The second gas mixture involved in the pressure equalization.</param>
/// <returns>A float (from 0 to 1) representing the dimensionless fraction of gas that needs to be transferred from the
/// mixture of higher pressure to the mixture of lower pressure.</returns>
/// <remarks>
/// <para>
/// This properly takes into account the effect
/// of gas merging from inlet to outlet affecting the temperature
/// (and possibly increasing the pressure) in the outlet.
/// </para>
/// <para>
/// The gas is assumed to expand freely,
/// so the temperature of the gas with the greater pressure is not changing.
/// </para>
/// </remarks>
/// <example>
/// If you want to calculate the moles required to equalize pressure between an inlet and an outlet,
/// multiply the fraction returned by the source moles.
/// </example>
public float FractionToEqualizePressure(GasMixture gasMixture1, GasMixture gasMixture2)
{
/*
Problem: the gas being merged from the inlet to the outlet could affect the
temp. of the gas and cause a pressure rise.
We want the pressure to be equalized, so we have to account for this.
For clarity, let's assume that gasMixture1 is the inlet and gasMixture2 is the outlet.
We require mechanical equilibrium, so \( P_1' = P_2' \)
Before the transfer, we have:
\( P_1 = \frac{n_1 R T_1}{V_1} \)
\( P_2 = \frac{n_2 R T_2}{V_2} \)
After removing fraction \( x \) moles from the inlet, we have:
\( P_1' = \frac{(1 - x) n_1 R T_1}{V_1} \)
The outlet will gain the same \( x n_1 \) moles of gas.
So \( n_2' = n_2 + x n_1 \)
After mixing, the outlet temperature will be changed.
Denote the new mixture temperature as \( T_2' \).
Volume is constant.
So we have:
\( P_2' = \frac{(n_2 + x n_1) R T_2}{V_2} \)
The total energy of the incoming inlet to outlet gas at \( T_1 \) plus the existing energy of the outlet gas at \( T_2 \)
will be equal to the energy of the new outlet gas at \( T_2' \).
This leads to the following derivation:
\( x n_1 C_1 T_1 + n_2 C_2 T_2 = (x n_1 C_1 + n_2 C_2) T_2' \)
Where \( C_1 \) and \( C_2 \) are the heat capacities of the inlet and outlet gases, respectively.
Solving for \( T_2' \) gives us:
\( T_2' = \frac{x n_1 C_1 T_1 + n_2 C_2 T_2}{x n_1 C_1 + n_2 C_2} \)
Once again, we require mechanical equilibrium (\( P_1' = P_2' \)),
so we can substitute \( T_2' \) into the pressure equation:
\( \frac{(1 - x) n_1 R T_1}{V_1} =
\frac{(n_2 + x n_1) R}{V_2} \cdot
\frac{x n_1 C_1 T_1 + n_2 C_2 T_2}
{x n_1 C_1 + n_2 C_2} \)
Now it's a matter of solving for \( x \).
Not going to show the full derivation here, just steps.
1. Cancel common factor \( R \).
2. Multiply both sides by \( x n_1 C_1 + n_2 C_2 \), so that everything
becomes a polynomial in terms of \( x \).
3. Expand both sides.
4. Collect like powers of \( x \).
5. After collecting, you should end up with a polynomial of the form:
\( (-n_1 C_1 T_1 (1 + \frac{V_2}{V_1})) x^2 +
(n_1 T_1 \frac{V_2}{V_1} (C_1 - C_2) - n_2 C_1 T_1 - n_1 C_2 T_2) x +
(n_1 T_1 \frac{V_2}{V_1} C_2 - n_2 C_2 T_2) = 0 \)
Divide through by \( n_1 C_1 T_1 \) and replace each ratio with a symbol for clarity:
\( k_V = \frac{V_2}{V_1} \)
\( k_n = \frac{n_2}{n_1} \)
\( k_T = \frac{T_2}{T_1} \)
\( k_C = \frac{C_2}{C_1} \)
*/
// Ensure that P_1 > P_2 so the quadratic works out.
if (gasMixture1.Pressure < gasMixture2.Pressure)
{
(gasMixture1, gasMixture2) = (gasMixture2, gasMixture1);
}
// Establish the dimensionless ratios.
var volumeRatio = gasMixture2.Volume / gasMixture1.Volume;
var molesRatio = gasMixture2.TotalMoles / gasMixture1.TotalMoles;
var temperatureRatio = gasMixture2.Temperature / gasMixture1.Temperature;
var heatCapacityRatio = GetHeatCapacity(gasMixture2) / GetHeatCapacity(gasMixture1);
// The quadratic equation is solved for the transfer fraction.
var quadraticA = 1 + volumeRatio;
var quadraticB = molesRatio - volumeRatio + heatCapacityRatio * (temperatureRatio + volumeRatio);
var quadraticC = heatCapacityRatio * (molesRatio * temperatureRatio - volumeRatio);
return (-quadraticB + MathF.Sqrt(quadraticB * quadraticB - 4 * quadraticA * quadraticC)) / (2 * quadraticA);
}
/// <summary>
/// Determines the number of moles that need to be removed from a <see cref="GasMixture"/> to reach a target pressure threshold.
/// </summary>
/// <param name="gasMixture">The gas mixture whose moles and properties will be used in the calculation.</param>
/// <param name="targetPressure">The target pressure threshold to calculate against.</param>
/// <returns>The difference in moles required to reach the target pressure threshold.</returns>
/// <remarks>The temperature of the gas is assumed to be not changing due to a free expansion.</remarks>
public static float MolesToPressureThreshold(GasMixture gasMixture, float targetPressure)
{
// Kid named PV = nRT.
return gasMixture.TotalMoles -
targetPressure * gasMixture.Volume / (Atmospherics.R * gasMixture.Temperature);
}
/// <summary>
/// Checks whether a gas mixture is probably safe.
/// This only checks temperature and pressure, not gas composition.

View File

@@ -0,0 +1,202 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Audio;
using JetBrains.Annotations;
using Robust.Shared.Timing;
namespace Content.Server.Atmos.Piping.Binary.EntitySystems;
/// <summary>
/// Handles serverside logic for pressure regulators. Gas will only flow through the regulator
/// if the pressure on the inlet side is over a certain pressure threshold.
/// See https://en.wikipedia.org/wiki/Pressure_regulator
/// </summary>
[UsedImplicitly]
public sealed class GasPressureRegulatorSystem : SharedGasPressureRegulatorSystem
{
[Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasPressureRegulatorComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<GasPressureRegulatorComponent, AtmosDeviceUpdateEvent>(OnPressureRegulatorUpdated);
SubscribeLocalEvent<GasPressureRegulatorComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<GasPressureRegulatorComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUiUpdate = _timing.CurTime + ent.Comp.UpdateInterval;
}
/// <summary>
/// Dirties the regulator every second or so, so that the UI can update.
/// The UI automatically updates after an AutoHandleStateEvent.
/// </summary>
/// <param name="frameTime"></param>
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<GasPressureRegulatorComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.NextUiUpdate > _timing.CurTime)
continue;
comp.NextUiUpdate += comp.UpdateInterval;
DirtyFields(uid,
comp,
null,
nameof(comp.InletPressure),
nameof(comp.OutletPressure),
nameof(comp.FlowRate));
}
}
private void OnInit(Entity<GasPressureRegulatorComponent> ent, ref ComponentInit args)
{
UpdateAppearance(ent);
}
/// <summary>
/// Handles the updating logic for the pressure regulator.
/// </summary>
/// <param name="ent"> the <see cref="Entity{T}" /> of the pressure regulator</param>
/// <param name="args"> Args provided to us via <see cref="AtmosDeviceUpdateEvent" /></param>
private void OnPressureRegulatorUpdated(Entity<GasPressureRegulatorComponent> ent,
ref AtmosDeviceUpdateEvent args)
{
if (!_nodeContainer.TryGetNodes(ent.Owner,
ent.Comp.InletName,
ent.Comp.OutletName,
out PipeNode? inletPipeNode,
out PipeNode? outletPipeNode))
{
ChangeStatus(false, ent, inletPipeNode, outletPipeNode, 0);
return;
}
/*
It's time for some math! :)
Gas is simply transferred from the inlet to the outlet, restricted by flow rate and pressure.
We want to transfer enough gas to bring the inlet pressure below the threshold,
and only as much as our max flow rate allows.
The equations:
PV = nRT
P1 = P2
Can be used to calculate the amount of gas we need to transfer.
*/
var p1 = inletPipeNode.Air.Pressure;
var p2 = outletPipeNode.Air.Pressure;
if (p1 <= ent.Comp.Threshold || p2 >= p1)
{
ChangeStatus(false, ent, inletPipeNode, outletPipeNode, 0);
return;
}
var t1 = inletPipeNode.Air.Temperature;
// First, calculate the amount of gas we need to transfer to bring us below the threshold.
var deltaMolesToPressureThreshold =
AtmosphereSystem.MolesToPressureThreshold(inletPipeNode.Air, ent.Comp.Threshold);
// Second, calculate the moles required to equalize the pressure.
// We round here to avoid the valve staying enabled for 0.00001 pressure differences.
var deltaMolesToEqualizePressure =
float.Round(_atmosphere.FractionToEqualizePressure(inletPipeNode.Air, outletPipeNode.Air) *
inletPipeNode.Air.TotalMoles,
1,
MidpointRounding.ToPositiveInfinity);
// Third, make sure we only transfer the minimum of the two.
// We do this so that we don't accidentally transfer so much gas to the point
// where the outlet pressure is higher than the inlet.
var deltaMolesToTransfer = Math.Min(deltaMolesToPressureThreshold, deltaMolesToEqualizePressure);
// Fourth, convert to the desired volume to transfer.
var desiredVolumeToTransfer = deltaMolesToTransfer * ((Atmospherics.R * t1) / p1);
// And finally, limit the transfer volume to the max flow rate of the valve.
var actualVolumeToTransfer = Math.Min(desiredVolumeToTransfer,
ent.Comp.MaxTransferRate * _atmosphere.PumpSpeedup() * args.dt);
// We remove the gas from the inlet and merge it into the outlet.
var removed = inletPipeNode.Air.RemoveVolume(actualVolumeToTransfer);
_atmosphere.Merge(outletPipeNode.Air, removed);
// Calculate the flow rate in L/s for the UI.
var sentFlowRate = MathF.Round(actualVolumeToTransfer / args.dt, 1);
ChangeStatus(true, ent, inletPipeNode, outletPipeNode, sentFlowRate);
}
/// <summary>
/// Updates the visual appearance of the pressure regulator based on its current state.
/// </summary>
/// <param name="ent">The <see cref="Entity{GasPressureRegulatorComponent, AppearanceComponent}"/>
/// representing the pressure regulator with respective components.</param>
private void UpdateAppearance(Entity<GasPressureRegulatorComponent> ent)
{
_appearance.SetData(ent,
PressureRegulatorVisuals.State,
ent.Comp.Enabled);
}
/// <summary>
/// Updates the pressure regulator's appearance and sound based on its current state, while
/// also preventing network spamming.
/// Also prepares data for dirtying.
/// </summary>
/// <param name="enabled">The new state to set</param>
/// <param name="ent">The pressure regulator to update</param>
/// <param name="inletNode">The inlet node of the pressure regulator</param>
/// <param name="outletNode">The outlet node of the pressure regulator</param>
/// <param name="flowRate">Current flow rate of the pressure regulator</param>
private void ChangeStatus(bool enabled,
Entity<GasPressureRegulatorComponent> ent,
PipeNode? inletNode,
PipeNode? outletNode,
float flowRate)
{
// First, set data on the component server-side.
ent.Comp.InletPressure = inletNode?.Air.Pressure ?? 0f;
ent.Comp.OutletPressure = outletNode?.Air.Pressure ?? 0f;
ent.Comp.FlowRate = flowRate;
// We need to prevent spamming the network with updates, so only check if we've
// switched states.
if (ent.Comp.Enabled == enabled)
return;
ent.Comp.Enabled = enabled;
_ambientSound.SetAmbience(ent, enabled);
UpdateAppearance(ent);
// The regulator has changed state, so we need to dirty all applicable fields *right now* so the UI updates
// at the same time as everything else.
DirtyFields(ent.AsNullable(),
null,
nameof(ent.Comp.InletPressure),
nameof(ent.Comp.OutletPressure),
nameof(ent.Comp.FlowRate));
}
}

View File

@@ -0,0 +1,70 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Database;
using Content.Shared.Examine;
namespace Content.Shared.Atmos.EntitySystems;
/// <summary>
/// Handles all shared interactions with the gas pressure regulator.
/// </summary>
public abstract class SharedGasPressureRegulatorSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] protected readonly SharedUserInterfaceSystem UserInterfaceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasPressureRegulatorComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<GasPressureRegulatorComponent, GasPressureRegulatorChangeThresholdMessage>(
OnThresholdChangeMessage);
}
/// <summary>
/// Presents predicted examine information to the person examining the valve.
/// </summary>
/// <param name="ent"> <see cref="Entity{T}"/> of the valve</param>
/// <param name="args">Event arguments for examination</param>
private void OnExamined(Entity<GasPressureRegulatorComponent> ent, ref ExaminedEvent args)
{
if (!Transform(ent).Anchored || !args.IsInDetailsRange)
return;
using (args.PushGroup(nameof(GasPressureRegulatorComponent)))
{
args.PushMarkup(Loc.GetString("gas-pressure-regulator-system-examined",
("statusColor", ent.Comp.Enabled ? "green" : "red"),
("open", ent.Comp.Enabled)));
args.PushMarkup(Loc.GetString("gas-pressure-regulator-examined-threshold-pressure",
("threshold", $"{ent.Comp.Threshold:0.#}")));
args.PushMarkup(Loc.GetString("gas-pressure-regulator-examined-flow-rate",
("flowRate", $"{ent.Comp.FlowRate:0.#}")));
}
}
/// <summary>
/// Validates, logs, and updates the pressure threshold of the valve.
/// </summary>
/// <param name="ent">The <see cref="Entity{T}"/> of the valve.</param>
/// <param name="args">The received pressure from the <see cref="GasPressurePumpChangeOutputPressureMessage"/>message.</param>
private void OnThresholdChangeMessage(Entity<GasPressureRegulatorComponent> ent,
ref GasPressureRegulatorChangeThresholdMessage args)
{
ent.Comp.Threshold = Math.Max(0f, args.ThresholdPressure);
_adminLogger.Add(LogType.AtmosVolumeChanged,
LogImpact.Medium,
$"{ToPrettyString(args.Actor):player} set the pressure threshold on {ToPrettyString(ent):device} to {ent.Comp.Threshold}");
// Dirty the entire entity to ensure we get all of that Fresh:tm: UI info from the server.
Dirty(ent);
UpdateUi(ent);
}
protected virtual void UpdateUi(Entity<GasPressureRegulatorComponent> ent)
{
}
}

View File

@@ -0,0 +1,88 @@
using Content.Shared.Guidebook;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Atmos.Piping.Binary.Components;
/// <summary>
/// Defines a gas pressure regulator,
/// which releases gas depending on a set pressure threshold between two pipe nodes.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState(true, true), AutoGenerateComponentPause]
public sealed partial class GasPressureRegulatorComponent : Component
{
/// <summary>
/// Determines whether the valve is open or closed.
/// Used for showing the valve animation, the UI,
/// and on examine.
/// </summary>
[DataField, AutoNetworkedField]
public bool Enabled;
/// <summary>
/// Specifies the pipe node name to be treated as the inlet.
/// </summary>
[DataField]
public string InletName = "inlet";
/// <summary>
/// Specifies the pipe node name to be treated as the outlet.
/// </summary>
[DataField]
public string OutletName = "outlet";
/// <summary>
/// The max transfer rate of the pressure regulator.
/// </summary>
[GuidebookData]
[DataField]
public float MaxTransferRate = Atmospherics.MaxTransferRate;
/// <summary>
/// The server time at which the next UI update will be sent.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextUiUpdate = TimeSpan.Zero;
/// <summary>
/// Sets the opening threshold of the pressure regulator.
/// </summary>
/// <example> If set to 500 kPa, the regulator will only
/// open if the pressure in the inlet side is above
/// 500 kPa. </example>
[DataField, AutoNetworkedField]
public float Threshold;
/// <summary>
/// How often the UI update is sent.
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
#region UI/Examine Info
/// <summary>
/// The current flow rate of the pressure regulator.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[DataField, AutoNetworkedField]
public float FlowRate;
/// <summary>
/// Current inlet pressure the pressure regulator.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[DataField, AutoNetworkedField]
public float InletPressure;
/// <summary>
/// Current outlet pressure of the pressure regulator.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
[DataField, AutoNetworkedField]
public float OutletPressure;
#endregion
}

View File

@@ -0,0 +1,25 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Atmos.Piping.Binary.Components;
/// <summary>
/// Represents the unique key for the UI.
/// </summary>
[Serializable, NetSerializable]
public enum GasPressureRegulatorUiKey : byte
{
Key,
}
/// <summary>
/// Message sent to change the pressure threshold of the gas pressure regulator.
/// </summary>
/// <param name="pressure">The new pressure threshold value.</param>
[Serializable, NetSerializable]
public sealed class GasPressureRegulatorChangeThresholdMessage(float pressure) : BoundUserInterfaceMessage
{
/// <summary>
/// Gets the new threshold pressure value.
/// </summary>
public float ThresholdPressure { get; } = pressure;
}

View File

@@ -31,4 +31,10 @@ namespace Content.Shared.Atmos.Piping
{
Enabled,
}
[Serializable, NetSerializable]
public enum PressureRegulatorVisuals : byte
{
State,
}
}

View File

@@ -0,0 +1,7 @@
# Examine Text
gas-pressure-regulator-system-examined = The valve is [color={$statusColor}]{$open ->
[true] open
*[false] closed
}[/color].
gas-pressure-regulator-examined-threshold-pressure = The threshold pressure is set at [color=lightblue]{$threshold} kPa[/color].
gas-pressure-regulator-examined-flow-rate = The flow rate meter indicates [color=lightblue]{$flowRate} L/s[/color].

View File

@@ -0,0 +1,19 @@
# UI Labels
gas-pressure-regulator-ui-set-threshold = Set
gas-pressure-regulator-ui-zero-threshold = Zero
gas-pressure-regulator-ui-set-to-current-pressure = Set to Inlet Pressure
gas-pressure-regulator-ui-add-10 = +10
gas-pressure-regulator-ui-add-100 = +100
gas-pressure-regulator-ui-add-1000 = +1000
gas-pressure-regulator-ui-subtract-10 = -10
gas-pressure-regulator-ui-subtract-100 = -100
gas-pressure-regulator-ui-subtract-1000 = -1000
gas-pressure-regulator-ui-title = Inlet Pressure Regulator
gas-pressure-regulator-ui-target = Setpoint
gas-pressure-regulator-ui-flow = Flow
gas-pressure-regulator-ui-outlet = Outlet
gas-pressure-regulator-ui-inlet = Inlet
# Units
gas-pressure-regulator-ui-flow-rate-unit = L/s
gas-pressure-regulator-ui-pressure-unit = kPa

View File

@@ -20,6 +20,7 @@ guide-entry-manualvalve = Manual Valve
guide-entry-signalvalve = Signal Valve
guide-entry-pneumaticvalve = Pneumatic Valve
guide-entry-passivegate = Passive Gate
guide-entry-ressureregulator = Pressure Regulator
guide-entry-mixingandfiltering = Mixing and Filtering
guide-entry-gascanisters = Gas Canisters
guide-entry-thermomachines = Thermomachines

View File

@@ -155,6 +155,62 @@
guides:
- Pumps
- type: entity
parent: GasBinaryBase
id: GasPressureRegulator
name: inlet pressure regulator
description: A valve that releases gas when the inlet pressure exceeds a certain threshold.
placement:
mode: SnapgridCenter
components:
- type: Rotatable
- type: Transform
noRot: false
- type: SubFloorHide
visibleLayers:
- enum.SubfloorLayers.FirstLayer
- type: Sprite
sprite: Structures/Piping/Atmospherics/pump.rsi
layers:
- sprite: Structures/Piping/Atmospherics/pipe.rsi
state: pipeStraight
map: [ "enum.PipeVisualLayers.Pipe" ]
- state: pumpPressureRegulator
map: [ "enum.SubfloorLayers.FirstLayer", "enabled" ]
- type: Appearance
- type: GenericVisualizer
visuals:
enum.PressureRegulatorVisuals.State:
enabled:
True: { state: pumpPressureRegulatorOn }
False: { state: pumpPressureRegulator }
- type: PipeColorVisuals
- type: GasPressureRegulator
enabled: false
threshold: 4500
- type: Construction
graph: GasBinary
node: pressureregulator
- type: ActivatableUI
key: enum.GasPressureRegulatorUiKey.Key
blockSpectators: true
- type: ActivatableUIRequiresAnchor
- type: UserInterface
interfaces:
enum.GasPressureRegulatorUiKey.Key:
type: GasPressureRegulatorBoundUserInterface
- type: AmbientSound
enabled: false
volume: -9
range: 5
sound:
path: /Audio/Ambience/Objects/gas_hiss.ogg
- type: AtmosMonitoringConsoleDevice
navMapBlip: GasFlowRegulator
- type: GuideHelp
guides:
- PressureRegulator
- type: entity
parent: GasBinaryBase
id: GasPassiveGate

View File

@@ -120,6 +120,7 @@
- SignalValve
- PneumaticValve
- PassiveGate
- PressureRegulator
- type: guideEntry
id: ManualValve
@@ -141,6 +142,11 @@
name: guide-entry-passivegate
text: "/ServerInfo/Guidebook/Engineering/PassiveGate.xml"
- type: guideEntry
id: PressureRegulator
name: guide-entry-ressureregulator
text: "/ServerInfo/Guidebook/Engineering/PressureRegulator.xml"
- type: guideEntry
id: MixingAndFiltering
name: guide-entry-mixingandfiltering

View File

@@ -22,6 +22,12 @@
amount: 2
doAfter: 1
- to: pressureregulator
steps:
- material: Steel
amount: 2
doAfter: 1
- to: valve
steps:
- material: Steel
@@ -106,6 +112,22 @@
- tool: Welding
doAfter: 1
- node: pressureregulator
entity: GasPressureRegulator
edges:
- to: start
conditions:
- !type:EntityAnchored
anchored: false
completed:
- !type:SpawnPrototype
prototype: SheetSteel1
amount: 2
- !type:DeleteEntity
steps:
- tool: Welding
doAfter: 1
- node: valve
entity: GasValve
edges:

View File

@@ -572,6 +572,17 @@
conditions:
- !type:NoUnstackableInTile
- type: construction
id: GasPressureRegulator
graph: GasBinary
startNode: start
targetNode: pressureregulator
category: construction-category-utilities
placementMode: SnapgridCenter
canBuildInImpassable: false
conditions:
- !type:NoUnstackableInTile
- type: construction
id: GasValve
graph: GasBinary

View File

@@ -49,6 +49,7 @@
<GuideEntityEmbed Entity="SignalControlledValve" Caption=""/>
<GuideEntityEmbed Entity="PressureControlledValve" Caption=""/>
<GuideEntityEmbed Entity="GasPassiveGate" Caption=""/>
<GuideEntityEmbed Entity="GasPressureRegulator" Caption=""/>
</Box>
<Box>
## Valves

View File

@@ -0,0 +1,23 @@
<Document>
# Inlet Pressure Regulator
The Inlet Pressure Regulator is a passive device that allows gas to escape from a [textlink="pipenet" link="PipeNetworks"] when the pressure exceeds a certain threshold.
<Box>
<GuideEntityEmbed Entity="GasPressureRegulator"/>
</Box>
## Operation
The valve will automatically [color=green]open[/color] when the pressure in the pipe exceeds the set threshold, allowing gas to escape to the connected output pipe.
The valve will [color=red]close[/color] again when the pressure drops below the set threshold.
The flow rate of the valve is limited to [color=orange][protodata="GasPressureRegulator" comp="GasPressureRegulator" member="MaxTransferRate"/] L/s[/color].
## Example Uses
The valve is commonly used to prevent overpressure situations in gas systems, such as [textlink="TEG" link="TEG"] cooling loops and [textlink="pipes" link="Pipes"], which would cause a failure in the system (clogged [textlink="pumps" link="Pumps"]).
The valve can also be used to vent off ready-to-use, hot gas from a burn chamber.
For example, it may be undesirable to allow a burn chamber to drop below a specific pressure for a long time, as this may cause the gas to cool down too much and thin out, which would cause a flameout.
An inlet pressure regulator can be used to vent off excess gas, while keeping the pressure in the burn chamber above a certain threshold, which may help in sustaining a fire in the chamber.
</Document>

View File

@@ -6,6 +6,7 @@
<GuideEntityEmbed Entity="SignalControlledValve"/>
<GuideEntityEmbed Entity="PressureControlledValve"/>
<GuideEntityEmbed Entity="GasPassiveGate"/>
<GuideEntityEmbed Entity="GasPressureRegulator"/>
</Box>
All valves do not require [textlink="power" link="Power"] to operate.

View File

@@ -1,11 +1,11 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from https://github.com/tgstation/tgstation at commit 57cd1d59ca019dd0e7811ac451f295f818e573da. Signal valve is a digital valve modified by deltanedas. Manual valve modified by Deerstop at https://github.com/space-wizards/space-station-14/pull/34378. pvalve taken from https://github.com/tgstation/tgstation/commit/584068b59e271c0108557902e8516c70d6ae56f2 and modified by ArtisticRoomba (GitHub)",
"size": {
"x": 32,
"y": 32
},
"license":"CC-BY-SA-3.0",
"copyright":"Taken from https://github.com/tgstation/tgstation at commit 57cd1d59ca019dd0e7811ac451f295f818e573da. Signal valve is a digital valve modified by deltanedas. Manual valve modified by Deerstop at https://github.com/space-wizards/space-station-14/pull/34378",
"states": [
{
"name": "pumpDigitalValve",
@@ -42,7 +42,36 @@
{
"name": "pumpPressureOn",
"directions": 4,
"delays":[ [ 0.1, 0.1, 0.1, 0.1, 0.1 ], [ 0.1, 0.1, 0.1, 0.1, 0.1 ], [ 0.1, 0.1, 0.1, 0.1, 0.1 ], [ 0.1, 0.1, 0.1, 0.1, 0.1 ] ]
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "pumpVolume",
@@ -51,12 +80,86 @@
{
"name": "pumpVolumeOn",
"directions": 4,
"delays":[ [ 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1 ], [ 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1 ], [ 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1 ], [ 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1 ] ]
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
},
{
"name": "pumpVolumeBlocked",
"directions": 4,
"delays":[ [ 1.0, 1.0 ], [ 1.0, 1.0 ], [ 1.0, 1.0 ], [ 1.0, 1.0 ] ]
"delays": [
[
1,
1
],
[
1,
1
],
[
1,
1
],
[
1,
1
]
]
},
{
"name": "pumpPressureRegulator",
"directions": 4
},
{
"name": "pumpPressureRegulatorOn",
"directions": 4
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

View File

@@ -1,11 +1,11 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from https://github.com/tgstation/tgstation at commit 57cd1d59ca019dd0e7811ac451f295f818e573da. Signal valve is a digital valve modified by deltanedas. Manual valve modified by Deerstop at https://github.com/space-wizards/space-station-14/pull/34378. Modified by chromiumboy. Modified by ArtisticRoomba.",
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "Taken from https://github.com/tgstation/tgstation at commit 57cd1d59ca019dd0e7811ac451f295f818e573da. Signal valve is a digital valve modified by deltanedas. Manual valve modified by Deerstop at https://github.com/space-wizards/space-station-14/pull/34378. Modified by chromiumboy.",
"states": [
{
"name": "pumpDigitalValve",
@@ -136,22 +136,30 @@
"directions": 4,
"delays": [
[
1.0,
1.0
1,
1
],
[
1.0,
1.0
1,
1
],
[
1.0,
1.0
1,
1
],
[
1.0,
1.0
1,
1
]
]
},
{
"name": "pumpPressureRegulator",
"directions": 4
},
{
"name": "pumpPressureRegulatorOn",
"directions": 4
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

View File

@@ -1,11 +1,11 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from https://github.com/tgstation/tgstation at commit 57cd1d59ca019dd0e7811ac451f295f818e573da. Signal valve is a digital valve modified by deltanedas. Manual valve modified by Deerstop at https://github.com/space-wizards/space-station-14/pull/34378. Modified by chromiumboy. Modified by ArtisticRoomba.",
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "Taken from https://github.com/tgstation/tgstation at commit 57cd1d59ca019dd0e7811ac451f295f818e573da. Signal valve is a digital valve modified by deltanedas. Manual valve modified by Deerstop at https://github.com/space-wizards/space-station-14/pull/34378. Modified by chromiumboy.",
"states": [
{
"name": "pumpDigitalValve",
@@ -136,22 +136,30 @@
"directions": 4,
"delays": [
[
1.0,
1.0
1,
1
],
[
1.0,
1.0
1,
1
],
[
1.0,
1.0
1,
1
],
[
1.0,
1.0
1,
1
]
]
},
{
"name": "pumpPressureRegulator",
"directions": 4
},
{
"name": "pumpPressureRegulatorOn",
"directions": 4
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B