Files
tbd-station-14/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressureRegulatorSystem.cs
ArtisticRoomba f874459092 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>
2025-07-03 18:00:34 +02:00

203 lines
8.0 KiB
C#

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));
}
}