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)