diff --git a/Content.Client/Atmos/Components/GasAnalyzerComponent.cs b/Content.Client/Atmos/Components/GasAnalyzerComponent.cs
index e16cca3dcf..8f86235358 100644
--- a/Content.Client/Atmos/Components/GasAnalyzerComponent.cs
+++ b/Content.Client/Atmos/Components/GasAnalyzerComponent.cs
@@ -1,77 +1,9 @@
-using Content.Client.Items.Components;
-using Content.Client.Message;
-using Content.Client.Stylesheets;
using Content.Shared.Atmos.Components;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Localization;
-using Robust.Shared.Timing;
-using Robust.Shared.ViewVariables;
namespace Content.Client.Atmos.Components
{
[RegisterComponent]
- internal sealed class GasAnalyzerComponent : SharedGasAnalyzerComponent, IItemStatus
+ internal sealed class GasAnalyzerComponent : SharedGasAnalyzerComponent
{
- [ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
- [ViewVariables] private GasAnalyzerDanger Danger { get; set; }
-
- Control IItemStatus.MakeControl()
- {
- return new StatusControl(this);
- }
-
- ///
- public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
- {
- if (curState is not GasAnalyzerComponentState state)
- return;
-
- Danger = state.Danger;
- _uiUpdateNeeded = true;
- }
-
- private sealed class StatusControl : Control
- {
- private readonly GasAnalyzerComponent _parent;
- private readonly RichTextLabel _label;
-
- public StatusControl(GasAnalyzerComponent parent)
- {
- _parent = parent;
- _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
- AddChild(_label);
-
- Update();
- }
-
- ///
- protected override void FrameUpdate(FrameEventArgs args)
- {
- base.FrameUpdate(args);
-
- if (!_parent._uiUpdateNeeded)
- {
- return;
- }
-
- Update();
- }
-
- public void Update()
- {
- _parent._uiUpdateNeeded = false;
-
- var color = _parent.Danger switch
- {
- GasAnalyzerDanger.Warning => "orange",
- GasAnalyzerDanger.Hazard => "red",
- _ => "green",
- };
-
- _label.SetMarkup(Loc.GetString("itemstatus-pressure-warn", ("color", color), ("danger", _parent.Danger)));
- }
- }
}
}
diff --git a/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs b/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs
index c79fa03a38..5aee69dd47 100644
--- a/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs
+++ b/Content.Client/Atmos/UI/GasAnalyzerBoundUserInterface.cs
@@ -1,5 +1,4 @@
using Robust.Client.GameObjects;
-using Robust.Shared.GameObjects;
using static Content.Shared.Atmos.Components.SharedGasAnalyzerComponent;
namespace Content.Client.Atmos.UI
@@ -10,34 +9,41 @@ namespace Content.Client.Atmos.UI
{
}
- private GasAnalyzerWindow? _menu;
+ private GasAnalyzerWindow? _window;
protected override void Open()
{
base.Open();
- _menu = new GasAnalyzerWindow(this);
- _menu.OnClose += Close;
- _menu.OpenCentered();
+ _window = new GasAnalyzerWindow(this);
+ _window.OnClose += OnClose;
+ _window.OpenCentered();
}
- protected override void UpdateState(BoundUserInterfaceState state)
+ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
- base.UpdateState(state);
-
- _menu?.Populate((GasAnalyzerBoundUserInterfaceState) state);
+ if (_window == null)
+ return;
+ if (message is not GasAnalyzerUserMessage cast)
+ return;
+ _window.Populate(cast);
}
- public void Refresh()
+ ///
+ /// Closes UI and tells the server to disable the analyzer
+ ///
+ private void OnClose()
{
- SendMessage(new GasAnalyzerRefreshMessage());
+ SendMessage(new GasAnalyzerDisableMessage());
+ Close();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
- if (disposing) _menu?.Dispose();
+ if (disposing)
+ _window?.Dispose();
}
}
}
diff --git a/Content.Client/Atmos/UI/GasAnalyzerMenu.cs b/Content.Client/Atmos/UI/GasAnalyzerMenu.cs
deleted file mode 100644
index acb58d706b..0000000000
--- a/Content.Client/Atmos/UI/GasAnalyzerMenu.cs
+++ /dev/null
@@ -1,282 +0,0 @@
-using Content.Client.Resources;
-using Content.Client.Stylesheets;
-using Content.Shared.Temperature;
-using Robust.Client.Graphics;
-using Robust.Client.ResourceManagement;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
-using Robust.Shared.Maths;
-using static Content.Shared.Atmos.Components.SharedGasAnalyzerComponent;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
-
-namespace Content.Client.Atmos.UI
-{
- public sealed class GasAnalyzerWindow : BaseWindow
- {
- public GasAnalyzerBoundUserInterface Owner { get; }
-
- private readonly Control _topContainer;
- private readonly Control _statusContainer;
-
- private readonly Label _nameLabel;
-
- public TextureButton CloseButton { get; set; }
-
- public GasAnalyzerWindow(GasAnalyzerBoundUserInterface owner)
- {
- var resourceCache = IoCManager.Resolve();
-
- Owner = owner;
- var rootContainer = new LayoutContainer { Name = "WireRoot" };
- AddChild(rootContainer);
-
- MouseFilter = MouseFilterMode.Stop;
-
- var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
- var back = new StyleBoxTexture
- {
- Texture = panelTex,
- Modulate = Color.FromHex("#25252A"),
- };
- back.SetPatchMargin(StyleBox.Margin.All, 10);
-
- var topPanel = new PanelContainer
- {
- PanelOverride = back,
- MouseFilter = MouseFilterMode.Pass
- };
- var bottomWrap = new LayoutContainer
- {
- Name = "BottomWrap"
- };
-
- rootContainer.AddChild(topPanel);
- rootContainer.AddChild(bottomWrap);
-
- LayoutContainer.SetAnchorPreset(topPanel, LayoutContainer.LayoutPreset.Wide);
- LayoutContainer.SetMarginBottom(topPanel, -80);
-
- LayoutContainer.SetAnchorPreset(bottomWrap, LayoutContainer.LayoutPreset.VerticalCenterWide);
- LayoutContainer.SetGrowHorizontal(bottomWrap, LayoutContainer.GrowDirection.Both);
-
- var topContainerWrap = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- Children =
- {
- (_topContainer = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical
- }),
- new Control {MinSize = (0, 110)}
- }
- };
-
- rootContainer.AddChild(topContainerWrap);
-
- LayoutContainer.SetAnchorPreset(topContainerWrap, LayoutContainer.LayoutPreset.Wide);
-
- var font = resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
- var fontSmall = resourceCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 10);
-
- Button refreshButton;
- var topRow = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- Margin = new Thickness(4, 4, 12, 2),
- Children =
- {
- (_nameLabel = new Label
- {
- Text = Loc.GetString("gas-analyzer-window-name"),
- FontOverride = font,
- FontColorOverride = StyleNano.NanoGold,
- VerticalAlignment = VAlignment.Center
- }),
- new Control
- {
- MinSize = (20, 0),
- HorizontalExpand = true,
- },
- (refreshButton = new Button {Text = Loc.GetString("gas-analyzer-window-refresh-button")}), //TODO: refresh icon?
- new Control
- {
- MinSize = (2, 0),
- },
- (CloseButton = new TextureButton
- {
- StyleClasses = {DefaultWindow.StyleClassWindowCloseButton},
- VerticalAlignment = VAlignment.Center
- })
- }
- };
-
- refreshButton.OnPressed += a =>
- {
- Owner.Refresh();
- };
-
- var middle = new PanelContainer
- {
- PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#202025") },
- Children =
- {
- (_statusContainer = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- Margin = new Thickness(8, 8, 4, 4)
- })
- }
- };
-
- _topContainer.AddChild(topRow);
- _topContainer.AddChild(new PanelContainer
- {
- MinSize = (0, 2),
- PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#525252ff") }
- });
- _topContainer.AddChild(middle);
- _topContainer.AddChild(new PanelContainer
- {
- MinSize = (0, 2),
- PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#525252ff") }
- });
- CloseButton.OnPressed += _ => Close();
- SetSize = (300, 420);
- }
-
-
- public void Populate(GasAnalyzerBoundUserInterfaceState state)
- {
- _statusContainer.RemoveAllChildren();
- if (state.Error != null)
- {
- _statusContainer.AddChild(new Label
- {
- Text = Loc.GetString("gas-analyzer-window-error-text", ("errorText", state.Error)),
- FontColorOverride = Color.Red
- });
- return;
- }
-
- _statusContainer.AddChild(new Label
- {
- Text = Loc.GetString("gas-analyzer-window-pressure-text", ("pressure", $"{state.Pressure:0.##}"))
- });
- _statusContainer.AddChild(new Label
- {
- Text = Loc.GetString("gas-analyzer-window-temperature-text",
- ("tempK", $"{state.Temperature:0.#}"),
- ("tempC", $"{TemperatureHelpers.KelvinToCelsius(state.Temperature):0.#}"))
- });
- // Return here cause all that stuff down there is gas stuff (so we don't get the seperators)
- if (state.Gases == null || state.Gases.Length == 0)
- {
- return;
- }
-
- // Seperator
- _statusContainer.AddChild(new Control
- {
- MinSize = new Vector2(0, 10)
- });
-
- // Add a table with all the gases
- var tableKey = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical
- };
- var tableVal = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical
- };
- _statusContainer.AddChild(new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- Children =
- {
- tableKey,
- new Control
- {
- MinSize = new Vector2(20, 0)
- },
- tableVal
- }
- });
- // This is the gas bar thingy
- var height = 30;
- var minSize = 24; // This basically allows gases which are too small, to be shown properly
- var gasBar = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- MinSize = new Vector2(0, height)
- };
- // Seperator
- _statusContainer.AddChild(new Control
- {
- MinSize = new Vector2(0, 10)
- });
-
- var totalGasAmount = 0f;
- foreach (var gas in state.Gases)
- {
- totalGasAmount += gas.Amount;
- }
-
- for (int i = 0; i < state.Gases.Length; i++)
- {
- var gas = state.Gases[i];
- var color = Color.FromHex($"#{gas.Color}", Color.White);
- // Add to the table
- tableKey.AddChild(new Label
- {
- Text = Loc.GetString(gas.Name)
- });
- tableVal.AddChild(new Label
- {
- Text = Loc.GetString("gas-analyzer-window-molality-text", ("mol", $"{gas.Amount:0.##}"))
- });
-
- // Add to the gas bar //TODO: highlight the currently hover one
- var left = (i == 0) ? 0f : 2f;
- var right = (i == state.Gases.Length - 1) ? 0f : 2f;
- gasBar.AddChild(new PanelContainer
- {
- ToolTip = Loc.GetString("gas-analyzer-window-molality-percentage-text",
- ("gasName", gas.Name),
- ("amount", $"{gas.Amount:0.##}"),
- ("percentage", $"{(gas.Amount / totalGasAmount * 100):0.#}")),
- HorizontalExpand = true,
- SizeFlagsStretchRatio = gas.Amount,
- MouseFilter = MouseFilterMode.Pass,
- PanelOverride = new StyleBoxFlat
- {
- BackgroundColor = color,
- PaddingLeft = left,
- PaddingRight = right
- },
- MinSize = new Vector2(minSize, 0)
- });
- }
-
- _statusContainer.AddChild(gasBar);
- }
-
- protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
- {
- return DragMode.Move;
- }
-
- protected override bool HasPoint(Vector2 point)
- {
- // This makes it so our base window won't count for hit tests,
- // but we will still receive mouse events coming in from Pass mouse filter mode.
- // So basically, it perfectly shells out the hit tests to the panels we have!
- return false;
- }
- }
-}
diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml
new file mode 100644
index 0000000000..c8eb42cf56
--- /dev/null
+++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
new file mode 100644
index 0000000000..81c35096b5
--- /dev/null
+++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
@@ -0,0 +1,326 @@
+using Content.Shared.Atmos;
+using Content.Shared.Temperature;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.XAML;
+using static Content.Shared.Atmos.Components.SharedGasAnalyzerComponent;
+
+namespace Content.Client.Atmos.UI
+{
+ [GenerateTypedNameReferences]
+ public sealed partial class GasAnalyzerWindow : DefaultWindow
+ {
+ private GasAnalyzerBoundUserInterface _owner;
+ private IEntityManager _entityManager;
+
+ public GasAnalyzerWindow(GasAnalyzerBoundUserInterface owner)
+ {
+ RobustXamlLoader.Load(this);
+ _entityManager = IoCManager.Resolve();
+ _owner = owner;
+ }
+
+ public void Populate(GasAnalyzerUserMessage msg)
+ {
+ if (msg.Error != null)
+ {
+ CTopBox.AddChild(new Label
+ {
+ Text = Loc.GetString("gas-analyzer-window-error-text", ("errorText", msg.Error)),
+ FontColorOverride = Color.Red
+ });
+ return;
+ }
+
+ if (msg.NodeGasMixes.Length == 0)
+ {
+ CTopBox.AddChild(new Label
+ {
+ Text = Loc.GetString("gas-analyzer-window-no-data")
+ });
+ MinSize = new Vector2(CTopBox.DesiredSize.X + 40, MinSize.Y);
+ return;
+ }
+
+ Vector2 minSize;
+
+ // Environment Tab
+ var envMix = msg.NodeGasMixes[0];
+
+ CTabContainer.SetTabTitle(1, envMix.Name);
+ CEnvironmentMix.RemoveAllChildren();
+ GenerateGasDisplay(envMix, CEnvironmentMix);
+
+ // Device Tab
+ if (msg.NodeGasMixes.Length > 1)
+ {
+ CTabContainer.SetTabVisible(0, true);
+ CTabContainer.SetTabTitle(0, Loc.GetString("gas-analyzer-window-tab-title-capitalized", ("title", msg.DeviceName)));
+ // Set up Grid
+ GridIcon.OverrideDirection = msg.NodeGasMixes.Length switch
+ {
+ // Unary layout
+ 2 => Direction.South,
+ // Binary layout
+ 3 => Direction.East,
+ // Trinary layout
+ 4 => Direction.East,
+ _ => GridIcon.OverrideDirection
+ };
+
+ GridIcon.Sprite = _entityManager.GetComponent(msg.DeviceUid);
+ LeftPanel.RemoveAllChildren();
+ MiddlePanel.RemoveAllChildren();
+ RightPanel.RemoveAllChildren();
+ if (msg.NodeGasMixes.Length == 2)
+ {
+ // Unary, use middle
+ LeftPanelLabel.Text = string.Empty;
+ MiddlePanelLabel.Text = Loc.GetString("gas-analyzer-window-tab-title-capitalized", ("title", msg.NodeGasMixes[1].Name));
+ RightPanelLabel.Text = string.Empty;
+
+ LeftPanel.Visible = false;
+ MiddlePanel.Visible = true;
+ RightPanel.Visible = false;
+
+ GenerateGasDisplay(msg.NodeGasMixes[1], MiddlePanel);
+
+ minSize = new Vector2(CDeviceGrid.DesiredSize.X + 40, MinSize.Y);
+ }
+ else if (msg.NodeGasMixes.Length == 3)
+ {
+ // Binary, use left and right
+ LeftPanelLabel.Text = Loc.GetString("gas-analyzer-window-tab-title-capitalized", ("title", msg.NodeGasMixes[1].Name));
+ MiddlePanelLabel.Text = string.Empty;
+ RightPanelLabel.Text = Loc.GetString("gas-analyzer-window-tab-title-capitalized", ("title", msg.NodeGasMixes[2].Name));
+
+ LeftPanel.Visible = true;
+ MiddlePanel.Visible = false;
+ RightPanel.Visible = true;
+
+ GenerateGasDisplay(msg.NodeGasMixes[1], LeftPanel);
+ GenerateGasDisplay(msg.NodeGasMixes[2], RightPanel);
+
+ minSize = new Vector2(CDeviceGrid.DesiredSize.X + 40, MinSize.Y);
+ }
+ else if (msg.NodeGasMixes.Length == 4)
+ {
+ // Trinary, use all three
+ // Trinary can be flippable, which complicates how to display things currently
+ LeftPanelLabel.Text = Loc.GetString("gas-analyzer-window-tab-title-capitalized",
+ ("title", msg.DeviceFlipped ? msg.NodeGasMixes[1].Name : msg.NodeGasMixes[3].Name));
+ MiddlePanelLabel.Text = Loc.GetString("gas-analyzer-window-tab-title-capitalized", ("title", msg.NodeGasMixes[2].Name));
+ RightPanelLabel.Text = Loc.GetString("gas-analyzer-window-tab-title-capitalized",
+ ("title", msg.DeviceFlipped ? msg.NodeGasMixes[3].Name : msg.NodeGasMixes[1].Name));
+
+ LeftPanel.Visible = true;
+ MiddlePanel.Visible = true;
+ RightPanel.Visible = true;
+
+ GenerateGasDisplay(msg.DeviceFlipped ? msg.NodeGasMixes[1] : msg.NodeGasMixes[3], LeftPanel);
+ GenerateGasDisplay(msg.NodeGasMixes[2], MiddlePanel);
+ GenerateGasDisplay(msg.DeviceFlipped ? msg.NodeGasMixes[3] : msg.NodeGasMixes[1], RightPanel);
+
+ minSize = new Vector2(CDeviceGrid.DesiredSize.X + 40, MinSize.Y);
+ }
+ else
+ {
+ // oh shit of fuck its more than 4 this ui isn't gonna look pretty anymore
+ for (var i = 1; i < msg.NodeGasMixes.Length; i++)
+ {
+ GenerateGasDisplay(msg.NodeGasMixes[i], CDeviceMixes);
+ }
+ LeftPanel.Visible = false;
+ MiddlePanel.Visible = false;
+ RightPanel.Visible = false;
+ minSize = new Vector2(CDeviceMixes.DesiredSize.X + 40, MinSize.Y);
+ }
+ }
+ else
+ {
+ // Hide device tab, no device selected
+ CTabContainer.SetTabVisible(0, false);
+ CTabContainer.CurrentTab = 1;
+ minSize = new Vector2(CEnvironmentMix.DesiredSize.X + 40, MinSize.Y);
+ }
+
+ MinSize = minSize;
+ }
+
+ private void GenerateGasDisplay(GasMixEntry gasMix, Control parent)
+ {
+ var panel = new PanelContainer
+ {
+ VerticalExpand = true,
+ HorizontalExpand = true,
+ Margin = new Thickness(4),
+ PanelOverride = new StyleBoxFlat{BorderColor = Color.FromHex("#4f4f4f"), BorderThickness = new Thickness(1)}
+ };
+ var dataContainer = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Vertical, VerticalExpand = true, Margin = new Thickness(4)};
+
+
+ parent.AddChild(panel);
+ panel.AddChild(dataContainer);
+
+ // Pressure label
+ var presBox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal };
+
+ presBox.AddChild(new Label
+ {
+ Text = Loc.GetString("gas-analyzer-window-pressure-text")
+ });
+ presBox.AddChild(new Control
+ {
+ MinSize = new Vector2(10, 0),
+ HorizontalExpand = true
+ });
+ presBox.AddChild(new Label
+ {
+ Text = Loc.GetString("gas-analyzer-window-pressure-val-text", ("pressure", $"{gasMix.Pressure:0.##}")),
+ Align = Label.AlignMode.Right,
+ HorizontalExpand = true
+ });
+ dataContainer.AddChild(presBox);
+
+ // If there is no gas, it doesn't really have a temperature, so skip displaying it
+ if (gasMix.Pressure > Atmospherics.GasMinMoles)
+ {
+ // Temperature label
+ var tempBox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal };
+
+ tempBox.AddChild(new Label
+ {
+ Text = Loc.GetString("gas-analyzer-window-temperature-text")
+ });
+ tempBox.AddChild(new Control
+ {
+ MinSize = new Vector2(10, 0),
+ HorizontalExpand = true
+ });
+ tempBox.AddChild(new Label
+ {
+ Text = Loc.GetString("gas-analyzer-window-temperature-val-text",
+ ("tempK", $"{gasMix.Temperature:0.#}"),
+ ("tempC", $"{TemperatureHelpers.KelvinToCelsius(gasMix.Temperature):0.#}")),
+ Align = Label.AlignMode.Right,
+ HorizontalExpand = true
+ });
+ dataContainer.AddChild(tempBox);
+ }
+
+ if (gasMix.Gases == null || gasMix.Gases?.Length == 0)
+ {
+ // Separator
+ dataContainer.AddChild(new Control
+ {
+ MinSize = new Vector2(0, 10)
+ });
+
+ // Add a label that there are no gases so it's less confusing
+ dataContainer.AddChild(new Label
+ {
+ Text = Loc.GetString("gas-analyzer-window-no-gas-text"),
+ FontColorOverride = Color.Gray
+ });
+ // return, everything below is for the fancy gas display stuff
+ return;
+ }
+ // Separator
+ dataContainer.AddChild(new Control
+ {
+ MinSize = new Vector2(0, 10)
+ });
+
+ // Add a table with all the gases
+ var tableKey = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical
+ };
+ var tableVal = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical
+ };
+ dataContainer.AddChild(new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Children =
+ {
+ tableKey,
+ new Control
+ {
+ MinSize = new Vector2(10, 0),
+ HorizontalExpand = true
+ },
+ tableVal
+ }
+ });
+ // This is the gas bar thingy
+ var height = 30;
+ var minSize = 24; // This basically allows gases which are too small, to be shown properly
+ var gasBar = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalExpand = true,
+ MinSize = new Vector2(0, height)
+ };
+ // Separator
+ dataContainer.AddChild(new Control
+ {
+ MinSize = new Vector2(0, 10),
+ VerticalExpand = true
+ });
+
+ var totalGasAmount = 0f;
+ foreach (var gas in gasMix.Gases!)
+ {
+ totalGasAmount += gas.Amount;
+ }
+
+ for (var j = 0; j < gasMix.Gases.Length; j++)
+ {
+ var gas = gasMix.Gases[j];
+ var color = Color.FromHex($"#{gas.Color}", Color.White);
+ // Add to the table
+ tableKey.AddChild(new Label
+ {
+ Text = Loc.GetString(gas.Name)
+ });
+ tableVal.AddChild(new Label
+ {
+ Text = Loc.GetString("gas-analyzer-window-molarity-text",
+ ("mol", $"{gas.Amount:0.##}"),
+ ("percentage", $"{(gas.Amount / totalGasAmount * 100):0.#}")),
+ Align = Label.AlignMode.Right,
+ HorizontalExpand = true
+ });
+
+ // Add to the gas bar //TODO: highlight the currently hover one
+ var left = (j == 0) ? 0f : 2f;
+ var right = (j == gasMix.Gases.Length - 1) ? 0f : 2f;
+ gasBar.AddChild(new PanelContainer
+ {
+ ToolTip = Loc.GetString("gas-analyzer-window-molarity-percentage-text",
+ ("gasName", gas.Name),
+ ("amount", $"{gas.Amount:0.##}"),
+ ("percentage", $"{(gas.Amount / totalGasAmount * 100):0.#}")),
+ HorizontalExpand = true,
+ SizeFlagsStretchRatio = gas.Amount,
+ MouseFilter = MouseFilterMode.Stop,
+ PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = color,
+ PaddingLeft = left,
+ PaddingRight = right
+ },
+ MinSize = new Vector2(minSize, 0)
+ });
+ }
+
+ dataContainer.AddChild(gasBar);
+ }
+ }
+}
diff --git a/Content.Server/Atmos/Components/GasAnalyzableComponent.cs b/Content.Server/Atmos/Components/GasAnalyzableComponent.cs
deleted file mode 100644
index 71e74cec08..0000000000
--- a/Content.Server/Atmos/Components/GasAnalyzableComponent.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * GasAnalyzableComponent is a component for anything that can be examined with a gas analyzer.
- */
-namespace Content.Server.Atmos.Components
-{
- [RegisterComponent]
- public sealed class GasAnalyzableComponent : Component
- {
- // Empty
- }
-}
diff --git a/Content.Server/Atmos/Components/GasAnalyzerComponent.cs b/Content.Server/Atmos/Components/GasAnalyzerComponent.cs
index 39263dd61b..e3bd45eb46 100644
--- a/Content.Server/Atmos/Components/GasAnalyzerComponent.cs
+++ b/Content.Server/Atmos/Components/GasAnalyzerComponent.cs
@@ -1,14 +1,4 @@
-using System.Threading.Tasks;
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.Hands.Components;
-using Content.Server.UserInterface;
-using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
-using Content.Shared.Interaction;
-using Content.Shared.Maps;
-using Content.Shared.Popups;
-using Robust.Server.GameObjects;
-using Robust.Server.Player;
using Robust.Shared.Map;
namespace Content.Server.Atmos.Components
@@ -17,227 +7,24 @@ namespace Content.Server.Atmos.Components
[ComponentReference(typeof(SharedGasAnalyzerComponent))]
public sealed class GasAnalyzerComponent : SharedGasAnalyzerComponent
{
- [Dependency] private readonly IEntityManager _entities = default!;
+ [ViewVariables] public EntityUid? Target;
+ [ViewVariables] public EntityUid User;
+ [ViewVariables] public EntityCoordinates? LastPosition;
+ [ViewVariables] public bool Enabled;
+ }
- private GasAnalyzerDanger _pressureDanger;
- private float _timeSinceSync;
- private const float TimeBetweenSyncs = 2f;
- private bool _checkPlayer = false; // Check at the player pos or at some other tile?
- private EntityCoordinates? _position; // The tile that we scanned
- private AppearanceComponent? _appearance;
-
- [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(GasAnalyzerUiKey.Key);
-
- protected override void Initialize()
- {
- base.Initialize();
-
- if (UserInterface != null)
- {
- UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
- }
-
- _entities.TryGetComponent(Owner, out _appearance);
- }
-
- public override ComponentState GetComponentState()
- {
- return new GasAnalyzerComponentState(_pressureDanger);
- }
+ ///
+ /// Used to keep track of which analyzers are active for update purposes
+ ///
+ [RegisterComponent]
+ public sealed class ActiveGasAnalyzerComponent : Component
+ {
+ // Set to a tiny bit after the default because otherwise the user often gets a blank window when first using
+ public float AccumulatedFrametime = 2.01f;
///
- /// Call this from other components to open the gas analyzer UI.
- /// Uses the player position.
+ /// How often to update the analyzer
///
- /// The session to open the ui for
- public void OpenInterface(IPlayerSession session)
- {
- _checkPlayer = true;
- _position = null;
- UserInterface?.Open(session);
- UpdateUserInterface();
- UpdateAppearance(true);
- Resync();
- }
-
- ///
- /// Call this from other components to open the gas analyzer UI.
- /// Uses a given position.
- ///
- /// The session to open the ui for
- /// The position to analyze the gas
- public void OpenInterface(IPlayerSession session, EntityCoordinates pos)
- {
- _checkPlayer = false;
- _position = pos;
- UserInterface?.Open(session);
- UpdateUserInterface();
- UpdateAppearance(true);
- Resync();
- }
-
- public void ToggleInterface(IPlayerSession session)
- {
- if (UserInterface == null)
- return;
-
- if (UserInterface.SessionHasOpen(session))
- CloseInterface(session);
- else
- OpenInterface(session);
- }
-
- public void CloseInterface(IPlayerSession session)
- {
- _position = null;
- UserInterface?.Close(session);
- // Our OnClose will do the appearance stuff
- Resync();
- }
-
- public void UpdateAppearance(bool open)
- {
- _appearance?.SetData(GasAnalyzerVisuals.VisualState,
- open ? GasAnalyzerVisualState.Working : GasAnalyzerVisualState.Off);
- }
-
- public void Update(float frameTime)
- {
- _timeSinceSync += frameTime;
- if (_timeSinceSync > TimeBetweenSyncs)
- {
- Resync();
- UpdateUserInterface();
- }
- }
-
- private void Resync()
- {
- // Already get the pressure before Dirty(), because we can't get the EntitySystem in that thread or smth
- var pressure = 0f;
- var tile = EntitySystem.Get().GetContainingMixture(Owner, true);
- if (tile != null)
- {
- pressure = tile.Pressure;
- }
-
- if (pressure >= Atmospherics.HazardHighPressure || pressure <= Atmospherics.HazardLowPressure)
- {
- _pressureDanger = GasAnalyzerDanger.Hazard;
- }
- else if (pressure >= Atmospherics.WarningHighPressure || pressure <= Atmospherics.WarningLowPressure)
- {
- _pressureDanger = GasAnalyzerDanger.Warning;
- }
- else
- {
- _pressureDanger = GasAnalyzerDanger.Nominal;
- }
-
- Dirty();
- _timeSinceSync = 0f;
- }
-
- private void UpdateUserInterface()
- {
- if (UserInterface == null)
- {
- return;
- }
-
- string? error = null;
-
- // Check if the player is still holding the gas analyzer => if not, don't update
- foreach (var session in UserInterface.SubscribedSessions)
- {
- if (session.AttachedEntity is not {Valid: true} playerEntity)
- return;
-
- if (!_entities.TryGetComponent(playerEntity, out HandsComponent? handsComponent))
- return;
-
- if (handsComponent?.ActiveHandEntity is not {Valid: true} activeHandEntity ||
- !_entities.TryGetComponent(activeHandEntity, out GasAnalyzerComponent? gasAnalyzer))
- {
- return;
- }
- }
-
- var pos = _entities.GetComponent(Owner).Coordinates;
- if (!_checkPlayer && _position.HasValue)
- {
- // Check if position is out of range => don't update
- if (!_position.Value.InRange(_entities, pos, SharedInteractionSystem.InteractionRange))
- return;
-
- pos = _position.Value;
- }
-
- var gridUid = pos.GetGridUid(_entities);
- var mapUid = pos.GetMapUid(_entities);
- var position = pos.ToVector2i(_entities, IoCManager.Resolve());
-
- var atmosphereSystem = EntitySystem.Get();
- var tile = atmosphereSystem.GetTileMixture(gridUid, mapUid, position);
- if (tile == null)
- {
- error = "No Atmosphere!";
- UserInterface.SetState(
- new GasAnalyzerBoundUserInterfaceState(
- 0,
- 0,
- null,
- error));
- return;
- }
-
- var gases = new List();
-
- for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
- {
- var gas = atmosphereSystem.GetGas(i);
-
- if (tile.Moles[i] <= Atmospherics.GasMinMoles) continue;
-
- gases.Add(new GasEntry(gas.Name, tile.Moles[i], gas.Color));
- }
-
- UserInterface.SetState(
- new GasAnalyzerBoundUserInterfaceState(
- tile.Pressure,
- tile.Temperature,
- gases.ToArray(),
- error));
- }
-
- private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
- {
- var message = serverMsg.Message;
- switch (message)
- {
- case GasAnalyzerRefreshMessage msg:
- if (serverMsg.Session.AttachedEntity is not {Valid: true} player)
- {
- return;
- }
-
- if (!_entities.TryGetComponent(player, out HandsComponent? handsComponent))
- {
- Owner.PopupMessage(player, Loc.GetString("gas-analyzer-component-player-has-no-hands-message"));
- return;
- }
-
- if (handsComponent.ActiveHandEntity is not {Valid: true} activeHandEntity ||
- !_entities.TryGetComponent(activeHandEntity, out GasAnalyzerComponent? gasAnalyzer))
- {
- serverMsg.Session.AttachedEntity.Value.PopupMessage(Loc.GetString("gas-analyzer-component-need-gas-analyzer-in-hand-message"));
- return;
- }
-
- UpdateUserInterface();
- Resync();
- break;
- }
- }
+ public float UpdateInterval = 1f;
}
}
diff --git a/Content.Server/Atmos/EntitySystems/GasAnalyzableSystem.cs b/Content.Server/Atmos/EntitySystems/GasAnalyzableSystem.cs
deleted file mode 100644
index bd98f5183f..0000000000
--- a/Content.Server/Atmos/EntitySystems/GasAnalyzableSystem.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using Content.Server.Atmos.Components;
-using Content.Server.NodeContainer.Nodes;
-using Content.Server.NodeContainer;
-using Content.Shared.Atmos.Components;
-using Content.Shared.Examine;
-using Content.Shared.Temperature;
-using Content.Shared.Verbs;
-using JetBrains.Annotations;
-using Robust.Shared.Utility;
-using System.Linq;
-
-namespace Content.Server.Atmos.EntitySystems
-{
- [UsedImplicitly]
- public sealed class GasAnalyzableSystem : EntitySystem
- {
- [Dependency] private readonly ExamineSystemShared _examineSystem = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent>(OnGetExamineVerbs);
- SubscribeLocalEvent((_,c,_) => c.UpdateAppearance(false));
- }
-
- private void OnGetExamineVerbs(EntityUid uid, GasAnalyzableComponent component, GetVerbsEvent args)
- {
- // Must be in details range to try this.
- if (_examineSystem.IsInDetailsRange(args.User, args.Target))
- {
- var held = args.Using;
- var enabled = held != null && EntityManager.HasComponent(held);
- var verb = new ExamineVerb
- {
- Disabled = !enabled,
- Message = Loc.GetString("gas-analyzable-system-verb-tooltip"),
- Text = Loc.GetString("gas-analyzable-system-verb-name"),
- Category = VerbCategory.Examine,
- IconTexture = "/Textures/Interface/VerbIcons/examine.svg.192dpi.png",
- Act = () =>
- {
- var markup = FormattedMessage.FromMarkup(GeneratePipeMarkup(uid));
- _examineSystem.SendExamineTooltip(args.User, uid, markup, false, false);
- }
- };
-
- args.Verbs.Add(verb);
- }
- }
-
- private string GeneratePipeMarkup(EntityUid uid, NodeContainerComponent? nodeContainer = null)
- {
- if (!Resolve(uid, ref nodeContainer))
- return Loc.GetString("gas-analyzable-system-internal-error-missing-component");
-
- List portNames = new List();
- List portData = new List();
- foreach (var node in nodeContainer.Nodes)
- {
- if (node.Value is not PipeNode pn)
- continue;
- float pressure = pn.Air.Pressure;
- float temp = pn.Air.Temperature;
- portNames.Add(node.Key);
- portData.Add(Loc.GetString("gas-analyzable-system-statistics",
- ("pressure", pressure),
- ("tempK", $"{temp:0.#}"),
- ("tempC", $"{TemperatureHelpers.KelvinToCelsius(temp):0.#}")
- ));
- }
-
- int count = portNames.Count;
- if (count == 0)
- return Loc.GetString("gas-anlayzable-system-internal-error-no-gas-node");
- else if (count == 1)
- // omit names if only one node
- return Loc.GetString("gas-analyzable-system-header") + "\n" + portData[0];
- else
- {
- var outputs = portNames.Zip(portData, ((name, data) => name + ":\n" + data));
- return Loc.GetString("gas-analyzable-system-header") + "\n\n" + String.Join("\n\n", outputs);
- }
- }
- }
-}
diff --git a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs
index c7cf03c0d3..1fffb78cef 100644
--- a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs
@@ -1,9 +1,16 @@
-using Content.Server.Atmos.Components;
+using Content.Server.Atmos;
+using Content.Server.Atmos.Components;
+using Content.Server.NodeContainer;
+using Content.Server.NodeContainer.Nodes;
using Content.Server.Popups;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
+using static Content.Shared.Atmos.Components.SharedGasAnalyzerComponent;
namespace Content.Server.Atmos.EntitySystems
{
@@ -11,22 +18,41 @@ namespace Content.Server.Atmos.EntitySystems
public sealed class GasAnalyzerSystem : EntitySystem
{
[Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly AtmosphereSystem _atmo = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly UserInterfaceSystem _userInterface = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnDisabledMessage);
+ SubscribeLocalEvent(OnDropped);
+ SubscribeLocalEvent(OnUseInHand);
}
public override void Update(float frameTime)
{
- foreach (var analyzer in EntityManager.EntityQuery(true))
+
+ foreach (var analyzer in EntityQuery())
{
- analyzer.Update(frameTime);
+ // Don't update every tick
+ analyzer.AccumulatedFrametime += frameTime;
+
+ if (analyzer.AccumulatedFrametime < analyzer.UpdateInterval)
+ continue;
+
+ analyzer.AccumulatedFrametime -= analyzer.UpdateInterval;
+
+ if (!UpdateAnalyzer(analyzer.Owner))
+ RemCompDeferred(analyzer.Owner);
}
}
+ ///
+ /// Activates the analyzer when used in the world, scanning either the target entity or the tile clicked
+ ///
private void OnAfterInteract(EntityUid uid, GasAnalyzerComponent component, AfterInteractEvent args)
{
if (!args.CanReach)
@@ -34,13 +60,207 @@ namespace Content.Server.Atmos.EntitySystems
_popup.PopupEntity(Loc.GetString("gas-analyzer-component-player-cannot-reach-message"), args.User, Filter.Entities(args.User));
return;
}
+ ActivateAnalyzer(uid, component, args.User, args.Target);
+ OpenUserInterface(args.User, component);
+ args.Handled = true;
+ }
- if (TryComp(args.User, out ActorComponent? actor))
+ ///
+ /// Activates the analyzer with no target, so it only scans the tile the user was on when activated
+ ///
+ private void OnUseInHand(EntityUid uid, GasAnalyzerComponent component, UseInHandEvent args)
+ {
+ ActivateAnalyzer(uid, component, args.User);
+ args.Handled = true;
+ }
+
+ ///
+ /// Handles analyzer activation logic
+ ///
+ private void ActivateAnalyzer(EntityUid uid, GasAnalyzerComponent component, EntityUid user, EntityUid? target = null)
+ {
+ component.Target = target;
+ component.User = user;
+ component.LastPosition = Transform(target ?? user).Coordinates;
+ component.Enabled = true;
+ Dirty(component);
+ UpdateAppearance(component);
+ if(!HasComp(uid))
+ AddComp(uid);
+ UpdateAnalyzer(uid);
+ }
+
+ ///
+ /// Close the UI, turn the analyzer off, and don't update when it's dropped
+ ///
+ private void OnDropped(EntityUid uid, GasAnalyzerComponent component, DroppedEvent args)
+ {
+ if(args.User is { } userId && component.Enabled)
+ _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, Filter.Entities(userId));
+ DisableAnalyzer(uid, component, args.User);
+ }
+
+ ///
+ /// Closes the UI, sets the icon to off, and removes it from the update list
+ ///
+ private void DisableAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null, EntityUid? user = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ if (user != null && TryComp(user, out var actor))
+ _userInterface.TryClose(uid, GasAnalyzerUiKey.Key, actor.PlayerSession);
+
+ component.Enabled = false;
+ Dirty(component);
+ UpdateAppearance(component);
+ RemCompDeferred(uid);
+ }
+
+ ///
+ /// Disables the analyzer when the user closes the UI
+ ///
+ private void OnDisabledMessage(EntityUid uid, GasAnalyzerComponent component, GasAnalyzerDisableMessage message)
+ {
+ if (message.Session.AttachedEntity is not {Valid: true})
+ return;
+ DisableAnalyzer(uid, component);
+ }
+
+ private void OpenUserInterface(EntityUid user, GasAnalyzerComponent component)
+ {
+ if (!TryComp(user, out var actor))
+ return;
+
+ _userInterface.TryOpen(component.Owner, GasAnalyzerUiKey.Key, actor.PlayerSession);
+ }
+
+ ///
+ /// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button
+ ///
+ private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return false;
+
+ // check if the user has walked away from what they scanned
+ var userPos = Transform(component.User).Coordinates;
+ if (component.LastPosition.HasValue)
{
- component.OpenInterface(actor.PlayerSession, args.ClickLocation);
+ // Check if position is out of range => don't update and disable
+ if (!component.LastPosition.Value.InRange(EntityManager, userPos, SharedInteractionSystem.InteractionRange))
+ {
+ if(component.User is { } userId && component.Enabled)
+ _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, Filter.Entities(userId));
+ DisableAnalyzer(uid, component, component.User);
+ return false;
+ }
}
- args.Handled = true;
+ var gasMixList = new List();
+
+ // Fetch the environmental atmosphere around the scanner. This must be the first entry
+ var tileMixture = _atmo.GetContainingMixture(component.Owner, true);
+ if (tileMixture != null)
+ {
+ gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Pressure, tileMixture.Temperature,
+ GenerateGasEntryArray(tileMixture)));
+ }
+ else
+ {
+ // No gases were found
+ gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f));
+ }
+
+ var deviceFlipped = false;
+ if (component.Target != null)
+ {
+ // gas analyzed was used on an entity, try to request gas data via event for override
+ var ev = new GasAnalyzerScanEvent();
+ RaiseLocalEvent(component.Target.Value, ev, false);
+
+ if (ev.GasMixtures != null)
+ {
+ foreach (var mixes in ev.GasMixtures)
+ {
+ if(mixes.Value != null)
+ gasMixList.Add(new GasMixEntry(mixes.Key, mixes.Value.Pressure, mixes.Value.Temperature, GenerateGasEntryArray(mixes.Value)));
+ }
+
+ deviceFlipped = ev.DeviceFlipped;
+ }
+ else
+ {
+ // No override, fetch manually, to handle flippable devices you must subscribe to GasAnalyzerScanEvent
+ if (TryComp(component.Target, out NodeContainerComponent? node))
+ {
+ foreach (var pair in node.Nodes)
+ {
+ if (pair.Value is PipeNode pipeNode)
+ gasMixList.Add(new GasMixEntry(pair.Key, pipeNode.Air.Pressure, pipeNode.Air.Temperature, GenerateGasEntryArray(pipeNode.Air)));
+ }
+ }
+ }
+ }
+
+ // Don't bother sending a UI message with no content, and stop updating I guess?
+ if (gasMixList.Count == 0)
+ return false;
+
+ _userInterface.TrySendUiMessage(component.Owner, GasAnalyzerUiKey.Key,
+ new GasAnalyzerUserMessage(gasMixList.ToArray(),
+ component.Target != null ? Name(component.Target.Value) : string.Empty,
+ component.Target ?? EntityUid.Invalid,
+ deviceFlipped));
+ return true;
+ }
+
+ ///
+ /// Sets the appearance based on the analyzers Enabled state
+ ///
+ private void UpdateAppearance(GasAnalyzerComponent analyzer)
+ {
+ _appearance.SetData(analyzer.Owner, GasAnalyzerVisuals.Enabled, analyzer.Enabled);
+ }
+
+ ///
+ /// Generates a GasEntry array for a given GasMixture
+ ///
+ private GasEntry[] GenerateGasEntryArray(GasMixture? mixture)
+ {
+ var gases = new List();
+
+ for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
+ {
+ var gas = _atmo.GetGas(i);
+
+ if (mixture?.Moles[i] <= Atmospherics.GasMinMoles)
+ continue;
+
+ if (mixture != null)
+ gases.Add(new GasEntry(gas.Name, mixture.Moles[i], gas.Color));
+ }
+
+ return gases.ToArray();
}
}
}
+
+///
+/// Raised when the analyzer is used. An atmospherics device that does not rely on a NodeContainer or
+/// wishes to override the default analyzer behaviour of fetching all nodes in the attached NodeContainer
+/// should subscribe to this and return the GasMixtures as desired. A device that is flippable should subscribe
+/// to this event to report if it is flipped or not. See GasFilterSystem or GasMixerSystem for an example.
+///
+public sealed class GasAnalyzerScanEvent : EntityEventArgs
+{
+ ///
+ /// Key is the mix name (ex "pipe", "inlet", "filter"), value is the pipe direction and GasMixture. Add all mixes that should be reported when scanned.
+ ///
+ public Dictionary? GasMixtures;
+
+ ///
+ /// If the device is flipped. Flipped is defined as when the inline input is 90 degrees CW to the side input
+ ///
+ public bool DeviceFlipped;
+}
diff --git a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
index 4a411d349f..b2e1df1d76 100644
--- a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
@@ -44,6 +44,7 @@ namespace Content.Server.Atmos.EntitySystems
SubscribeLocalEvent(OnParentChange);
SubscribeLocalEvent(OnGasTankSetPressure);
SubscribeLocalEvent(OnGasTankToggleInternals);
+ SubscribeLocalEvent(OnAnalyzed);
}
private void OnGasShutdown(EntityUid uid, GasTankComponent component, ComponentShutdown args)
@@ -87,7 +88,7 @@ namespace Content.Server.Atmos.EntitySystems
{
// When an item is moved from hands -> pockets, the container removal briefly dumps the item on the floor.
// So this is a shitty fix, where the parent check is just delayed. But this really needs to get fixed
- // properly at some point.
+ // properly at some point.
component.CheckUser = true;
}
@@ -323,5 +324,13 @@ namespace Content.Server.Atmos.EntitySystems
{
return GetInternalsComponent(component) != null;
}
+
+ ///
+ /// Returns the gas mixture for the gas analyzer
+ ///
+ private void OnAnalyzed(EntityUid uid, GasTankComponent component, GasAnalyzerScanEvent args)
+ {
+ args.GasMixtures = new Dictionary { {Name(uid), component.Air} };
+ }
}
}
diff --git a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
index f10cf1ca17..97887e36d9 100644
--- a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
+++ b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasFilterSystem.cs
@@ -34,6 +34,7 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
SubscribeLocalEvent(OnFilterUpdated);
SubscribeLocalEvent(OnFilterLeaveAtmosphere);
SubscribeLocalEvent(OnFilterInteractHand);
+ SubscribeLocalEvent(OnFilterAnalyzed);
// Bound UI subscriptions
SubscribeLocalEvent(OnTransferRateChangeMessage);
SubscribeLocalEvent(OnSelectGasMessage);
@@ -159,5 +160,29 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
}
}
+
+ ///
+ /// Returns the gas mixture for the gas analyzer
+ ///
+ private void OnFilterAnalyzed(EntityUid uid, GasFilterComponent component, GasAnalyzerScanEvent args)
+ {
+ if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer))
+ return;
+
+ var gasMixDict = new Dictionary();
+
+ nodeContainer.TryGetNode(component.InletName, out PipeNode? inlet);
+ nodeContainer.TryGetNode(component.FilterName, out PipeNode? filterNode);
+
+ if(inlet != null)
+ gasMixDict.Add(Loc.GetString("gas-analyzer-window-text-inlet"), inlet.Air);
+ if(filterNode != null)
+ gasMixDict.Add(Loc.GetString("gas-analyzer-window-text-filter"), filterNode.Air);
+ if(nodeContainer.TryGetNode(component.OutletName, out PipeNode? outlet))
+ gasMixDict.Add(Loc.GetString("gas-analyzer-window-text-outlet"), outlet.Air);
+
+ args.GasMixtures = gasMixDict;
+ args.DeviceFlipped = inlet != null && filterNode != null && inlet.CurrentPipeDirection.ToDirection() == filterNode.CurrentPipeDirection.ToDirection().GetClockwise90Degrees();
+ }
}
}
diff --git a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs
index 213ff994f3..9de18bb2b5 100644
--- a/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs
+++ b/Content.Server/Atmos/Piping/Trinary/EntitySystems/GasMixerSystem.cs
@@ -31,6 +31,7 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
SubscribeLocalEvent(OnInit);
SubscribeLocalEvent(OnMixerUpdated);
SubscribeLocalEvent(OnMixerInteractHand);
+ SubscribeLocalEvent(OnMixerAnalyzed);
// Bound UI subscriptions
SubscribeLocalEvent(OnOutputPressureChangeMessage);
SubscribeLocalEvent(OnChangeNodePercentageMessage);
@@ -203,5 +204,29 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
$"{EntityManager.ToPrettyString(args.Session.AttachedEntity!.Value):player} set the ratio on {EntityManager.ToPrettyString(uid):device} to {mixer.InletOneConcentration}:{mixer.InletTwoConcentration}");
DirtyUI(uid, mixer);
}
+
+ ///
+ /// Returns the gas mixture for the gas analyzer
+ ///
+ private void OnMixerAnalyzed(EntityUid uid, GasMixerComponent component, GasAnalyzerScanEvent args)
+ {
+ if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer))
+ return;
+
+ var gasMixDict = new Dictionary();
+
+ nodeContainer.TryGetNode(component.InletOneName, out PipeNode? inletOne);
+ nodeContainer.TryGetNode(component.InletTwoName, out PipeNode? inletTwo);
+
+ if(inletOne != null)
+ gasMixDict.Add($"{inletOne.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletOne.Air);
+ if(inletTwo != null)
+ gasMixDict.Add($"{inletTwo.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletTwo.Air);
+ if(nodeContainer.TryGetNode(component.OutletName, out PipeNode? outlet))
+ gasMixDict.Add(Loc.GetString("gas-analyzer-window-text-outlet"), outlet.Air);
+
+ args.GasMixtures = gasMixDict;
+ args.DeviceFlipped = inletOne != null && inletTwo != null && inletOne.CurrentPipeDirection.ToDirection() == inletTwo.CurrentPipeDirection.ToDirection().GetClockwise90Degrees();
+ }
}
}
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
index ca5239a138..a9dd4ec1f8 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasCanisterSystem.cs
@@ -38,6 +38,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
SubscribeLocalEvent(OnCanisterContainerInserted);
SubscribeLocalEvent(OnCanisterContainerRemoved);
SubscribeLocalEvent(CalculateCanisterPrice);
+ SubscribeLocalEvent(OnAnalyzed);
// Bound UI subscriptions
SubscribeLocalEvent(OnHoldingTankEjectMessage);
SubscribeLocalEvent(OnCanisterChangeReleasePressure);
@@ -314,5 +315,13 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
}
args.Price += basePrice * purity;
}
+
+ ///
+ /// Returns the gas mixture for the gas analyzer
+ ///
+ private void OnAnalyzed(EntityUid uid, GasCanisterComponent component, GasAnalyzerScanEvent args)
+ {
+ args.GasMixtures = new Dictionary { {Name(uid), component.Air} };
+ }
}
}
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs
index 09e35b0892..23bdc776df 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasThermoMachineSystem.cs
@@ -69,7 +69,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
private void OnGasThermoRefreshParts(EntityUid uid, GasThermoMachineComponent component, RefreshPartsEvent args)
{
- // Here we evaluate the average quality of relevant machine parts.
+ // Here we evaluate the average quality of relevant machine parts.
var nLasers = 0;
var nBins= 0;
var matterBinRating = 0;
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs
index 7165904267..e2203aae4f 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs
@@ -44,6 +44,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
SubscribeLocalEvent(OnInit);
SubscribeLocalEvent(OnExamine);
SubscribeLocalEvent(OnSignalReceived);
+ SubscribeLocalEvent(OnAnalyzed);
}
private void OnGasVentPumpUpdated(EntityUid uid, GasVentPumpComponent vent, AtmosDeviceUpdateEvent args)
@@ -271,5 +272,28 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
}
}
}
+
+ ///
+ /// Returns the gas mixture for the gas analyzer
+ ///
+ private void OnAnalyzed(EntityUid uid, GasVentPumpComponent component, GasAnalyzerScanEvent args)
+ {
+ if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer))
+ return;
+
+ var gasMixDict = new Dictionary();
+
+ // these are both called pipe, above it switches using this so I duplicated that...?
+ var nodeName = component.PumpDirection switch
+ {
+ VentPumpDirection.Releasing => component.Inlet,
+ VentPumpDirection.Siphoning => component.Outlet,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ if(nodeContainer.TryGetNode(nodeName, out PipeNode? pipe))
+ gasMixDict.Add(nodeName, pipe.Air);
+
+ args.GasMixtures = gasMixDict;
+ }
}
}
diff --git a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs
index cc96a19b58..d122d29922 100644
--- a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs
+++ b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs
@@ -38,6 +38,7 @@ namespace Content.Server.Atmos.Portable
SubscribeLocalEvent(OnPowerChanged);
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnDestroyed);
+ SubscribeLocalEvent(OnScrubberAnalyzed);
}
private void OnDeviceUpdated(EntityUid uid, PortableScrubberComponent component, AtmosDeviceUpdateEvent args)
@@ -158,5 +159,21 @@ namespace Content.Server.Atmos.Portable
appearance.SetData(PortableScrubberVisuals.IsDraining, isDraining);
}
+
+ ///
+ /// Returns the gas mixture for the gas analyzer
+ ///
+ private void OnScrubberAnalyzed(EntityUid uid, PortableScrubberComponent component, GasAnalyzerScanEvent args)
+ {
+ var gasMixDict = new Dictionary();
+ // If it's connected to a port, include the port side
+ if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer))
+ {
+ if(nodeContainer != null && nodeContainer.TryGetNode(component.PortName, out PipeNode? port))
+ gasMixDict.Add(component.PortName, port.Air);
+ }
+ gasMixDict.Add(Name(uid), component.Air);
+ args.GasMixtures = gasMixDict;
+ }
}
}
diff --git a/Content.Server/UserInterface/ActivatableUIComponent.cs b/Content.Server/UserInterface/ActivatableUIComponent.cs
index 25b1a3ee67..85d5a187c9 100644
--- a/Content.Server/UserInterface/ActivatableUIComponent.cs
+++ b/Content.Server/UserInterface/ActivatableUIComponent.cs
@@ -49,6 +49,12 @@ namespace Content.Server.UserInterface
[DataField("allowSpectator")]
public bool AllowSpectator = true;
+ ///
+ /// Whether the UI should close when the item is deselected due to a hand swap or drop
+ ///
+ [DataField("closeOnHandDeselect")]
+ public bool CloseOnHandDeselect = true;
+
///
/// The client channel currently using the object, or null if there's none/not single user.
/// NOTE: DO NOT DIRECTLY SET, USE ActivatableUISystem.SetCurrentSingleUser
diff --git a/Content.Server/UserInterface/ActivatableUISystem.cs b/Content.Server/UserInterface/ActivatableUISystem.cs
index 40d86c48da..2bd60ac3ec 100644
--- a/Content.Server/UserInterface/ActivatableUISystem.cs
+++ b/Content.Server/UserInterface/ActivatableUISystem.cs
@@ -25,7 +25,7 @@ namespace Content.Server.UserInterface
SubscribeLocalEvent(OnActivate);
SubscribeLocalEvent(OnUseInHand);
- SubscribeLocalEvent((uid, aui, _) => CloseAll(uid, aui));
+ SubscribeLocalEvent(OnHandDeselected);
SubscribeLocalEvent((uid, aui, _) => CloseAll(uid, aui));
// *THIS IS A BLATANT WORKAROUND!* RATIONALE: Microwaves need it
SubscribeLocalEvent(OnParentChanged);
@@ -169,6 +169,14 @@ namespace Content.Server.UserInterface
if (!Resolve(uid, ref aui, false)) return;
aui.UserInterface?.CloseAll();
}
+
+ private void OnHandDeselected(EntityUid uid, ActivatableUIComponent? aui, HandDeselectedEvent args)
+ {
+ if (!Resolve(uid, ref aui, false)) return;
+ if (!aui.CloseOnHandDeselect)
+ return;
+ CloseAll(uid, aui);
+ }
}
public sealed class ActivatableUIOpenAttemptEvent : CancellableEntityEventArgs
diff --git a/Content.Shared/Atmos/Components/SharedGasAnalyzerComponent.cs b/Content.Shared/Atmos/Components/SharedGasAnalyzerComponent.cs
index bdc373ebeb..2a167bf124 100644
--- a/Content.Shared/Atmos/Components/SharedGasAnalyzerComponent.cs
+++ b/Content.Shared/Atmos/Components/SharedGasAnalyzerComponent.cs
@@ -6,29 +6,60 @@ namespace Content.Shared.Atmos.Components
[NetworkedComponent()]
public abstract class SharedGasAnalyzerComponent : Component
{
+
[Serializable, NetSerializable]
public enum GasAnalyzerUiKey
{
Key,
}
+ ///
+ /// Atmospheric data is gathered in the system and sent to the user
+ ///
[Serializable, NetSerializable]
- public sealed class GasAnalyzerBoundUserInterfaceState : BoundUserInterfaceState
+ public sealed class GasAnalyzerUserMessage : BoundUserInterfaceMessage
{
- public float Pressure;
- public float Temperature;
- public GasEntry[]? Gases;
+ public string DeviceName;
+ public EntityUid DeviceUid;
+ public bool DeviceFlipped;
public string? Error;
-
- public GasAnalyzerBoundUserInterfaceState(float pressure, float temperature, GasEntry[]? gases, string? error = null)
+ public GasMixEntry[] NodeGasMixes;
+ public GasAnalyzerUserMessage(GasMixEntry[] nodeGasMixes, string deviceName, EntityUid deviceUid, bool deviceFlipped, string? error = null)
{
- Pressure = pressure;
- Temperature = temperature;
- Gases = gases;
+ NodeGasMixes = nodeGasMixes;
+ DeviceName = deviceName;
+ DeviceUid = deviceUid;
+ DeviceFlipped = deviceFlipped;
Error = error;
}
}
+ ///
+ /// Contains information on a gas mix entry, turns into a tab in the UI
+ ///
+ [Serializable, NetSerializable]
+ public struct GasMixEntry
+ {
+ ///
+ /// Name of the tab in the UI
+ ///
+ public readonly string Name;
+ public readonly float Pressure;
+ public readonly float Temperature;
+ public readonly GasEntry[]? Gases;
+
+ public GasMixEntry(string name, float pressure, float temperature, GasEntry[]? gases = null)
+ {
+ Name = name;
+ Pressure = pressure;
+ Temperature = temperature;
+ Gases = gases;
+ }
+ }
+
+ ///
+ /// Individual gas entry data for populating the UI
+ ///
[Serializable, NetSerializable]
public struct GasEntry
{
@@ -54,43 +85,15 @@ namespace Content.Shared.Atmos.Components
}
[Serializable, NetSerializable]
- public sealed class GasAnalyzerRefreshMessage : BoundUserInterfaceMessage
+ public sealed class GasAnalyzerDisableMessage : BoundUserInterfaceMessage
{
- public GasAnalyzerRefreshMessage() {}
- }
-
- [Serializable, NetSerializable]
- public enum GasAnalyzerDanger
- {
- Nominal,
- Warning,
- Hazard
- }
-
- [Serializable, NetSerializable]
- public sealed class GasAnalyzerComponentState : ComponentState
- {
- public GasAnalyzerDanger Danger;
-
- public GasAnalyzerComponentState(GasAnalyzerDanger danger)
- {
- Danger = danger;
- }
+ public GasAnalyzerDisableMessage() {}
}
}
- [NetSerializable]
- [Serializable]
- public enum GasAnalyzerVisuals
+ [Serializable, NetSerializable]
+ public enum GasAnalyzerVisuals : byte
{
- VisualState,
- }
-
- [NetSerializable]
- [Serializable]
- public enum GasAnalyzerVisualState
- {
- Off,
- Working,
+ Enabled,
}
}
diff --git a/Resources/Locale/en-US/atmos/gas-analyzable-system.ftl b/Resources/Locale/en-US/atmos/gas-analyzable-system.ftl
deleted file mode 100644
index 01c8fc7d37..0000000000
--- a/Resources/Locale/en-US/atmos/gas-analyzable-system.ftl
+++ /dev/null
@@ -1,7 +0,0 @@
-gas-analyzable-system-internal-error-missing-component = Your gas analyzer whirrs for a while, then stops.
-gas-anlayzable-system-internal-error-no-gas-node = Your gas analyzer reads, "NO GAS FOUND".
-gas-analyzable-system-verb-name = Analyze
-gas-analyzable-system-verb-tooltip = Use a gas analyzer to examine the contents of this device.
-gas-analyzable-system-header = Your gas analyzer shows a list of statistics:
-gas-analyzable-system-statistics = Pressure: {PRESSURE($pressure)}
- Temperature: {$tempK}K ({$tempC}°C)
diff --git a/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl b/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl
index 3601c97c39..888e1bdf41 100644
--- a/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl
+++ b/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl
@@ -1,19 +1,28 @@
## Entity
-gas-analyzer-component-player-has-no-hands-message = You have no hands.
-gas-analyzer-component-need-gas-analyzer-in-hand-message = You need a Gas Analyzer in your hand!
gas-analyzer-component-player-cannot-reach-message = You can't reach there.
+gas-analyzer-shutoff = The gas analyzer shuts off.
## UI
gas-analyzer-window-name = Gas Analyzer
+gas-analyzer-window-environment-tab-label = Environment
+gas-analyzer-window-tab-title-capitalized = {CAPITALIZE($title)}
gas-analyzer-window-refresh-button = Refresh
+gas-analyzer-window-no-data = No Data
+gas-analyzer-window-no-gas-text = No Gases
gas-analyzer-window-error-text = Error: {$errorText}
-gas-analyzer-window-pressure-text = Pressure: {$pressure} kPa
-gas-analyzer-window-temperature-text = Temperature: {$tempK}K ({$tempC}°C)
-gas-analyzer-window-molality-text = {$mol} mol
-gas-analyzer-window-molality-percentage-text = {$gasName}: {$amount} mol ({$percentage}%)
+gas-analyzer-window-pressure-text = Pressure:
+gas-analyzer-window-pressure-val-text = {$pressure} kPa
+gas-analyzer-window-temperature-text = Temperature:
+gas-analyzer-window-temperature-val-text = {$tempK}K ({$tempC}°C)
+gas-analyzer-window-molarity-text = {$mol} mol ({$percentage}%)
+gas-analyzer-window-molarity-percentage-text = {$gasName}: {$amount} mol ({$percentage}%)
# Used for GasEntry.ToString()
gas-entry-info = {$gasName}: {$gasAmount} mol
-itemstatus-pressure-warn = Pressure: [color={$color}]{$danger}[/color]
\ No newline at end of file
+
+# overrides for trinary devices to have saner names
+gas-analyzer-window-text-inlet = Inlet
+gas-analyzer-window-text-outlet = Outlet
+gas-analyzer-window-text-filter = Filter
diff --git a/Resources/Prototypes/Entities/Objects/Specific/atmos.yml b/Resources/Prototypes/Entities/Objects/Specific/atmos.yml
index 0f944d26c4..2312a239cb 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/atmos.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/atmos.yml
@@ -12,6 +12,9 @@
netsync: false
- type: GasAnalyzer
- type: ActivatableUI
+ inHandsOnly: true
+ singleUser: true
+ closeOnHandDeselect: false
key: enum.GasAnalyzerUiKey.Key
- type: UserInterface
interfaces:
@@ -20,10 +23,10 @@
- type: Appearance
- type: GenericVisualizer
visuals:
- enum.GasAnalyzerVisuals.VisualState:
- analyzer:
- Off: { state: icon }
- Working: { state: working }
+ enum.GasAnalyzerVisuals.Enabled:
+ enabled:
+ True: { state: working }
+ False: { state: icon }
- type: Tag
tags:
- DroneUsable
diff --git a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml
index 2bda18bcee..a2e923cb5e 100644
--- a/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml
+++ b/Resources/Prototypes/Entities/Structures/Piping/Atmospherics/pipes.yml
@@ -48,7 +48,6 @@
range: 2
sound:
path: /Audio/Ambience/Objects/gas_hiss.ogg
- - type: GasAnalyzable
#Note: The PipeDirection of the PipeNode should be the south-facing version, because the entity starts at an angle of 0 (south)