Add gas recyclers (#9934)

* Add RemoveVolume()

RemoveVolume(vol) captures the common pattern of:

    air.RemoveRatio(vol / air.Volume)

Change existing code to use this method where appropriate.

* Add gas recyclers

Gas recyclers catalyze the conversion of CO2 and N2O to O2 and N2. The
gas recycler component takes waste gas from the input net and releases
the result into the output net.

To make things more fun, the input net must be pressurized to 3 MPa and
heated to at least 300 C; otherwise, no reaction will occur.

Game-mechanic wise, gas recyclers contain the catalyst for the
conversion reaction, and therefore, requires no external power. However,
the external pumps and heaters required to make the reaction happen
still do.

* Fix gas recyclers

Fix negative sqrt, fix pressure check after remove.
This commit is contained in:
Kevin Zheng
2022-09-11 16:02:01 -07:00
committed by GitHub
parent dd66bf305e
commit 45349e3c0d
18 changed files with 256 additions and 12 deletions

View File

@@ -153,7 +153,7 @@ namespace Content.Server.Atmos.EntitySystems
}
else
{
var affected = tile.Air.RemoveRatio(tile.Hotspot.Volume / tile.Air.Volume);
var affected = tile.Air.RemoveVolume(tile.Hotspot.Volume);
affected.Temperature = tile.Hotspot.Temperature;
React(affected, tile);
tile.Hotspot.Temperature = affected.Temperature;

View File

@@ -176,6 +176,11 @@ namespace Content.Server.Atmos
return removed;
}
public GasMixture RemoveVolume(float vol)
{
return RemoveRatio(vol / Volume);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CopyFromMutable(GasMixture sample)
{

View File

@@ -0,0 +1,20 @@
using Content.Shared.Atmos;
namespace Content.Server.Atmos.Piping.Binary.Components
{
[RegisterComponent]
public sealed class GasRecyclerComponent : Component
{
[ViewVariables(VVAccess.ReadOnly)]
[DataField("reacting")]
public Boolean Reacting { get; set; } = false;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("inlet")]
public string InletName { get; set; } = "inlet";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("outlet")]
public string OutletName { get; set; } = "outlet";
}
}

View File

