diff --git a/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs b/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs
new file mode 100644
index 0000000000..5be7a367f7
--- /dev/null
+++ b/Content.Client/Atmos/UI/GasMixerBoundUserInteface.cs
@@ -0,0 +1,91 @@
+using System;
+using Content.Client.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Piping.Binary.Components;
+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 GasMixerBoundUserInterface : BoundUserInterface
+ {
+
+ private GasMixerWindow? _window;
+ private const float MaxPressure = Atmospherics.MaxOutputPressure;
+
+ public GasMixerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new GasMixerWindow();
+
+ if(State != null)
+ UpdateState(State);
+
+ _window.OpenCentered();
+
+ _window.OnClose += Close;
+
+ _window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed;
+ _window.MixerOutputPressureChanged += OnMixerOutputPressurePressed;
+ _window.MixerNodePercentageChanged += OnMixerSetPercentagePressed;
+ }
+
+ private void OnToggleStatusButtonPressed()
+ {
+ if (_window is null) return;
+ SendMessage(new GasMixerToggleStatusMessage(_window.MixerStatus));
+ }
+
+ private void OnMixerOutputPressurePressed(string value)
+ {
+ float pressure = float.TryParse(value, out var parsed) ? parsed : 0f;
+ if (pressure > MaxPressure) pressure = MaxPressure;
+
+ SendMessage(new GasMixerChangeOutputPressureMessage(pressure));
+ }
+
+ private void OnMixerSetPercentagePressed(string value)
+ {
+ // We don't need to send both nodes because it's just 1.0f - node
+ float node = float.TryParse(value, out var parsed) ? parsed : 1.0f;
+
+ if(_window is not null) node = _window.NodeOneLastEdited ? node : 1.0f - node;
+
+ SendMessage(new GasMixerChangeNodePercentageMessage(node));
+ }
+
+ ///
+ /// Update the UI state based on server-sent info
+ ///
+ ///
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ if (_window == null || state is not GasMixerBoundUserInterfaceState cast)
+ return;
+
+ _window.Title = (cast.MixerLabel);
+ _window.SetMixerStatus(cast.Enabled);
+ _window.SetOutputPressure(cast.OutputPressure);
+ _window.SetNodePercentages(cast.NodeOne);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing) return;
+ _window?.Dispose();
+ }
+ }
+}
diff --git a/Content.Client/Atmos/UI/GasMixerWindow.xaml b/Content.Client/Atmos/UI/GasMixerWindow.xaml
new file mode 100644
index 0000000000..9d69d9bf4c
--- /dev/null
+++ b/Content.Client/Atmos/UI/GasMixerWindow.xaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Atmos/UI/GasMixerWindow.xaml.cs b/Content.Client/Atmos/UI/GasMixerWindow.xaml.cs
new file mode 100644
index 0000000000..af4408bd9d
--- /dev/null
+++ b/Content.Client/Atmos/UI/GasMixerWindow.xaml.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Content.Client.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Prototypes;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
+
+namespace Content.Client.Atmos.UI
+{
+ ///
+ /// Client-side UI used to control a gas mixer.
+ ///
+ [GenerateTypedNameReferences]
+ public partial class GasMixerWindow : SS14Window
+ {
+ public bool MixerStatus = true;
+
+ public event Action? ToggleStatusButtonPressed;
+ public event Action? MixerOutputPressureChanged;
+ public event Action? MixerNodePercentageChanged;
+
+ public bool NodeOneLastEdited = true;
+
+ public GasMixerWindow()
+ {
+ RobustXamlLoader.Load(this);
+
+ ToggleStatusButton.OnPressed += _ => SetMixerStatus(!MixerStatus);
+ ToggleStatusButton.OnPressed += _ => ToggleStatusButtonPressed?.Invoke();
+
+ MixerPressureOutputInput.OnTextChanged += _ => SetOutputPressureButton.Disabled = false;
+ SetOutputPressureButton.OnPressed += _ =>
+ {
+ MixerOutputPressureChanged?.Invoke(MixerPressureOutputInput.Text ??= "");
+ SetOutputPressureButton.Disabled = true;
+ };
+
+ SetMaxPressureButton.OnPressed += _ =>
+ {
+ MixerPressureOutputInput.Text = Atmospherics.MaxOutputPressure.ToString(CultureInfo.InvariantCulture);
+ SetOutputPressureButton.Disabled = false;
+ };
+
+ MixerNodeOneInput.OnTextChanged += _ =>
+ {
+ SetMixerPercentageButton.Disabled = false;
+ NodeOneLastEdited = true;
+ };
+ MixerNodeTwoInput.OnTextChanged += _ =>
+ {
+ SetMixerPercentageButton.Disabled = false;
+ NodeOneLastEdited = false;
+ };
+
+ SetMixerPercentageButton.OnPressed += _ =>
+ {
+ MixerNodePercentageChanged?.Invoke(NodeOneLastEdited ? MixerNodeOneInput.Text ??= "" : MixerNodeTwoInput.Text ??= "");
+ SetMixerPercentageButton.Disabled = true;
+ };
+ }
+
+ public void SetOutputPressure(float pressure)
+ {
+ MixerPressureOutputInput.Text = pressure.ToString(CultureInfo.InvariantCulture);
+ }
+
+ public void SetNodePercentages(float nodeOne)
+ {
+ nodeOne *= 100.0f;
+ MixerNodeOneInput.Text = nodeOne.ToString(CultureInfo.InvariantCulture);
+
+ float nodeTwo = 100.0f - nodeOne;
+ MixerNodeTwoInput.Text = nodeTwo.ToString(CultureInfo.InvariantCulture);
+ }
+
+ public void SetMixerStatus(bool enabled)
+ {
+ MixerStatus = enabled;
+ if (enabled)
+ {
+ ToggleStatusButton.Text = Loc.GetString("comp-gas-mixer-ui-status-enabled");
+ }
+ else
+ {
+ ToggleStatusButton.Text = Loc.GetString("comp-gas-mixer-ui-status-disabled");
+ }
+ }
+ }
+}
diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs
index fdb0b99f21..ccfead7f73 100644
--- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs
+++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs
@@ -8,6 +8,7 @@ using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
+using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
@@ -94,8 +95,15 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
if (!args.User.TryGetComponent(out ActorComponent? actor))
return;
- _userInterfaceSystem.TryOpen(uid, GasPressurePumpUiKey.Key, actor.PlayerSession);
- DirtyUI(uid, component);
+ if (component.Owner.Transform.Anchored)
+ {
+ _userInterfaceSystem.TryOpen(uid, GasPressurePumpUiKey.Key, actor.PlayerSession);
+ DirtyUI(uid, component);
+ }
+ else
+ {
+ args.User.PopupMessageCursor(Loc.GetString("comp-gas-pump-ui-needs-anchor"));
+ }
args.Handled = true;
}
diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs
index a10ba64dc9..527f11e676 100644
--- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs
+++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs
@@ -8,6 +8,7 @@ using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
+using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
@@ -99,8 +100,15 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
if (!args.User.TryGetComponent(out ActorComponent? actor))
return;
- _userInterfaceSystem.TryOpen(uid, GasVolumePumpUiKey.Key, actor.PlayerSession);
- DirtyUI(uid, component);
+ if (component.Owner.Transform.Anchored)
+ {
+ _userInterfaceSystem.TryOpen(uid, GasVolumePumpUiKey.Key, actor.PlayerSession);
+ DirtyUI(uid, component);
+ }
+ else
+ {
+ args.User.PopupMessageCursor(Loc.GetString("comp-gas-pump-ui-needs-anchor"));
+ }
args.Handled = true;
}
diff --git a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
index 9fa586babc..cc4032059a 100644
--- a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
+++ b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
@@ -8,10 +8,12 @@ using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Trinary.Components;
using Content.Shared.Interaction;
+using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
+using Robust.Shared.Localization;
using Robust.Shared.Timing;
namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
@@ -82,8 +84,15 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
if (!args.User.TryGetComponent(out ActorComponent? actor))
return;
- _userInterfaceSystem.TryOpen(uid, GasFilterUiKey.Key, actor.PlayerSession);
- DirtyUI(uid, component);
+ if (component.Owner.Transform.Anchored)
+ {
+ _userInterfaceSystem.TryOpen(uid, GasFilterUiKey.Key, actor.PlayerSession);
+ DirtyUI(uid, component);
+ }
+ else
+ {
+ args.User.PopupMessageCursor(Loc.GetString("comp-gas-filter-ui-needs-anchor"));
+ }
args.Handled = true;
}
diff --git a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs
index 28db52ae59..bcb82771b2 100644
--- a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs
+++ b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs
@@ -4,19 +4,31 @@ using Content.Server.Atmos.Piping.Trinary.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
+using Content.Shared.Atmos.Piping.Trinary.Components;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
using JetBrains.Annotations;
+using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
{
[UsedImplicitly]
public class GasMixerSystem : EntitySystem
{
+ [Dependency] private UserInterfaceSystem _userInterfaceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnMixerUpdated);
+ SubscribeLocalEvent(OnMixerInteractHand);
+ // Bound UI subscriptions
+ SubscribeLocalEvent(OnOutputPressureChangeMessage);
+ SubscribeLocalEvent(OnChangeNodePercentageMessage);
+ SubscribeLocalEvent(OnToggleStatusMessage);
}
private void OnMixerUpdated(EntityUid uid, GasMixerComponent mixer, AtmosDeviceUpdateEvent args)
@@ -91,5 +103,53 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
outlet.AssumeAir(removed);
}
}
+
+ private void OnMixerInteractHand(EntityUid uid, GasMixerComponent component, InteractHandEvent args)
+ {
+ if (!args.User.TryGetComponent(out ActorComponent? actor))
+ return;
+
+ if (component.Owner.Transform.Anchored)
+ {
+ _userInterfaceSystem.TryOpen(uid, GasMixerUiKey.Key, actor.PlayerSession);
+ DirtyUI(uid, component);
+ }
+ else
+ {
+ args.User.PopupMessageCursor(Loc.GetString("comp-gas-mixer-ui-needs-anchor"));
+ }
+
+ args.Handled = true;
+ }
+
+ private void DirtyUI(EntityUid uid, GasMixerComponent? mixer)
+ {
+ if (!Resolve(uid, ref mixer))
+ return;
+
+ _userInterfaceSystem.TrySetUiState(uid, GasMixerUiKey.Key,
+ new GasMixerBoundUserInterfaceState(mixer.Owner.Name, mixer.TargetPressure, mixer.Enabled, mixer.InletOneConcentration));
+ }
+
+ private void OnToggleStatusMessage(EntityUid uid, GasMixerComponent mixer, GasMixerToggleStatusMessage args)
+ {
+ mixer.Enabled = args.Enabled;
+ DirtyUI(uid, mixer);
+ }
+
+ private void OnOutputPressureChangeMessage(EntityUid uid, GasMixerComponent mixer, GasMixerChangeOutputPressureMessage args)
+ {
+ mixer.TargetPressure = Math.Clamp(args.Pressure, 0f, Atmospherics.MaxOutputPressure);
+ DirtyUI(uid, mixer);
+ }
+
+ private void OnChangeNodePercentageMessage(EntityUid uid, GasMixerComponent mixer,
+ GasMixerChangeNodePercentageMessage args)
+ {
+ float nodeOne = Math.Clamp(args.NodeOne, 0f, 100.0f) / 100.0f;
+ mixer.InletOneConcentration = nodeOne;
+ mixer.InletTwoConcentration = 1.0f - mixer.InletOneConcentration;
+ DirtyUI(uid, mixer);
+ }
}
}
diff --git a/Content.Shared/Atmos/Piping/Trinary/Components/SharedGasMixerComponent.cs b/Content.Shared/Atmos/Piping/Trinary/Components/SharedGasMixerComponent.cs
new file mode 100644
index 0000000000..ed6ceebc71
--- /dev/null
+++ b/Content.Shared/Atmos/Piping/Trinary/Components/SharedGasMixerComponent.cs
@@ -0,0 +1,63 @@
+using System;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Atmos.Piping.Trinary.Components
+{
+ [Serializable, NetSerializable]
+ public enum GasMixerUiKey
+ {
+ Key,
+ }
+
+ [Serializable, NetSerializable]
+ public class GasMixerBoundUserInterfaceState : BoundUserInterfaceState
+ {
+ public string MixerLabel { get; }
+ public float OutputPressure { get; }
+ public bool Enabled { get; }
+
+ public float NodeOne { get; }
+
+ public GasMixerBoundUserInterfaceState(string mixerLabel, float outputPressure, bool enabled, float nodeOne)
+ {
+ MixerLabel = mixerLabel;
+ OutputPressure = outputPressure;
+ Enabled = enabled;
+ NodeOne = nodeOne;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public class GasMixerToggleStatusMessage : BoundUserInterfaceMessage
+ {
+ public bool Enabled { get; }
+
+ public GasMixerToggleStatusMessage(bool enabled)
+ {
+ Enabled = enabled;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public class GasMixerChangeOutputPressureMessage : BoundUserInterfaceMessage
+ {
+ public float Pressure { get; }
+
+ public GasMixerChangeOutputPressureMessage(float pressure)
+ {
+ Pressure = pressure;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public class GasMixerChangeNodePercentageMessage : BoundUserInterfaceMessage
+ {
+ public float NodeOne { get; }
+
+ public GasMixerChangeNodePercentageMessage(float nodeOne)
+ {
+ NodeOne = nodeOne;
+ }
+ }
+}
diff --git a/Resources/Locale/en-US/components/gas-filter-component.ftl b/Resources/Locale/en-US/components/gas-filter-component.ftl
index 827ddbd03f..20edf6938f 100644
--- a/Resources/Locale/en-US/components/gas-filter-component.ftl
+++ b/Resources/Locale/en-US/components/gas-filter-component.ftl
@@ -8,3 +8,5 @@ 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
+
+comp-gas-filter-ui-needs-anchor = Anchor it first!
diff --git a/Resources/Locale/en-US/components/gas-mixer-component.ftl b/Resources/Locale/en-US/components/gas-mixer-component.ftl
new file mode 100644
index 0000000000..75c739746e
--- /dev/null
+++ b/Resources/Locale/en-US/components/gas-mixer-component.ftl
@@ -0,0 +1,13 @@
+comp-gas-mixer-ui-mixer-status = Status:
+comp-gas-mixer-ui-status-enabled = On
+comp-gas-mixer-ui-status-disabled = Off
+
+comp-gas-mixer-ui-mixer-output-pressure = Output Pressure (kPa):
+
+comp-gas-mixer-ui-mixer-node-primary = Primary Port:
+comp-gas-mixer-ui-mixer-node-side = Side Port:
+
+comp-gas-mixer-ui-mixer-set = Set
+comp-gas-mixer-ui-mixer-max = Max
+
+comp-gas-mixer-ui-needs-anchor = Anchor it first!
diff --git a/Resources/Locale/en-US/components/gas-pump-component.ftl b/Resources/Locale/en-US/components/gas-pump-component.ftl
index f1fb95f812..ad03ac6be6 100644
--- a/Resources/Locale/en-US/components/gas-pump-component.ftl
+++ b/Resources/Locale/en-US/components/gas-pump-component.ftl
@@ -8,3 +8,5 @@ comp-gas-pump-ui-pump-set-max = Max
comp-gas-pump-ui-pump-output-pressure = Output Pressure (kPa):
comp-gas-pump-ui-pump-transfer-rate = Transfer Rate (L/s):
+
+comp-gas-pump-ui-needs-anchor = Anchor it first!
diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml
index 68737b536f..b50e80821c 100644
--- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml
+++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/trinary.yml
@@ -76,6 +76,10 @@
- type: SubFloorShowLayerVisualizer
- type: PipeConnectorVisualizer
- type: PipeColorVisualizer
+ - type: UserInterface
+ interfaces:
+ - key: enum.GasMixerUiKey.Key
+ type: GasMixerBoundUserInterface
- type: GasMixer
inletOne: inlet
inletTwo: filter