]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Pressure Relief Valve (#36708)
authorArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Thu, 3 Jul 2025 16:00:34 +0000 (09:00 -0700)
committerGitHub <noreply@github.com>
Thu, 3 Jul 2025 16:00:34 +0000 (18:00 +0200)
* 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>
32 files changed:
Content.Client/Atmos/EntitySystems/GasPressureRegulatorSystem.cs [new file with mode: 0644]
Content.Client/Atmos/UI/GasPressureRegulatorBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Atmos/UI/GasPressureRegulatorWindow.xaml [new file with mode: 0644]
Content.Client/Atmos/UI/GasPressureRegulatorWindow.xaml.cs [new file with mode: 0644]
Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressureRegulatorSystem.cs [new file with mode: 0644]
Content.Shared/Atmos/EntitySystems/SharedGasPressureRegulatorSystem.cs [new file with mode: 0644]
Content.Shared/Atmos/Piping/Binary/Components/GasPressureRegulatorComponent.cs [new file with mode: 0644]
Content.Shared/Atmos/Piping/Binary/Components/SharedGasPressureRegulatorComponent.cs [new file with mode: 0644]
Content.Shared/Atmos/Piping/EnabledAtmosDeviceVisuals.cs
Resources/Locale/en-US/atmos/gas-pressure-regulator-system.ftl [new file with mode: 0644]
Resources/Locale/en-US/components/gas-pressure-regulator-component.ftl [new file with mode: 0644]
Resources/Locale/en-US/guidebook/guides.ftl
Resources/Prototypes/Entities/Structures/Piping/Atmospherics/binary.yml
Resources/Prototypes/Guidebook/engineering.yml
Resources/Prototypes/Recipes/Construction/Graphs/utilities/atmos_binary.yml
Resources/Prototypes/Recipes/Construction/utilities.yml
Resources/ServerInfo/Guidebook/Engineering/GasManipulation.xml
Resources/ServerInfo/Guidebook/Engineering/PressureRegulator.xml [new file with mode: 0644]
Resources/ServerInfo/Guidebook/Engineering/Valves.xml
Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/meta.json
Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressure.png
Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressureRegulator.png [new file with mode: 0644]
Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressureRegulatorOn.png [new file with mode: 0644]
Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/meta.json
Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressure.png
Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressureRegulator.png [new file with mode: 0644]
Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressureRegulatorOn.png [new file with mode: 0644]
Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/meta.json
Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressure.png
Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressureRegulator.png [new file with mode: 0644]
Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressureRegulatorOn.png [new file with mode: 0644]

diff --git a/Content.Client/Atmos/EntitySystems/GasPressureRegulatorSystem.cs b/Content.Client/Atmos/EntitySystems/GasPressureRegulatorSystem.cs
new file mode 100644 (file)
index 0000000..6f12297
--- /dev/null
@@ -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();
+        }
+    }
+}
diff --git a/Content.Client/Atmos/UI/GasPressureRegulatorBoundUserInterface.cs b/Content.Client/Atmos/UI/GasPressureRegulatorBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..9d66a99
--- /dev/null
@@ -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));
+    }
+}
diff --git a/Content.Client/Atmos/UI/GasPressureRegulatorWindow.xaml b/Content.Client/Atmos/UI/GasPressureRegulatorWindow.xaml
new file mode 100644 (file)
index 0000000..b6a55f7
--- /dev/null
@@ -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>
diff --git a/Content.Client/Atmos/UI/GasPressureRegulatorWindow.xaml.cs b/Content.Client/Atmos/UI/GasPressureRegulatorWindow.xaml.cs
new file mode 100644 (file)
index 0000000..a7c0570
--- /dev/null
@@ -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;
+        }
+    }
+}
index f38ec3fef60b2b6247837955459d0b38ed4a79d3..6893940a97e6ef39954696d7c394b902a60dd93d 100644 (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.
diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressureRegulatorSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressureRegulatorSystem.cs
new file mode 100644 (file)
index 0000000..076d5ad
--- /dev/null
@@ -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));
+    }
+}
diff --git a/Content.Shared/Atmos/EntitySystems/SharedGasPressureRegulatorSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedGasPressureRegulatorSystem.cs
new file mode 100644 (file)
index 0000000..6e2d3e3
--- /dev/null
@@ -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)
+    {
+    }
+}
diff --git a/Content.Shared/Atmos/Piping/Binary/Components/GasPressureRegulatorComponent.cs b/Content.Shared/Atmos/Piping/Binary/Components/GasPressureRegulatorComponent.cs
new file mode 100644 (file)
index 0000000..2e893d2
--- /dev/null
@@ -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
+}
diff --git a/Content.Shared/Atmos/Piping/Binary/Components/SharedGasPressureRegulatorComponent.cs b/Content.Shared/Atmos/Piping/Binary/Components/SharedGasPressureRegulatorComponent.cs
new file mode 100644 (file)
index 0000000..74ca041
--- /dev/null
@@ -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;
+}
index dfc7b7a386cbaaa1408ecf400f53547babf9e889..5193afcdd9ac7b055b3eac97823a394e3233a786 100644 (file)
@@ -31,4 +31,10 @@ namespace Content.Shared.Atmos.Piping
     {
         Enabled,
     }