@@ -0,0 +1,120 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos.Piping;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Audio;
using Content.Shared.Examine;
using JetBrains.Annotations;
namespace Content.Server.Atmos.Piping.Binary.EntitySystems
{
[UsedImplicitly]
public sealed class GasReyclerSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
private const float MinTemp = 300 + Atmospherics.T0C; // 300 C
private const float MinPressure = 30 * Atmospherics.OneAtmosphere; // 3 MPa
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasRecyclerComponent, AtmosDeviceEnabledEvent>(OnEnabled);
SubscribeLocalEvent<GasRecyclerComponent, AtmosDeviceUpdateEvent>(OnUpdate);
SubscribeLocalEvent<GasRecyclerComponent, AtmosDeviceDisabledEvent>(OnDisabled);
SubscribeLocalEvent<GasRecyclerComponent, ExaminedEvent>(OnExamined);
}
private void OnEnabled(EntityUid uid, GasRecyclerComponent comp, AtmosDeviceEnabledEvent args)
{
UpdateAppearance(uid, comp);
}
private void OnExamined(EntityUid uid, GasRecyclerComponent comp, ExaminedEvent args)
{
if (!EntityManager.GetComponent<TransformComponent>(comp.Owner).Anchored || !args.IsInDetailsRange) // Not anchored? Out of range? No status.
return;
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer)
|| !nodeContainer.TryGetNode(comp.InletName, out PipeNode? inlet)
|| !nodeContainer.TryGetNode(comp.OutletName, out PipeNode? outlet))
{
return;
}
if (comp.Reacting)
{
args.PushMarkup(Loc.GetString("gas-recycler-reacting"));
}
else
{
if (inlet.Air.Pressure < MinPressure)
{
args.PushMarkup(Loc.GetString("gas-recycler-low-pressure"));
}
if (inlet.Air.Temperature < MinTemp)
{
args.PushMarkup(Loc.GetString("gas-recycler-low-temperature"));
}
}
}
private void OnUpdate(EntityUid uid, GasRecyclerComponent comp, AtmosDeviceUpdateEvent args)
{
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer)
|| !nodeContainer.TryGetNode(comp.InletName, out PipeNode? inlet)
|| !nodeContainer.TryGetNode(comp.OutletName, out PipeNode? outlet))
{
_ambientSoundSystem.SetAmbience(comp.Owner, false);
return;
}
// The gas recycler is a passive device, so it permits gas flow even if nothing is being reacted.
comp.Reacting = inlet.Air.Temperature >= MinTemp && inlet.Air.Pressure >= MinPressure;
var removed = inlet.Air.RemoveVolume(PassiveTransferVol(inlet.Air, outlet.Air));
if (comp.Reacting)
{
var nCO2 = removed.GetMoles(Gas.CarbonDioxide);
removed.AdjustMoles(Gas.CarbonDioxide, -nCO2);
removed.AdjustMoles(Gas.Oxygen, nCO2);
var nN2O = removed.GetMoles(Gas.NitrousOxide);
removed.AdjustMoles(Gas.NitrousOxide, -nN2O);
removed.AdjustMoles(Gas.Nitrogen, nN2O);
}
_atmosphereSystem.Merge(outlet.Air, removed);
UpdateAppearance(uid, comp);
_ambientSoundSystem.SetAmbience(comp.Owner, true);
}
public float PassiveTransferVol(GasMixture inlet, GasMixture outlet)
{
if (inlet.Pressure < outlet.Pressure)
{
return 0;
}
float overPressConst = 300; // pressure difference (in atm) to get 200 L/sec transfer rate
float alpha = Atmospherics.MaxTransferRate / (float)Math.Sqrt(overPressConst*Atmospherics.OneAtmosphere);
return alpha * (float)Math.Sqrt(inlet.Pressure - outlet.Pressure);
}
private void OnDisabled(EntityUid uid, GasRecyclerComponent comp, AtmosDeviceDisabledEvent args)
{
comp.Reacting = false;
UpdateAppearance(uid, comp);
}
private void UpdateAppearance(EntityUid uid, GasRecyclerComponent? comp = null, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref comp, ref appearance, false))
return;
appearance.SetData(PumpVisuals.Enabled, comp.Reacting);
}
}
}

View File

@@ -82,9 +82,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
return;
// We multiply the transfer rate in L/s by the seconds passed since the last process to get the liters.
var transferRatio = (float)(pump.TransferRate * (_gameTiming.CurTime - device.LastProcess).TotalSeconds) / inlet.Air.Volume;
var removed = inlet.Air.RemoveRatio(transferRatio);
var removed = inlet.Air.RemoveVolume((float)(pump.TransferRate * (_gameTiming.CurTime - device.LastProcess).TotalSeconds));
// Some of the gas from the mixture leaks when overclocked.
if (pump.Overclocked)

View File

@@ -62,15 +62,15 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
}
// We multiply the transfer rate in L/s by the seconds passed since the last process to get the liters.
var transferRatio = (float)(filter.TransferRate * (_gameTiming.CurTime - device.LastProcess).TotalSeconds) / inletNode.Air.Volume;
var transferVol = (float)(filter.TransferRate * (_gameTiming.CurTime - device.LastProcess).TotalSeconds);
if (transferRatio <= 0)
if (transferVol <= 0)
{
_ambientSoundSystem.SetAmbience(filter.Owner, false);
return;
}
var removed = inletNode.Air.RemoveRatio(transferRatio);
var removed = inletNode.Air.RemoveVolume(transferVol);
if (filter.FilteredGas.HasValue)
{

View File

@@ -66,15 +66,15 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
UpdateAppearance(uid, comp);
// We multiply the transfer rate in L/s by the seconds passed since the last process to get the liters.
var transferRatio = (float)(transferRate * (_gameTiming.CurTime - device.LastProcess).TotalSeconds) / inletNode.Air.Volume;
if (transferRatio <= 0)
var transferVolume = (float)(transferRate * (_gameTiming.CurTime - device.LastProcess).TotalSeconds);
if (transferVolume <= 0)
{
_ambientSoundSystem.SetAmbience(comp.Owner, false);
return;
}
_ambientSoundSystem.SetAmbience(comp.Owner, true);
var removed = inletNode.Air.RemoveRatio(transferRatio);
var removed = inletNode.Air.RemoveVolume(transferVolume);
_atmosphereSystem.Merge(outletNode.Air, removed);
}

