diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs
index 3570c3b374..a618a37aa6 100644
--- a/Content.Client/IgnoredComponents.cs
+++ b/Content.Client/IgnoredComponents.cs
@@ -235,7 +235,7 @@ namespace Content.Client
"SliceableFood",
"DamageOtherOnHit",
"DamageOnLand",
- "Recyclable"
+ "GasFilter"
};
}
}
diff --git a/Content.Server/GameObjects/Components/Atmos/Piping/GasFilterComponent.cs b/Content.Server/GameObjects/Components/Atmos/Piping/GasFilterComponent.cs
new file mode 100644
index 0000000000..70bbc7bbb6
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Atmos/Piping/GasFilterComponent.cs
@@ -0,0 +1,179 @@
+#nullable enable
+using Content.Server.Atmos;
+using Content.Server.GameObjects.Components.NodeContainer;
+using Content.Server.GameObjects.Components.NodeContainer.Nodes;
+using Content.Shared.Atmos;
+using Content.Shared.GameObjects.Components.Atmos;
+using Robust.Server.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.ComponentDependencies;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Log;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+using System;
+using System.Linq;
+
+namespace Content.Server.GameObjects.Components.Atmos.Piping.Filters
+{
+ [RegisterComponent]
+ public class GasFilterComponent : Component
+ {
+ public override string Name => "GasFilter";
+
+ ///
+ /// If the filter is currently filtering.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool FilterEnabled
+ {
+ get => _filterEnabled;
+ set
+ {
+ _filterEnabled = value;
+ UpdateAppearance();
+ }
+ }
+ private bool _filterEnabled;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public Gas GasToFilter
+ {
+ get => _gasToFilter;
+ set
+ {
+ _gasToFilter = value;
+ UpdateAppearance();
+ }
+ }
+ private Gas _gasToFilter;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int VolumeFilterRate
+ {
+ get => _volumeFilterRate;
+ set => _volumeFilterRate = Math.Clamp(value, 0, MaxVolumeFilterRate);
+ }
+ private int _volumeFilterRate;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int MaxVolumeFilterRate
+ {
+ get => _maxVolumeFilterRate;
+ set => Math.Max(value, 0);
+ }
+ private int _maxVolumeFilterRate;
+
+ [ViewVariables]
+ private PipeDirection _initialInletDirection;
+
+ ///
+ /// The direction the filtered-out gas goes.
+ ///
+ [ViewVariables]
+ private PipeDirection _initialFilterOutletDirection;
+
+ ///
+ /// The direction the rest of the gas goes.
+ ///
+ [ViewVariables]
+ private PipeDirection _initialOutletDirection;
+
+ [ViewVariables]
+ private PipeNode? _inletPipe;
+
+ [ViewVariables]
+ private PipeNode? _filterOutletPipe;
+
+ [ViewVariables]
+ private PipeNode? _outletPipe;
+
+ [ComponentDependency]
+ private readonly AppearanceComponent? _appearance = default;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+ serializer.DataField(ref _volumeFilterRate, "startingVolumePumpRate", 0);
+ serializer.DataField(ref _maxVolumeFilterRate, "maxVolumePumpRate", 100);
+ serializer.DataField(ref _gasToFilter, "gasToFilter", Gas.Phoron);
+ serializer.DataField(ref _initialInletDirection, "inletDirection", PipeDirection.None);
+ serializer.DataField(ref _initialFilterOutletDirection, "filterOutletDirection", PipeDirection.None);
+ serializer.DataField(ref _initialOutletDirection, "outletDirection", PipeDirection.None);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ Owner.EnsureComponent();
+ SetPipes();
+ UpdateAppearance();
+ }
+
+ public override void HandleMessage(ComponentMessage message, IComponent? component)
+ {
+ base.HandleMessage(message, component);
+ switch (message)
+ {
+ case PipeNetUpdateMessage:
+ Update();
+ break;
+ }
+ }
+
+ public void Update()
+ {
+ if (!FilterEnabled)
+ return;
+
+ if (_inletPipe == null || _inletPipe.Air == null ||
+ _filterOutletPipe == null || _filterOutletPipe.Air == null ||
+ _outletPipe == null || _outletPipe.Air == null)
+ return;
+
+ FilterGas(_inletPipe.Air, _filterOutletPipe.Air, _outletPipe.Air);
+ }
+
+ private void FilterGas(GasMixture inletGas, GasMixture filterOutletGas, GasMixture outletGas)
+ {
+ var volumeRatio = Math.Clamp(VolumeFilterRate / inletGas.Volume, 0, 1);
+ var gas = inletGas.RemoveRatio(volumeRatio);
+
+ var molesToSeperate = gas.GetMoles(GasToFilter);
+ gas.SetMoles(GasToFilter, 0);
+ filterOutletGas.AdjustMoles(GasToFilter, molesToSeperate);
+
+ outletGas.Merge(gas);
+ }
+
+ private void UpdateAppearance()
+ {
+ _appearance?.SetData(FilterVisuals.VisualState, new FilterVisualState());
+ }
+
+ private void SetPipes()
+ {
+ _inletPipe = null;
+ _filterOutletPipe = null;
+ _outletPipe = null;
+
+ if (!Owner.TryGetComponent(out var container))
+ {
+ Logger.Error($"{typeof(GasFilterComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} did not have a {nameof(NodeContainerComponent)}.");
+ return;
+ }
+
+ var pipeNodes = container.Nodes.OfType();
+
+ _inletPipe = pipeNodes.Where(pipe => pipe.PipeDirection == _initialInletDirection).FirstOrDefault();
+ _filterOutletPipe = pipeNodes.Where(pipe => pipe.PipeDirection == _initialFilterOutletDirection).FirstOrDefault();
+ _outletPipe = pipeNodes.Where(pipe => pipe.PipeDirection == _initialOutletDirection).FirstOrDefault();
+
+ if (_inletPipe == null || _filterOutletPipe == null || _outletPipe == null)
+ {
+ Logger.Error($"{typeof(GasFilterComponent)} on {Owner?.Prototype?.ID}, Uid {Owner?.Uid} could not find compatible {nameof(PipeNode)}s on its {nameof(NodeContainerComponent)}.");
+ return;
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Atmos/Atmospherics.cs b/Content.Shared/Atmos/Atmospherics.cs
index 76a7a45056..d87ac132cd 100644
--- a/Content.Shared/Atmos/Atmospherics.cs
+++ b/Content.Shared/Atmos/Atmospherics.cs
@@ -1,4 +1,6 @@
-using Robust.Shared.Maths;
+using Robust.Shared.Maths;
+using Robust.Shared.Serialization;
+using System;
namespace Content.Shared.Atmos
{
@@ -246,6 +248,7 @@ namespace Content.Shared.Atmos
///
/// Gases to Ids. Keep these updated with the prototypes!
///
+ [Serializable, NetSerializable]
public enum Gas : sbyte
{
Oxygen = 0,
diff --git a/Content.Shared/GameObjects/Components/Atmos/SharedFilterComponent.cs b/Content.Shared/GameObjects/Components/Atmos/SharedFilterComponent.cs
new file mode 100644
index 0000000000..8308d30cbb
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/Atmos/SharedFilterComponent.cs
@@ -0,0 +1,17 @@
+using System;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.GameObjects.Components.Atmos
+{
+ [Serializable, NetSerializable]
+ public enum FilterVisuals
+ {
+ VisualState
+ }
+
+ [Serializable, NetSerializable]
+ public class FilterVisualState
+ {
+
+ }
+}
diff --git a/Resources/Prototypes/Entities/Constructible/Ground/gasfilters.yml b/Resources/Prototypes/Entities/Constructible/Ground/gasfilters.yml
new file mode 100644
index 0000000000..fc84458bdc
--- /dev/null
+++ b/Resources/Prototypes/Entities/Constructible/Ground/gasfilters.yml
@@ -0,0 +1,50 @@
+- type: entity
+ abstract: true
+ id: GasFilterBase
+ placement:
+ mode: SnapgridCenter
+ components:
+ - type: Clickable
+ - type: InteractionOutline
+ - type: Physics
+ - type: SnapGrid
+ offset: Center
+ - type: Sprite
+ sprite: Constructible/Atmos/pipeitems.rsi
+ state: scrubber
+ - type: Damageable
+ resistances: metallicResistances
+ - type: Destructible
+ thresholds:
+ 100:
+ behaviors:
+ - !type:DoActsBehavior
+ acts: ["Destruction"]
+
+- type: entity
+ parent: GasFilterBase
+ id: GasFilter
+ name: Gas Filter
+ description: It filters gases.
+ components:
+ - type: Sprite
+ sprite: Constructible/Atmos/pipe.rsi
+ state: pipeTJunction2
+ - type: Icon
+ sprite: Constructible/Atmos/pipe.rsi
+ state: pipeTJunction2
+ - type: NodeContainer
+ nodes:
+ - !type:PipeNode
+ nodeGroupID: Pipe
+ pipeDirection: South
+ - !type:PipeNode
+ nodeGroupID: Pipe
+ pipeDirection: East
+ - !type:PipeNode
+ nodeGroupID: Pipe
+ pipeDirection: North
+ - type: GasFilter
+ inletDirection: South
+ filterOutletDirection: East
+ outletDirection: North