GasAnalyzerSystem.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. using System.Linq;
  2. using Content.Server.Atmos.Components;
  3. using Content.Server.NodeContainer;
  4. using Content.Server.NodeContainer.Nodes;
  5. using Content.Server.Popups;
  6. using Content.Shared.Atmos;
  7. using Content.Shared.Atmos.Components;
  8. using Content.Shared.Interaction;
  9. using Content.Shared.Interaction.Events;
  10. using JetBrains.Annotations;
  11. using Robust.Server.GameObjects;
  12. using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
  13. namespace Content.Server.Atmos.EntitySystems;
  14. [UsedImplicitly]
  15. public sealed class GasAnalyzerSystem : EntitySystem
  16. {
  17. [Dependency] private readonly PopupSystem _popup = default!;
  18. [Dependency] private readonly AtmosphereSystem _atmo = default!;
  19. [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
  20. [Dependency] private readonly UserInterfaceSystem _userInterface = default!;
  21. [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
  22. /// <summary>
  23. /// Minimum moles of a gas to be sent to the client.
  24. /// </summary>
  25. private const float UIMinMoles = 0.01f;
  26. public override void Initialize()
  27. {
  28. base.Initialize();
  29. SubscribeLocalEvent<GasAnalyzerComponent, AfterInteractEvent>(OnAfterInteract);
  30. SubscribeLocalEvent<GasAnalyzerComponent, GasAnalyzerDisableMessage>(OnDisabledMessage);
  31. SubscribeLocalEvent<GasAnalyzerComponent, DroppedEvent>(OnDropped);
  32. SubscribeLocalEvent<GasAnalyzerComponent, UseInHandEvent>(OnUseInHand);
  33. }
  34. public override void Update(float frameTime)
  35. {
  36. var query = EntityQueryEnumerator<ActiveGasAnalyzerComponent>();
  37. while (query.MoveNext(out var uid, out var analyzer))
  38. {
  39. // Don't update every tick
  40. analyzer.AccumulatedFrametime += frameTime;
  41. if (analyzer.AccumulatedFrametime < analyzer.UpdateInterval)
  42. continue;
  43. analyzer.AccumulatedFrametime -= analyzer.UpdateInterval;
  44. if (!UpdateAnalyzer(uid))
  45. RemCompDeferred<ActiveGasAnalyzerComponent>(uid);
  46. }
  47. }
  48. /// <summary>
  49. /// Activates the analyzer when used in the world, scanning the target entity (if it exists) and the tile the analyzer is in
  50. /// </summary>
  51. private void OnAfterInteract(Entity<GasAnalyzerComponent> entity, ref AfterInteractEvent args)
  52. {
  53. var target = args.Target;
  54. if (target != null && !_interactionSystem.InRangeUnobstructed((args.User, null), (target.Value, null)))
  55. {
  56. target = null; // if the target is out of reach, invalidate it
  57. }
  58. // always run the analyzer, regardless of weather or not there is a target
  59. // since we can always show the local environment.
  60. ActivateAnalyzer(entity, args.User, target);
  61. args.Handled = true;
  62. }
  63. /// <summary>
  64. /// Activates the analyzer with no target, so it only scans the tile the user was on when activated
  65. /// </summary>
  66. private void OnUseInHand(Entity<GasAnalyzerComponent> entity, ref UseInHandEvent args)
  67. {
  68. // Not checking for Handled because ActivatableUISystem already marks it as such.
  69. if (!entity.Comp.Enabled)
  70. ActivateAnalyzer(entity, args.User);
  71. else
  72. DisableAnalyzer(entity, args.User);
  73. args.Handled = true;
  74. }
  75. /// <summary>
  76. /// Handles analyzer activation logic
  77. /// </summary>
  78. private void ActivateAnalyzer(Entity<GasAnalyzerComponent> entity, EntityUid user, EntityUid? target = null)
  79. {
  80. if (!_userInterface.TryOpenUi(entity.Owner, GasAnalyzerUiKey.Key, user))
  81. return;
  82. entity.Comp.Target = target;
  83. entity.Comp.User = user;
  84. entity.Comp.Enabled = true;
  85. Dirty(entity);
  86. _appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled);
  87. EnsureComp<ActiveGasAnalyzerComponent>(entity.Owner);
  88. UpdateAnalyzer(entity.Owner, entity.Comp);
  89. }
  90. /// <summary>
  91. /// Close the UI, turn the analyzer off, and don't update when it's dropped
  92. /// </summary>
  93. private void OnDropped(Entity<GasAnalyzerComponent> entity, ref DroppedEvent args)
  94. {
  95. if (args.User is var userId && entity.Comp.Enabled)
  96. _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
  97. DisableAnalyzer(entity, args.User);
  98. }
  99. /// <summary>
  100. /// Closes the UI, sets the icon to off, and removes it from the update list
  101. /// </summary>
  102. private void DisableAnalyzer(Entity<GasAnalyzerComponent> entity, EntityUid? user = null)
  103. {
  104. _userInterface.CloseUi(entity.Owner, GasAnalyzerUiKey.Key, user);
  105. entity.Comp.Enabled = false;
  106. Dirty(entity);
  107. _appearance.SetData(entity.Owner, GasAnalyzerVisuals.Enabled, entity.Comp.Enabled);
  108. RemCompDeferred<ActiveGasAnalyzerComponent>(entity.Owner);
  109. }
  110. /// <summary>
  111. /// Disables the analyzer when the user closes the UI
  112. /// </summary>
  113. private void OnDisabledMessage(Entity<GasAnalyzerComponent> entity, ref GasAnalyzerDisableMessage message)
  114. {
  115. DisableAnalyzer(entity);
  116. }
  117. /// <summary>
  118. /// Fetches fresh data for the analyzer. Should only be called by Update or when the user requests an update via refresh button
  119. /// </summary>
  120. private bool UpdateAnalyzer(EntityUid uid, GasAnalyzerComponent? component = null)
  121. {
  122. if (!Resolve(uid, ref component))
  123. return false;
  124. // check if the user has walked away from what they scanned
  125. if (component.Target.HasValue)
  126. {
  127. // Listen! Even if you don't want the Gas Analyzer to work on moving targets, you should use
  128. // this code to determine if the object is still generally in range so that the check is consistent with the code
  129. // in OnAfterInteract() and also consistent with interaction code in general.
  130. if (!_interactionSystem.InRangeUnobstructed((component.User, null), (component.Target.Value, null)))
  131. {
  132. if (component.User is { } userId && component.Enabled)
  133. _popup.PopupEntity(Loc.GetString("gas-analyzer-object-out-of-range"), userId, userId);
  134. component.Target = null;
  135. }
  136. }
  137. var gasMixList = new List<GasMixEntry>();
  138. // Fetch the environmental atmosphere around the scanner. This must be the first entry
  139. var tileMixture = _atmo.GetContainingMixture(uid, true);
  140. if (tileMixture != null)
  141. {
  142. gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature,
  143. GenerateGasEntryArray(tileMixture)));
  144. }
  145. else
  146. {
  147. // No gases were found
  148. gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f));
  149. }
  150. var deviceFlipped = false;
  151. if (component.Target != null)
  152. {
  153. if (Deleted(component.Target))
  154. {
  155. component.Target = null;
  156. DisableAnalyzer((uid, component), component.User);
  157. return false;
  158. }
  159. var validTarget = false;
  160. // gas analyzed was used on an entity, try to request gas data via event for override
  161. var ev = new GasAnalyzerScanEvent();
  162. RaiseLocalEvent(component.Target.Value, ev);
  163. if (ev.GasMixtures != null)
  164. {
  165. foreach (var mixes in ev.GasMixtures)
  166. {
  167. if (mixes.Item2 != null)
  168. {
  169. gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2)));
  170. validTarget = true;
  171. }
  172. }
  173. deviceFlipped = ev.DeviceFlipped;
  174. }
  175. else
  176. {
  177. // No override, fetch manually, to handle flippable devices you must subscribe to GasAnalyzerScanEvent
  178. if (TryComp(component.Target, out NodeContainerComponent? node))
  179. {
  180. foreach (var pair in node.Nodes)
  181. {
  182. if (pair.Value is PipeNode pipeNode)
  183. {
  184. // check if the volume is zero for some reason so we don't divide by zero
  185. if (pipeNode.Air.Volume == 0f)
  186. continue;
  187. // only display the gas in the analyzed pipe element, not the whole system
  188. var pipeAir = pipeNode.Air.Clone();
  189. pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume);
  190. pipeAir.Volume = pipeNode.Volume;
  191. gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir)));
  192. validTarget = true;
  193. }
  194. }
  195. }
  196. }
  197. // If the target doesn't actually have any gas mixes to add,
  198. // invalidate it as the target
  199. if (!validTarget)
  200. {
  201. component.Target = null;
  202. }
  203. }
  204. // Don't bother sending a UI message with no content, and stop updating I guess?
  205. if (gasMixList.Count == 0)
  206. return false;
  207. _userInterface.ServerSendUiMessage(uid, GasAnalyzerUiKey.Key,
  208. new GasAnalyzerUserMessage(gasMixList.ToArray(),
  209. component.Target != null ? Name(component.Target.Value) : string.Empty,
  210. GetNetEntity(component.Target) ?? NetEntity.Invalid,
  211. deviceFlipped));
  212. return true;
  213. }
  214. /// <summary>
  215. /// Generates a GasEntry array for a given GasMixture
  216. /// </summary>
  217. private GasEntry[] GenerateGasEntryArray(GasMixture? mixture)
  218. {
  219. var gases = new List<GasEntry>();
  220. for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
  221. {
  222. var gas = _atmo.GetGas(i);
  223. if (mixture?[i] <= UIMinMoles)
  224. continue;
  225. if (mixture != null)
  226. {
  227. var gasName = Loc.GetString(gas.Name);
  228. gases.Add(new GasEntry(gasName, mixture[i], gas.Color));
  229. }
  230. }
  231. var gasesOrdered = gases.OrderByDescending(gas => gas.Amount);
  232. return gasesOrdered.ToArray();
  233. }
  234. }
  235. /// <summary>
  236. /// Raised when the analyzer is used. An atmospherics device that does not rely on a NodeContainer or
  237. /// wishes to override the default analyzer behaviour of fetching all nodes in the attached NodeContainer
  238. /// should subscribe to this and return the GasMixtures as desired. A device that is flippable should subscribe
  239. /// to this event to report if it is flipped or not. See GasFilterSystem or GasMixerSystem for an example.
  240. /// </summary>
  241. public sealed class GasAnalyzerScanEvent : EntityEventArgs
  242. {
  243. /// <summary>
  244. /// The string is for the name (ex "pipe", "inlet", "filter"), GasMixture for the corresponding gas mix. Add all mixes that should be reported when scanned.
  245. /// </summary>
  246. public List<(string, GasMixture?)>? GasMixtures;
  247. /// <summary>
  248. /// If the device is flipped. Flipped is defined as when the inline input is 90 degrees CW to the side input
  249. /// </summary>
  250. public bool DeviceFlipped;
  251. }