+
+    [Serializable, NetSerializable]
+    public enum PressureRegulatorVisuals : byte
+    {
+        State,
+    }
 }
diff --git a/Resources/Locale/en-US/atmos/gas-pressure-regulator-system.ftl b/Resources/Locale/en-US/atmos/gas-pressure-regulator-system.ftl
new file mode 100644 (file)
index 0000000..0f30f3d
--- /dev/null
@@ -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].
diff --git a/Resources/Locale/en-US/components/gas-pressure-regulator-component.ftl b/Resources/Locale/en-US/components/gas-pressure-regulator-component.ftl
new file mode 100644 (file)
index 0000000..2434e2d
--- /dev/null
@@ -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
index 004b6b39a587c8513c7aebe5f66c528c3880e846..b55193fef627ca51536beabf63ff3a1edaf0a1c5 100644 (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
index 88940f48824c789930479e1de7d67ca349380895..e9c5403805c8b3587b68cf03607620da914a4c47 100644 (file)
       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
index e0803044dc741c920ddf8fa1310e479b8a8d4ac0..96e049a3999c5d1be2400eee33f79c1aa73fc245 100644 (file)
   - SignalValve
   - PneumaticValve
   - PassiveGate
+  - PressureRegulator
 
 - type: guideEntry
   id: ManualValve
   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
index aed91671789fdad5593b72caabe4c663f12deab1..6159bc5936fe9971252f5733d6d035fbd930b380 100644 (file)
         amount: 2
         doAfter: 1
 
+    - to: pressureregulator
+      steps:
+      - material: Steel
+        amount: 2
+        doAfter: 1
+
     - to: valve
       steps:
       - material: Steel
       - 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:
index 85a0f5d3b1ba30b98e7fc3cf507412960a35da3c..f9c9867aac724157974fa3360171e01a29016a0a 100644 (file)
   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
index 3a3857ae4cb3b408c9437072171a7a174a13d568..6070e3b985164004fb50a45c8c259b852406a496 100644 (file)
@@ -49,6 +49,7 @@
     <GuideEntityEmbed Entity="SignalControlledValve" Caption=""/>
     <GuideEntityEmbed Entity="PressureControlledValve" Caption=""/>
     <GuideEntityEmbed Entity="GasPassiveGate" Caption=""/>
+    <GuideEntityEmbed Entity="GasPressureRegulator" Caption=""/>
   </Box>
   <Box>
     ## Valves
diff --git a/Resources/ServerInfo/Guidebook/Engineering/PressureRegulator.xml b/Resources/ServerInfo/Guidebook/Engineering/PressureRegulator.xml
new file mode 100644 (file)
index 0000000..2b23ea5
--- /dev/null
@@ -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>
index db8c18807cd8133bfbbe127007eb0984e9fcb5e0..46f5b0f62927f0ea6a6defee013456f5ebec2c90 100644 (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.
 
index 632c7bcf5be23bcd03df2e5da871a2c771ad9a85..4dceb4af72e727de2f15ea64a48df6d11e7df1df 100644 (file)
 {
-   "version":1,
-   "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",
-         "directions":4
-      },
-      {
-         "name":"pumpManualValve",
-         "directions":4
-      },
-      {
-         "name":"pumpManualValveOn",
-         "directions":4
-      },
-      {
-         "name":"pumpSignalValve",
-         "directions":4
-      },
-      {
-         "name":"pumpSignalValveOn",
-         "directions":4
-      },
-      {
-         "name":"pumpPassiveGate",
-         "directions":4
-      },
-      {
-         "name":"pumpPassiveGateOn",
-         "directions":4
-      },
-      {
-         "name":"pumpPressure",
-         "directions":4
-      },
-      {
-         "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 ] ]
-      },
-      {
-         "name":"pumpVolume",
-         "directions":4
-      },
-      {
-         "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 ] ]
-      },
-      {
-         "name":"pumpVolumeBlocked",
-         "directions":4,
-         "delays":[ [ 1.0, 1.0 ], [ 1.0, 1.0 ], [ 1.0, 1.0 ], [ 1.0, 1.0 ] ]
-      }
-   ]
+    "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
+    },
+    "states": [
+        {
+            "name": "pumpDigitalValve",
+            "directions": 4
+        },
+        {
+            "name": "pumpManualValve",
+            "directions": 4
+        },
+        {
+            "name": "pumpManualValveOn",
+            "directions": 4
+        },
+        {
+            "name": "pumpSignalValve",
+            "directions": 4
+        },
+        {
+            "name": "pumpSignalValveOn",
+            "directions": 4
+        },
+        {
+            "name": "pumpPassiveGate",
+            "directions": 4
+        },
+        {
+            "name": "pumpPassiveGateOn",
+            "directions": 4
+        },
+        {
+            "name": "pumpPressure",
+            "directions": 4
+        },
+        {
+            "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
+                ]
+            ]
+        },
+        {
+            "name": "pumpVolume",
+            "directions": 4
+        },
+        {
+            "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
+                ]
+            ]
+        },
+        {
+            "name": "pumpVolumeBlocked",
+            "directions": 4,
+            "delays": [
+                [
+                    1,
+                    1
+                ],
+                [
+                    1,
+                    1
+                ],
+                [
+                    1,
+                    1
+                ],
+                [
+                    1,
+                    1
+                ]
+            ]
+        },
+        {
+            "name": "pumpPressureRegulator",
+            "directions": 4
+        },
+        {
+            "name": "pumpPressureRegulatorOn",
+            "directions": 4
+        }
+    ]
 }