View File

@@ -109,8 +109,7 @@ namespace Content.Server.Body.Systems
return;
}
var ratio = (Atmospherics.BreathVolume / ev.Gas.Volume);
var actualGas = ev.Gas.RemoveRatio(ratio);
var actualGas = ev.Gas.RemoveVolume(Atmospherics.BreathVolume);
var lungRatio = 1.0f / organs.Count;
var gas = organs.Count == 1 ? actualGas : actualGas.RemoveRatio(lungRatio);

View File

@@ -0,0 +1,3 @@
gas-recycler-reacting = It is [color=green]converting[/color] waste gases.
gas-recycler-low-pressure = The input pressure is [color=darkred]too low[/color].
gas-recycler-low-temperature = The input temperature is [color=darkred]too low[/color].

View File

@@ -270,6 +270,7 @@
unlockedRecipes:
- ThermomachineFreezerMachineCircuitBoard
- PortableScrubberMachineCircuitBoard
- GasRecyclerMachineCircuitboard
# Avionics Circuitry Technology Tree

View File

@@ -541,3 +541,15 @@
materialRequirements:
Glass: 2
Cable: 2
- type: entity
id: GasRecyclerMachineCircuitboard
parent: BaseMachineCircuitboard
name: gas recycler board
description: A printed circuit board for a gas recycler
components:
- type: MachineBoard
prototype: GasRecycler
materialRequirements:
Steel: 10
Plasma: 10

View File

@@ -278,6 +278,7 @@
- WallmountGeneratorAPUElectronics
- WallmountSubstationElectronics
- EmitterCircuitboard
- GasRecyclerMachineCircuitboard
- type: Machine
board: CircuitImprinterMachineCircuitboard
- type: Lathe

View File

@@ -257,3 +257,49 @@
!type:PipeNode
nodeGroupID: Pipe
pipeDirection: South
- type: entity
parent: [ GasBinaryBase, BaseMachine, ConstructibleMachine ]
id: GasRecycler
name: gas recycler
description: Recycles carbon dioxide and nitrous oxide. Heater and compressor not included.
placement:
mode: SnapgridCenter
components:
- type: Sprite
sprite: Structures/Machines/gasrecycler.rsi
netsync: false
layers:
- sprite: Structures/Piping/Atmospherics/pipe.rsi
state: pipeStraight
map: [ "enum.PipeVisualLayers.Pipe" ]
- state: running
- state: unlit
shader: unshaded
- type: GenericVisualizer
visuals:
enum.PumpVisuals.Enabled:
enabled:
True: { state: running }
False: { state: unlit }
- type: Appearance
- type: PipeColorVisuals
- type: GasRecycler
- type: AmbientSound
enabled: false
volume: -9
range: 5
sound:
path: /Audio/Ambience/Objects/gas_pump.ogg
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 100
behaviors:
- !type:ChangeConstructionNodeBehavior
node: machineFrame
- !type:DoActsBehavior
acts: ["Destruction"]
- type: Machine
board: GasRecyclerMachineCircuitboard

View File

@@ -404,3 +404,12 @@
materials:
Steel: 100
Glass: 900
- type: latheRecipe
id: GasRecyclerMachineCircuitboard
icon: Objects/Misc/module.rsi/id_mod.png
result: GasRecyclerMachineCircuitboard
completetime: 4
materials:
Steel: 100
Glass: 900

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

View File

@@ -0,0 +1,30 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Created by Peptide90 for SS14",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "unlit"
},
{
"name": "running",
"delays": [
[
0.3,
0.3,
0.3,
0.3,
0.3,
0.3
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B