From 2e3365793c314702a5f49db0a1451ed4e1e6033f Mon Sep 17 00:00:00 2001 From: Mervill Date: Thu, 15 Aug 2024 07:45:13 -0700 Subject: [PATCH] Greatly improve the usability of the Gas Analyzer. (#30763) * greatly improve how the gas analyzer behaves * don't close the analyzer when the object goes out of range * cleanup * always switch to the device tab when a new device is analyzed * modern api part one * modern api part 2 * modern api part three * file scope namespace --- .../Atmos/UI/GasAnalyzerWindow.xaml.cs | 10 + .../Atmos/EntitySystems/GasAnalyzerSystem.cs | 411 +++++++++--------- .../EntitySystems/RadiationCollectorSystem.cs | 2 +- .../Atmos/Components/GasAnalyzerComponent.cs | 3 - .../en-US/atmos/gas-analyzer-component.ftl | 2 +- 5 files changed, 213 insertions(+), 215 deletions(-) diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs index b54af3a587..bb24da44e1 100644 --- a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs +++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs @@ -16,6 +16,8 @@ namespace Content.Client.Atmos.UI [GenerateTypedNameReferences] public sealed partial class GasAnalyzerWindow : DefaultWindow { + private NetEntity _currentEntity = NetEntity.Invalid; + public GasAnalyzerWindow() { RobustXamlLoader.Load(this); @@ -55,6 +57,13 @@ namespace Content.Client.Atmos.UI // Device Tab if (msg.NodeGasMixes.Length > 1) { + if (_currentEntity != msg.DeviceUid) + { + // when we get new device data switch to the device tab + CTabContainer.CurrentTab = 0; + _currentEntity = msg.DeviceUid; + } + CTabContainer.SetTabVisible(0, true); CTabContainer.SetTabTitle(0, Loc.GetString("gas-analyzer-window-tab-title-capitalized", ("title", msg.DeviceName))); // Set up Grid @@ -143,6 +152,7 @@ namespace Content.Client.Atmos.UI CTabContainer.SetTabVisible(0, false); CTabContainer.CurrentTab = 1; minSize = new Vector2(CEnvironmentMix.DesiredSize.X + 40, MinSize.Y); + _currentEntity = NetEntity.Invalid; } MinSize = minSize; diff --git a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs index 0f4490cd7e..81f0b96d02 100644 --- a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs +++ b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Server.Atmos; using Content.Server.Atmos.Components; using Content.Server.NodeContainer; using Content.Server.NodeContainer.Nodes; @@ -10,274 +9,266 @@ 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.GasAnalyzerComponent; -namespace Content.Server.Atmos.EntitySystems +namespace Content.Server.Atmos.EntitySystems; + +[UsedImplicitly] +public sealed class GasAnalyzerSystem : EntitySystem { - [UsedImplicitly] - 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!; + [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; + + /// + /// Minimum moles of a gas to be sent to the client. + /// + private const float UIMinMoles = 0.01f; + + public override void Initialize() { - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly AtmosphereSystem _atmo = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly UserInterfaceSystem _userInterface = default!; - [Dependency] private readonly TransformSystem _transform = default!; - - /// - /// Minimum moles of a gas to be sent to the client. - /// - private const float UIMinMoles = 0.01f; - - public override void Initialize() - { - base.Initialize(); + base.Initialize(); - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnDisabledMessage); - SubscribeLocalEvent(OnDropped); - SubscribeLocalEvent(OnUseInHand); - } + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnDisabledMessage); + SubscribeLocalEvent(OnDropped); + SubscribeLocalEvent(OnUseInHand); + } - public override void Update(float frameTime) + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var analyzer)) { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var analyzer)) - { - // Don't update every tick - analyzer.AccumulatedFrametime += frameTime; + // Don't update every tick + analyzer.AccumulatedFrametime += frameTime; - if (analyzer.AccumulatedFrametime < analyzer.UpdateInterval) - continue; + if (analyzer.AccumulatedFrametime < analyzer.UpdateInterval) + continue; - analyzer.AccumulatedFrametime -= analyzer.UpdateInterval; + analyzer.AccumulatedFrametime -= analyzer.UpdateInterval; - if (!UpdateAnalyzer(uid)) - RemCompDeferred(uid); - } + if (!UpdateAnalyzer(uid)) + RemCompDeferred(uid); } + } - /// - /// 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) + /// + /// Activates the analyzer when used in the world, scanning the target entity (if it exists) and the tile the analyzer is in + /// + private void OnAfterInteract(Entity entity, ref AfterInteractEvent args) + { + var target = args.Target; + if (target != null && !_interactionSystem.InRangeUnobstructed((args.User, null), (target.Value, null))) { - if (!args.CanReach) - { - _popup.PopupEntity(Loc.GetString("gas-analyzer-component-player-cannot-reach-message"), args.User, args.User); - return; - } - ActivateAnalyzer(uid, component, args.User, args.Target); - args.Handled = true; + target = null; // if the target is out of reach, invalidate it } + // always run the analyzer, regardless of weather or not there is a target + // since we can always show the local environment. + ActivateAnalyzer(entity, args.User, target); + args.Handled = true; + } - /// - /// 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) + /// + /// Activates the analyzer with no target, so it only scans the tile the user was on when activated + /// + private void OnUseInHand(Entity entity, ref UseInHandEvent args) + { + if (!entity.Comp.Enabled) { - ActivateAnalyzer(uid, component, args.User); - args.Handled = true; + ActivateAnalyzer(entity, args.User); } - - /// - /// Handles analyzer activation logic - /// - private void ActivateAnalyzer(EntityUid uid, GasAnalyzerComponent component, EntityUid user, EntityUid? target = null) + else { - if (!TryOpenUserInterface(uid, user, component)) - return; - - component.Target = target; - component.User = user; - if (target != null) - component.LastPosition = Transform(target.Value).Coordinates; - else - component.LastPosition = null; - component.Enabled = true; - Dirty(uid, component); - UpdateAppearance(uid, component); - EnsureComp(uid); - UpdateAnalyzer(uid, component); + DisableAnalyzer(entity, args.User); } + args.Handled = true; + } - /// - /// 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 var userId && component.Enabled) - _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId); - DisableAnalyzer(uid, component, args.User); - } + /// + /// Handles analyzer activation logic + /// + private void ActivateAnalyzer(Entity entity, EntityUid user, EntityUid? target = null) + { + if (!_userInterface.TryOpenUi(entity.Owner, GasAnalyzerUiKey.Key, user)) + return; + + entity.Comp.Target = target; + entity.Comp.User = user; + entity.Comp.Enabled = true; + Dirty(entity); + _appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled); + EnsureComp(entity.Owner); + UpdateAnalyzer(entity.Owner, entity.Comp); + } - /// - /// 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; + /// + /// Close the UI, turn the analyzer off, and don't update when it's dropped + /// + private void OnDropped(Entity entity, ref DroppedEvent args) + { + if (args.User is var userId && entity.Comp.Enabled) + _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId); + DisableAnalyzer(entity, args.User); + } + + /// + /// Closes the UI, sets the icon to off, and removes it from the update list + /// + private void DisableAnalyzer(Entity entity, EntityUid? user = null) + { + _userInterface.CloseUi(entity.Owner, GasAnalyzerUiKey.Key, user); - _userInterface.CloseUi(uid, GasAnalyzerUiKey.Key, user); + entity.Comp.Enabled = false; + Dirty(entity); + _appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled); + RemCompDeferred(entity.Owner); + } - component.Enabled = false; - Dirty(uid, component); - UpdateAppearance(uid, component); - RemCompDeferred(uid); - } + /// + /// Disables the analyzer when the user closes the UI + /// + private void OnDisabledMessage(Entity entity, ref GasAnalyzerDisableMessage message) + { + DisableAnalyzer(entity); + } - /// - /// Disables the analyzer when the user closes the UI - /// - private void OnDisabledMessage(EntityUid uid, GasAnalyzerComponent component, GasAnalyzerDisableMessage message) - { - DisableAnalyzer(uid, component); - } + /// + /// 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; - private bool TryOpenUserInterface(EntityUid uid, EntityUid user, GasAnalyzerComponent? component = null) + // check if the user has walked away from what they scanned + if (component.Target.HasValue) { - if (!Resolve(uid, ref component, false)) - return false; + // Listen! Even if you don't want the Gas Analyzer to work on moving targets, you should use + // this code to determine if the object is still generally in range so that the check is consistent with the code + // in OnAfterInteract() and also consistent with interaction code in general. + if (!_interactionSystem.InRangeUnobstructed((component.User, null), (component.Target.Value, null))) + { + if (component.User is { } userId && component.Enabled) + _popup.PopupEntity(Loc.GetString("gas-analyzer-object-out-of-range"), userId, userId); - return _userInterface.TryOpenUi(uid, GasAnalyzerUiKey.Key, user); + component.Target = null; + } } - /// - /// 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) + var gasMixList = new List(); + + // Fetch the environmental atmosphere around the scanner. This must be the first entry + var tileMixture = _atmo.GetContainingMixture(uid, true); + if (tileMixture != null) { - if (!Resolve(uid, ref component)) - return false; + gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, 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, 0f)); + } - if (!TryComp(component.User, out TransformComponent? xform)) + var deviceFlipped = false; + if (component.Target != null) + { + if (Deleted(component.Target)) { - DisableAnalyzer(uid, component); + component.Target = null; + DisableAnalyzer((uid, component), component.User); return false; } - // check if the user has walked away from what they scanned - var userPos = xform.Coordinates; - if (component.LastPosition.HasValue) - { - // Check if position is out of range => don't update and disable - if (!_transform.InRange(component.LastPosition.Value, userPos, SharedInteractionSystem.InteractionRange)) - { - if (component.User is { } userId && component.Enabled) - _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId); - DisableAnalyzer(uid, component, component.User); - return false; - } - } - - var gasMixList = new List(); + var validTarget = false; - // Fetch the environmental atmosphere around the scanner. This must be the first entry - var tileMixture = _atmo.GetContainingMixture(uid, true); - if (tileMixture != null) - { - gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, 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, 0f)); - } + // 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); - var deviceFlipped = false; - if (component.Target != null) + if (ev.GasMixtures != null) { - if (Deleted(component.Target)) - { - component.Target = null; - DisableAnalyzer(uid, component, component.User); - return false; - } - - // 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); - - if (ev.GasMixtures != null) + foreach (var mixes in ev.GasMixtures) { - foreach (var mixes in ev.GasMixtures) + if (mixes.Item2 != null) { - if (mixes.Item2 != null) - gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2))); + gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2))); + validTarget = true; } - - deviceFlipped = ev.DeviceFlipped; } - else + + deviceFlipped = ev.DeviceFlipped; + } + else + { + // No override, fetch manually, to handle flippable devices you must subscribe to GasAnalyzerScanEvent + if (TryComp(component.Target, out NodeContainerComponent? node)) { - // 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) { - foreach (var pair in node.Nodes) + if (pair.Value is PipeNode pipeNode) { - if (pair.Value is PipeNode pipeNode) - { - // check if the volume is zero for some reason so we don't divide by zero - if (pipeNode.Air.Volume == 0f) - continue; - // only display the gas in the analyzed pipe element, not the whole system - var pipeAir = pipeNode.Air.Clone(); - pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume); - pipeAir.Volume = pipeNode.Volume; - gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir))); - } + // check if the volume is zero for some reason so we don't divide by zero + if (pipeNode.Air.Volume == 0f) + continue; + // only display the gas in the analyzed pipe element, not the whole system + var pipeAir = pipeNode.Air.Clone(); + pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume); + pipeAir.Volume = pipeNode.Volume; + gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir))); + validTarget = true; } } } } - // Don't bother sending a UI message with no content, and stop updating I guess? - if (gasMixList.Count == 0) - return false; - - _userInterface.ServerSendUiMessage(uid, GasAnalyzerUiKey.Key, - new GasAnalyzerUserMessage(gasMixList.ToArray(), - component.Target != null ? Name(component.Target.Value) : string.Empty, - GetNetEntity(component.Target) ?? NetEntity.Invalid, - deviceFlipped)); - return true; + // If the target doesn't actually have any gas mixes to add, + // invalidate it as the target + if (!validTarget) + { + component.Target = null; + } } - /// - /// Sets the appearance based on the analyzers Enabled state - /// - private void UpdateAppearance(EntityUid uid, GasAnalyzerComponent analyzer) - { - _appearance.SetData(uid, GasAnalyzerVisuals.Enabled, analyzer.Enabled); - } + // Don't bother sending a UI message with no content, and stop updating I guess? + if (gasMixList.Count == 0) + return false; - /// - /// Generates a GasEntry array for a given GasMixture - /// - private GasEntry[] GenerateGasEntryArray(GasMixture? mixture) - { - var gases = new List(); + _userInterface.ServerSendUiMessage(uid, GasAnalyzerUiKey.Key, + new GasAnalyzerUserMessage(gasMixList.ToArray(), + component.Target != null ? Name(component.Target.Value) : string.Empty, + GetNetEntity(component.Target) ?? NetEntity.Invalid, + deviceFlipped)); + return true; + } - for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++) - { - var gas = _atmo.GetGas(i); + /// + /// 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?[i] <= UIMinMoles) - continue; + if (mixture?[i] <= UIMinMoles) + continue; - if (mixture != null) - { - var gasName = Loc.GetString(gas.Name); - gases.Add(new GasEntry(gasName, mixture[i], gas.Color)); - } + if (mixture != null) + { + var gasName = Loc.GetString(gas.Name); + gases.Add(new GasEntry(gasName, mixture[i], gas.Color)); } + } - var gasesOrdered = gases.OrderByDescending(gas => gas.Amount); + var gasesOrdered = gases.OrderByDescending(gas => gas.Amount); - return gasesOrdered.ToArray(); - } + return gasesOrdered.ToArray(); } } diff --git a/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs b/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs index c262988c86..3fab18b1b7 100644 --- a/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/RadiationCollectorSystem.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; -using Content.Server.Atmos; +using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.Components; using Content.Server.Popups; using Content.Server.Power.Components; diff --git a/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs b/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs index dec9516c01..c143e8cf85 100644 --- a/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs +++ b/Content.Shared/Atmos/Components/GasAnalyzerComponent.cs @@ -13,9 +13,6 @@ public sealed partial class GasAnalyzerComponent : Component [ViewVariables] public EntityUid User; - [ViewVariables(VVAccess.ReadWrite)] - public EntityCoordinates? LastPosition; - [DataField("enabled"), ViewVariables(VVAccess.ReadWrite)] public bool Enabled; diff --git a/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl b/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl index 652bb19cb5..a2cb5301b2 100644 --- a/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl +++ b/Resources/Locale/en-US/atmos/gas-analyzer-component.ftl @@ -1,6 +1,6 @@ ## Entity -gas-analyzer-component-player-cannot-reach-message = You can't reach there. +gas-analyzer-object-out-of-range = The object went out of range. gas-analyzer-shutoff = The gas analyzer shuts off. ## UI -- 2.52.0