index 8903bc856494bc09fcbca8b79ba7ac1de14bb100..eb35e1a42ead91135891251f06656cd50fe4f6b0 100644 (file)
Binary files a/Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressure.png and b/Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressure.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressureRegulator.png b/Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressureRegulator.png
new file mode 100644 (file)
index 0000000..cb7322d
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressureRegulator.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressureRegulatorOn.png b/Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressureRegulatorOn.png
new file mode 100644 (file)
index 0000000..8a8f85c
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/pump.rsi/pumpPressureRegulatorOn.png differ
index ef75896795379da035c22a738815b1aaf61d2e9f..d7a2ccb3b617635d5bfc92b38397bfe31d94966e 100644 (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",
             "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
         }
     ]
 }
index 3a634fc9294b4ab9c7156ff4771c43e83225b1ac..94e5322cfca0f5bb02ef4cbf62a5a3faeef7bfb7 100644 (file)
Binary files a/Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressure.png and b/Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressure.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressureRegulator.png b/Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressureRegulator.png
new file mode 100644 (file)
index 0000000..ee53abc
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressureRegulator.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressureRegulatorOn.png b/Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressureRegulatorOn.png
new file mode 100644 (file)
index 0000000..8523dbc
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/pump_alt1.rsi/pumpPressureRegulatorOn.png differ
index ef75896795379da035c22a738815b1aaf61d2e9f..d7a2ccb3b617635d5bfc92b38397bfe31d94966e 100644 (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",
             "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
         }
     ]
 }
index cb5286e46c2f0b5e07fbfaa2bd95dd56748e6c70..a5961b099611f9aa0c5f31ff28177ccb84c6f6b0 100644 (file)
Binary files a/Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressure.png and b/Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressure.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressureRegulator.png b/Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressureRegulator.png
new file mode 100644 (file)
index 0000000..dcca92d
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressureRegulator.png differ
diff --git a/Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressureRegulatorOn.png b/Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressureRegulatorOn.png
new file mode 100644 (file)
index 0000000..3f5eabd
Binary files /dev/null and b/Resources/Textures/Structures/Piping/Atmospherics/pump_alt2.rsi/pumpPressureRegulatorOn.png differ