using System.Linq;
-using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
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!;
+
+ /// <summary>
+ /// Minimum moles of a gas to be sent to the client.
+ /// </summary>
+ 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!;
-
- /// <summary>
- /// Minimum moles of a gas to be sent to the client.
- /// </summary>
- private const float UIMinMoles = 0.01f;
-
- public override void Initialize()
- {
- base.Initialize();
+ base.Initialize();
- SubscribeLocalEvent<GasAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
- SubscribeLocalEvent<GasAnalyzerComponent, GasAnalyzerDisableMessage>(OnDisabledMessage);
- SubscribeLocalEvent<GasAnalyzerComponent, DroppedEvent>(OnDropped);
- SubscribeLocalEvent<GasAnalyzerComponent, UseInHandEvent>(OnUseInHand);
- }
+ SubscribeLocalEvent<GasAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
+ SubscribeLocalEvent<GasAnalyzerComponent, GasAnalyzerDisableMessage>(OnDisabledMessage);
+ SubscribeLocalEvent<GasAnalyzerComponent, DroppedEvent>(OnDropped);
+ SubscribeLocalEvent<GasAnalyzerComponent, UseInHandEvent>(OnUseInHand);
+ }
- public override void Update(float frameTime)
+ public override void Update(float frameTime)
+ {
+ var query = EntityQueryEnumerator<ActiveGasAnalyzerComponent>();
+ while (query.MoveNext(out var uid, out var analyzer))
{
- var query = EntityQueryEnumerator<ActiveGasAnalyzerComponent>();
- 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<ActiveGasAnalyzerComponent>(uid);
- }
+ if (!UpdateAnalyzer(uid))
+ RemCompDeferred<ActiveGasAnalyzerComponent>(uid);
}
+ }
- /// <summary>
- /// Activates the analyzer when used in the world, scanning either the target entity or the tile clicked
- /// </summary>
- private void OnAfterInteract(EntityUid uid, GasAnalyzerComponent component, AfterInteractEvent args)
+ /// <summary>
+ /// Activates the analyzer when used in the world, scanning the target entity (if it exists) and the tile the analyzer is in
+ /// </summary>
+ private void OnAfterInteract(Entity<GasAnalyzerComponent> 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;
+ }
- /// <summary>
- /// Activates the analyzer with no target, so it only scans the tile the user was on when activated
- /// </summary>
- private void OnUseInHand(EntityUid uid, GasAnalyzerComponent component, UseInHandEvent args)
+ /// <summary>
+ /// Activates the analyzer with no target, so it only scans the tile the user was on when activated
+ /// </summary>
+ private void OnUseInHand(Entity<GasAnalyzerComponent> entity, ref UseInHandEvent args)
+ {
+ if (!entity.Comp.Enabled)
{
- ActivateAnalyzer(uid, component, args.User);
- args.Handled = true;
+ ActivateAnalyzer(entity, args.User);
}
-
- /// <summary>
- /// Handles analyzer activation logic
- /// </summary>
- 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<ActiveGasAnalyzerComponent>(uid);
- UpdateAnalyzer(uid, component);
+ DisableAnalyzer(entity, args.User);
}
+ args.Handled = true;
+ }
- /// <summary>
- /// Close the UI, turn the analyzer off, and don't update when it's dropped
- /// </summary>
- 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);
- }
+ /// <summary>
+ /// Handles analyzer activation logic
+ /// </summary>
+ private void ActivateAnalyzer(Entity<GasAnalyzerComponent> 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<ActiveGasAnalyzerComponent>(entity.Owner);
+ UpdateAnalyzer(entity.Owner, entity.Comp);
+ }
- /// <summary>
- /// Closes the UI, sets the icon to off, and removes it from the update list
- /// </summary>
- private void DisableAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null, EntityUid? user = null)
- {
- if (!Resolve(uid, ref component))
- return;
+ /// <summary>
+ /// Close the UI, turn the analyzer off, and don't update when it's dropped
+ /// </summary>
+ private void OnDropped(Entity<GasAnalyzerComponent> 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);
+ }
+
+ /// <summary>
+ /// Closes the UI, sets the icon to off, and removes it from the update list
+ /// </summary>
+ private void DisableAnalyzer(Entity<GasAnalyzerComponent> 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<ActiveGasAnalyzerComponent>(entity.Owner);
+ }
- component.Enabled = false;
- Dirty(uid, component);
- UpdateAppearance(uid, component);
- RemCompDeferred<ActiveGasAnalyzerComponent>(uid);
- }
+ /// <summary>
+ /// Disables the analyzer when the user closes the UI
+ /// </summary>
+ private void OnDisabledMessage(Entity<GasAnalyzerComponent> entity, ref GasAnalyzerDisableMessage message)
+ {
+ DisableAnalyzer(entity);
+ }
- /// <summary>
- /// Disables the analyzer when the user closes the UI
- /// </summary>
- private void OnDisabledMessage(EntityUid uid, GasAnalyzerComponent component, GasAnalyzerDisableMessage message)
- {
- DisableAnalyzer(uid, component);
- }
+ /// <summary>
+ /// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button
+ /// </summary>
+ 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;
+ }
}
- /// <summary>
- /// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button
- /// </summary>
- private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null)
+ var gasMixList = new List<GasMixEntry>();
+
+ // 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<GasMixEntry>();
+ 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;
+ }
}
- /// <summary>
- /// Sets the appearance based on the analyzers Enabled state
- /// </summary>
- 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;
- /// <summary>
- /// Generates a GasEntry array for a given GasMixture
- /// </summary>
- private GasEntry[] GenerateGasEntryArray(GasMixture? mixture)
- {
- var gases = new List<GasEntry>();
+ _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);
+ /// <summary>
+ /// Generates a GasEntry array for a given GasMixture
+ /// </summary>
+ private GasEntry[] GenerateGasEntryArray(GasMixture? mixture)
+ {
+ var gases = new List<GasEntry>();
+
+ 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();
}
}