diff --git a/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs b/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs
new file mode 100644
index 0000000000..271748e9d9
--- /dev/null
+++ b/Content.Client/Atmos/UI/GasFilterBoundUserInterface.cs
@@ -0,0 +1,98 @@
+using System;
+using Content.Client.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Piping.Trinary.Components;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using Robust.Shared.GameObjects;
+
+namespace Content.Client.Atmos.UI
+{
+ ///
+ /// Initializes a and updates it when new server messages are received.
+ ///
+ [UsedImplicitly]
+ public class GasFilterBoundUserInterface : BoundUserInterface
+ {
+
+ private GasFilterWindow? _window;
+ private const float MaxTransferRate = Atmospherics.MaxTransferRate;
+
+ public GasFilterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ var atmosSystem = EntitySystem.Get();
+
+ _window = new GasFilterWindow(atmosSystem.Gases);
+
+ if(State != null)
+ UpdateState(State);
+
+ _window.OpenCentered();
+
+ _window.OnClose += Close;
+
+ _window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed;
+ _window.FilterTransferRateChanged += OnFilterTransferRatePressed;
+ _window.SelectGasPressed += OnSelectGasPressed;
+ }
+
+ private void OnToggleStatusButtonPressed()
+ {
+ if (_window is null) return;
+ SendMessage(new GasFilterToggleStatusMessage(_window.FilterStatus));
+ }
+
+ private void OnFilterTransferRatePressed(string value)
+ {
+ float rate = float.TryParse(value, out var parsed) ? parsed : 0f;
+ if (rate > MaxTransferRate) rate = MaxTransferRate;
+
+ SendMessage(new GasFilterChangeRateMessage(rate));
+ }
+
+ private void OnSelectGasPressed()
+ {
+ if (_window is null || _window.SelectedGas is null) return;
+ if (!Int32.TryParse(_window.SelectedGas, out var gas)) return;
+ SendMessage(new GasFilterSelectGasMessage(gas));
+ }
+
+ ///
+ /// Update the UI state based on server-sent info
+ ///
+ ///
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ if (_window == null || state is not GasFilterBoundUserInterfaceState cast)
+ return;
+
+ _window.Title = (cast.FilterLabel);
+ _window.SetFilterStatus(cast.Enabled);
+ _window.SetTransferRate(cast.TransferRate);
+ if (cast.FilteredGas is not null)
+ {
+ var atmos = EntitySystem.Get();
+ var gas = atmos.GetGas((Gas) cast.FilteredGas);
+ _window.SetGasFiltered(gas.ID, gas.Name);
+ }
+ else
+ {
+ _window.SetGasFiltered(null, "None");
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing) return;
+ _window?.Dispose();
+ }
+ }
+}
diff --git a/Content.Client/Atmos/UI/GasFilterWindow.xaml b/Content.Client/Atmos/UI/GasFilterWindow.xaml
new file mode 100644
index 0000000000..b6ea716fb4
--- /dev/null
+++ b/Content.Client/Atmos/UI/GasFilterWindow.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Atmos/UI/GasFilterWindow.xaml.cs b/Content.Client/Atmos/UI/GasFilterWindow.xaml.cs
new file mode 100644
index 0000000000..c1ad89d744
--- /dev/null
+++ b/Content.Client/Atmos/UI/GasFilterWindow.xaml.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Content.Shared.Atmos.Prototypes;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Localization;
+
+namespace Content.Client.Atmos.UI
+{
+ ///
+ /// Client-side UI used to control a gas filter.
+ ///
+ [GenerateTypedNameReferences]
+ public partial class GasFilterWindow : SS14Window
+ {
+ private readonly ButtonGroup _buttonGroup = new();
+
+ public bool FilterStatus = true;
+ public string? SelectedGas;
+ public string? CurrentGasId;
+
+ public event Action? ToggleStatusButtonPressed;
+ public event Action? FilterTransferRateChanged;
+ public event Action? SelectGasPressed;
+
+ public GasFilterWindow(IEnumerable gases)
+ {
+ RobustXamlLoader.Load(this);
+ PopulateGasList(gases);
+
+ ToggleStatusButton.OnPressed += _ => SetFilterStatus(!FilterStatus);
+ ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
+
+ FilterTransferRateInput.OnTextChanged += _ => SetFilterRate.Disabled = false;
+ SetFilterRate.OnPressed += _ =>
+ {
+ FilterTransferRateChanged?.Invoke(FilterTransferRateInput.Text ??= "");
+ SetFilterRate.Disabled = true;
+ };
+
+ SelectGasButton.OnPressed += _ => SelectGasPressed?.Invoke();
+
+ GasList.OnItemSelected += GasListOnItemSelected;
+ GasList.OnItemDeselected += GasListOnItemDeselected;
+ }
+
+ public void SetTransferRate(float rate)
+ {
+ FilterTransferRateInput.Text = rate.ToString(CultureInfo.InvariantCulture);
+ }
+
+ public void SetFilterStatus(bool enabled)
+ {
+ FilterStatus = enabled;
+ if (enabled)
+ {
+ ToggleStatusButton.Text = Loc.GetString("comp-gas-filter-ui-status-enabled");
+ }
+ else
+ {
+ ToggleStatusButton.Text = Loc.GetString("comp-gas-filter-ui-status-disabled");
+ }
+ }
+
+ public void SetGasFiltered(string? id, string name)
+ {
+ CurrentGasId = id;
+ CurrentGasLabel.Text = Loc.GetString("comp-gas-filter-ui-filter-gas-current") + $" {name}";
+ GasList.ClearSelected();
+ SelectGasButton.Disabled = true;
+ }
+
+ private void PopulateGasList(IEnumerable gases)
+ {
+ foreach (GasPrototype gas in gases)
+ {
+ GasList.Add(GetGasItem(gas.ID, gas.Name, GasList));
+ }
+ }
+
+ private static ItemList.Item GetGasItem(string id, string name, ItemList itemList)
+ {
+ return new(itemList)
+ {
+ Metadata = id,
+ Text = name
+ };
+ }
+
+ private void GasListOnItemSelected(ItemList.ItemListSelectedEventArgs obj)
+ {
+ SelectedGas = (string) obj.ItemList[obj.ItemIndex].Metadata!;
+ if(SelectedGas != CurrentGasId) SelectGasButton.Disabled = false;
+ }
+
+ private void GasListOnItemDeselected(ItemList.ItemListDeselectedEventArgs obj)
+ {
+ SelectedGas = CurrentGasId;
+ SelectGasButton.Disabled = true;
+ }
+ }
+}
diff --git a/Content.Server/Atmos/Piping/Trinary/Components/GasFilterComponent.cs b/Content.Server/Atmos/Piping/Trinary/Components/GasFilterComponent.cs
index 4c547215f4..4e0f2fd80e 100644
--- a/Content.Server/Atmos/Piping/Trinary/Components/GasFilterComponent.cs
+++ b/Content.Server/Atmos/Piping/Trinary/Components/GasFilterComponent.cs
@@ -1,3 +1,5 @@
+using System;
+using Content.Server.Atmos.Piping.Trinary.EntitySystems;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
diff --git a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
index 219f21bddf..9fa586babc 100644
--- a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
+++ b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
@@ -1,9 +1,13 @@
+using System;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Trinary.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
+using Content.Server.UserInterface;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping;
+using Content.Shared.Atmos.Piping.Trinary.Components;
+using Content.Shared.Interaction;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
@@ -16,12 +20,18 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
public class GasFilterSystem : EntitySystem
{
[Dependency] private IGameTiming _gameTiming = default!;
+ [Dependency] private UserInterfaceSystem _userInterfaceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnFilterUpdated);
+ SubscribeLocalEvent(OnFilterInteractHand);
+ // Bound UI subscriptions
+ SubscribeLocalEvent(OnTransferRateChangeMessage);
+ SubscribeLocalEvent(OnSelectGasMessage);
+ SubscribeLocalEvent(OnToggleStatusMessage);
}
private void OnFilterUpdated(EntityUid uid, GasFilterComponent filter, AtmosDeviceUpdateEvent args)
@@ -66,5 +76,49 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
outletNode.AssumeAir(removed);
}
+
+ private void OnFilterInteractHand(EntityUid uid, GasFilterComponent component, InteractHandEvent args)
+ {
+ if (!args.User.TryGetComponent(out ActorComponent? actor))
+ return;
+
+ _userInterfaceSystem.TryOpen(uid, GasFilterUiKey.Key, actor.PlayerSession);
+ DirtyUI(uid, component);
+
+ args.Handled = true;
+ }
+
+ private void DirtyUI(EntityUid uid, GasFilterComponent? filter)
+ {
+
+ if (!Resolve(uid, ref filter))
+ return;
+
+ _userInterfaceSystem.TrySetUiState(uid, GasFilterUiKey.Key,
+ new GasFilterBoundUserInterfaceState(filter.Owner.Name, filter.TransferRate, filter.Enabled, filter.FilteredGas));
+ }
+
+ private void OnToggleStatusMessage(EntityUid uid, GasFilterComponent filter, GasFilterToggleStatusMessage args)
+ {
+ filter.Enabled = args.Enabled;
+ DirtyUI(uid, filter);
+ }
+
+ private void OnTransferRateChangeMessage(EntityUid uid, GasFilterComponent filter, GasFilterChangeRateMessage args)
+ {
+ filter.TransferRate = Math.Clamp(args.Rate, 0f, Atmospherics.MaxTransferRate);
+ DirtyUI(uid, filter);
+
+ }
+
+ private void OnSelectGasMessage(EntityUid uid, GasFilterComponent filter, GasFilterSelectGasMessage args)
+ {
+ if (Enum.TryParse(args.ID.ToString(), true, out var parsedGas))
+ {
+ filter.FilteredGas = parsedGas;
+ DirtyUI(uid, filter);
+ }
+
+ }
}
}
diff --git a/Content.Shared/Atmos/Piping/Trinary/Components/SharedGasFilterComponent.cs b/Content.Shared/Atmos/Piping/Trinary/Components/SharedGasFilterComponent.cs
new file mode 100644
index 0000000000..a9ce0bbdf7
--- /dev/null
+++ b/Content.Shared/Atmos/Piping/Trinary/Components/SharedGasFilterComponent.cs
@@ -0,0 +1,62 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Atmos.Piping.Trinary.Components
+{
+ [Serializable, NetSerializable]
+ public enum GasFilterUiKey
+ {
+ Key,
+ }
+
+ [Serializable, NetSerializable]
+ public class GasFilterBoundUserInterfaceState : BoundUserInterfaceState
+ {
+ public string FilterLabel { get; }
+ public float TransferRate { get; }
+ public bool Enabled { get; }
+ public Gas? FilteredGas { get; }
+
+ public GasFilterBoundUserInterfaceState(string filterLabel, float transferRate, bool enabled, Gas? filteredGas)
+ {
+ FilterLabel = filterLabel;
+ TransferRate = transferRate;
+ Enabled = enabled;
+ FilteredGas = filteredGas;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public class GasFilterToggleStatusMessage : BoundUserInterfaceMessage
+ {
+ public bool Enabled { get; }
+
+ public GasFilterToggleStatusMessage(bool enabled)
+ {
+ Enabled = enabled;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public class GasFilterChangeRateMessage : BoundUserInterfaceMessage
+ {
+ public float Rate { get; }
+
+ public GasFilterChangeRateMessage(float rate)
+ {
+ Rate = rate;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public class GasFilterSelectGasMessage : BoundUserInterfaceMessage
+ {
+ public int ID { get; }
+
+ public GasFilterSelectGasMessage(int id)
+ {
+ ID = id;
+ }
+ }
+}
diff --git a/Resources/Locale/en-US/components/gas-filter-component.ftl b/Resources/Locale/en-US/components/gas-filter-component.ftl
new file mode 100644
index 0000000000..827ddbd03f
--- /dev/null
+++ b/Resources/Locale/en-US/components/gas-filter-component.ftl
@@ -0,0 +1,10 @@
+comp-gas-filter-ui-filter-status = Status:
+comp-gas-filter-ui-status-enabled = On
+comp-gas-filter-ui-status-disabled = Off
+
+comp-gas-filter-ui-filter-transfer-rate = Transfer Rate (L/s):
+comp-gas-filter-ui-filter-set-rate = Set
+
+comp-gas-filter-ui-filter-gas-current = Currently Filtering:
+comp-gas-filter-ui-filter-gas-select = Select a gas to filter out:
+comp-gas-filter-ui-filter-gas-confirm = Set Gas
diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml
index 1858f0e8ea..68737b536f 100644
--- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml
+++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml
@@ -47,6 +47,10 @@
- type: GasFilterVisualizer
disabledState: gasFilter
enabledState: gasFilterOn
+ - type: UserInterface
+ interfaces:
+ - key: enum.GasFilterUiKey.Key
+ type: GasFilterBoundUserInterface
- type: GasFilter
- type: entity