AtmosMonitoringConsoleSystem.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. using Content.Server.Atmos.Components;
  2. using Content.Server.Atmos.Piping.Components;
  3. using Content.Server.DeviceNetwork.Components;
  4. using Content.Server.NodeContainer;
  5. using Content.Server.NodeContainer.EntitySystems;
  6. using Content.Server.NodeContainer.NodeGroups;
  7. using Content.Server.NodeContainer.Nodes;
  8. using Content.Server.Power.Components;
  9. using Content.Shared.Atmos;
  10. using Content.Shared.Atmos.Components;
  11. using Content.Shared.Atmos.Consoles;
  12. using Content.Shared.Labels.Components;
  13. using Content.Shared.Pinpointer;
  14. using Robust.Server.GameObjects;
  15. using Robust.Shared.Map;
  16. using Robust.Shared.Map.Components;
  17. using Robust.Shared.Timing;
  18. using System.Diagnostics.CodeAnalysis;
  19. using System.Linq;
  20. namespace Content.Server.Atmos.Consoles;
  21. public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleSystem
  22. {
  23. [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
  24. [Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
  25. [Dependency] private readonly IGameTiming _gameTiming = default!;
  26. // Private variables
  27. // Note: this data does not need to be saved
  28. private Dictionary<EntityUid, Dictionary<Vector2i, AtmosPipeChunk>> _gridAtmosPipeChunks = new();
  29. private float _updateTimer = 1.0f;
  30. // Constants
  31. private const float UpdateTime = 1.0f;
  32. private const int ChunkSize = 4;
  33. public override void Initialize()
  34. {
  35. base.Initialize();
  36. // Console events
  37. SubscribeLocalEvent<AtmosMonitoringConsoleComponent, ComponentInit>(OnConsoleInit);
  38. SubscribeLocalEvent<AtmosMonitoringConsoleComponent, AnchorStateChangedEvent>(OnConsoleAnchorChanged);
  39. SubscribeLocalEvent<AtmosMonitoringConsoleComponent, EntParentChangedMessage>(OnConsoleParentChanged);
  40. // Tracked device events
  41. SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, NodeGroupsRebuilt>(OnEntityNodeGroupsRebuilt);
  42. SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, AtmosPipeColorChangedEvent>(OnEntityPipeColorChanged);
  43. SubscribeLocalEvent<AtmosMonitoringConsoleDeviceComponent, EntityTerminatingEvent>(OnEntityShutdown);
  44. // Grid events
  45. SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
  46. }
  47. #region Event handling
  48. private void OnConsoleInit(EntityUid uid, AtmosMonitoringConsoleComponent component, ComponentInit args)
  49. {
  50. InitializeAtmosMonitoringConsole(uid, component);
  51. }
  52. private void OnConsoleAnchorChanged(EntityUid uid, AtmosMonitoringConsoleComponent component, AnchorStateChangedEvent args)
  53. {
  54. InitializeAtmosMonitoringConsole(uid, component);
  55. }
  56. private void OnConsoleParentChanged(EntityUid uid, AtmosMonitoringConsoleComponent component, EntParentChangedMessage args)
  57. {
  58. component.ForceFullUpdate = true;
  59. InitializeAtmosMonitoringConsole(uid, component);
  60. }
  61. private void OnEntityNodeGroupsRebuilt(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, NodeGroupsRebuilt args)
  62. {
  63. InitializeAtmosMonitoringDevice(uid, component);
  64. }
  65. private void OnEntityPipeColorChanged(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, AtmosPipeColorChangedEvent args)
  66. {
  67. InitializeAtmosMonitoringDevice(uid, component);
  68. }
  69. private void OnEntityShutdown(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component, EntityTerminatingEvent args)
  70. {
  71. ShutDownAtmosMonitoringEntity(uid, component);
  72. }
  73. private void OnGridSplit(ref GridSplitEvent args)
  74. {
  75. // Collect grids
  76. var allGrids = args.NewGrids.ToList();
  77. if (!allGrids.Contains(args.Grid))
  78. allGrids.Add(args.Grid);
  79. // Rebuild the pipe networks on the affected grids
  80. foreach (var ent in allGrids)
  81. {
  82. if (!TryComp<MapGridComponent>(ent, out var grid))
  83. continue;
  84. RebuildAtmosPipeGrid(ent, grid);
  85. }
  86. // Update atmos monitoring consoles that stand upon an updated grid
  87. var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
  88. while (query.MoveNext(out var ent, out var entConsole, out var entXform))
  89. {
  90. if (entXform.GridUid == null)
  91. continue;
  92. if (!allGrids.Contains(entXform.GridUid.Value))
  93. continue;
  94. InitializeAtmosMonitoringConsole(ent, entConsole);
  95. }
  96. }
  97. #endregion
  98. #region UI updates
  99. public override void Update(float frameTime)
  100. {
  101. base.Update(frameTime);
  102. _updateTimer += frameTime;
  103. if (_updateTimer >= UpdateTime)
  104. {
  105. _updateTimer -= UpdateTime;
  106. var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
  107. while (query.MoveNext(out var ent, out var entConsole, out var entXform))
  108. {
  109. if (entXform?.GridUid == null)
  110. continue;
  111. UpdateUIState(ent, entConsole, entXform);
  112. }
  113. }
  114. }
  115. public void UpdateUIState
  116. (EntityUid uid,
  117. AtmosMonitoringConsoleComponent component,
  118. TransformComponent xform)
  119. {
  120. if (!_userInterfaceSystem.IsUiOpen(uid, AtmosMonitoringConsoleUiKey.Key))
  121. return;
  122. var gridUid = xform.GridUid!.Value;
  123. if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
  124. return;
  125. if (!TryComp<GridAtmosphereComponent>(gridUid, out var atmosphere))
  126. return;
  127. // The grid must have a NavMapComponent to visualize the map in the UI
  128. EnsureComp<NavMapComponent>(gridUid);
  129. // Gathering data to be send to the client
  130. var atmosNetworks = new List<AtmosMonitoringConsoleEntry>();
  131. var query = AllEntityQuery<GasPipeSensorComponent, TransformComponent>();
  132. while (query.MoveNext(out var ent, out var entSensor, out var entXform))
  133. {
  134. if (entXform?.GridUid != xform.GridUid)
  135. continue;
  136. if (!entXform.Anchored)
  137. continue;
  138. var entry = CreateAtmosMonitoringConsoleEntry(ent, entXform);
  139. if (entry != null)
  140. atmosNetworks.Add(entry.Value);
  141. }
  142. // Set the UI state
  143. _userInterfaceSystem.SetUiState(uid, AtmosMonitoringConsoleUiKey.Key,
  144. new AtmosMonitoringConsoleBoundInterfaceState(atmosNetworks.ToArray()));
  145. }
  146. private AtmosMonitoringConsoleEntry? CreateAtmosMonitoringConsoleEntry(EntityUid uid, TransformComponent xform)
  147. {
  148. AtmosMonitoringConsoleEntry? entry = null;
  149. var netEnt = GetNetEntity(uid);
  150. var name = MetaData(uid).EntityName;
  151. var address = string.Empty;
  152. if (xform.GridUid == null)
  153. return null;
  154. if (!TryGettingFirstPipeNode(uid, out var pipeNode, out var netId) ||
  155. pipeNode == null ||
  156. netId == null)
  157. return null;
  158. var pipeColor = TryComp<AtmosPipeColorComponent>(uid, out var colorComponent) ? colorComponent.Color : Color.White;
  159. // Name the entity based on its label, if available
  160. if (TryComp<LabelComponent>(uid, out var label) && label.CurrentLabel != null)
  161. name = label.CurrentLabel;
  162. // Otherwise use its base name and network address
  163. else if (TryComp<DeviceNetworkComponent>(uid, out var deviceNet))
  164. address = deviceNet.Address;
  165. // Entry for unpowered devices
  166. if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPowerReceiver) && !apcPowerReceiver.Powered)
  167. {
  168. entry = new AtmosMonitoringConsoleEntry(netEnt, GetNetCoordinates(xform.Coordinates), netId.Value, name, address)
  169. {
  170. IsPowered = false,
  171. Color = pipeColor
  172. };
  173. return entry;
  174. }
  175. // Entry for powered devices
  176. var gasData = new Dictionary<Gas, float>();
  177. var isAirPresent = pipeNode.Air.TotalMoles > 0;
  178. if (isAirPresent)
  179. {
  180. foreach (var gas in Enum.GetValues<Gas>())
  181. {
  182. if (pipeNode.Air[(int)gas] > 0)
  183. gasData.Add(gas, pipeNode.Air[(int)gas] / pipeNode.Air.TotalMoles);
  184. }
  185. }
  186. entry = new AtmosMonitoringConsoleEntry(netEnt, GetNetCoordinates(xform.Coordinates), netId.Value, name, address)
  187. {
  188. TemperatureData = isAirPresent ? pipeNode.Air.Temperature : 0f,
  189. PressureData = pipeNode.Air.Pressure,
  190. TotalMolData = pipeNode.Air.TotalMoles,
  191. GasData = gasData,
  192. Color = pipeColor
  193. };
  194. return entry;
  195. }
  196. private Dictionary<NetEntity, AtmosDeviceNavMapData> GetAllAtmosDeviceNavMapData(EntityUid gridUid)
  197. {
  198. var atmosDeviceNavMapData = new Dictionary<NetEntity, AtmosDeviceNavMapData>();
  199. var query = AllEntityQuery<AtmosMonitoringConsoleDeviceComponent, TransformComponent>();
  200. while (query.MoveNext(out var ent, out var entComponent, out var entXform))
  201. {
  202. if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data))
  203. atmosDeviceNavMapData.Add(data.Value.NetEntity, data.Value);
  204. }
  205. return atmosDeviceNavMapData;
  206. }
  207. private bool TryGetAtmosDeviceNavMapData
  208. (EntityUid uid,
  209. AtmosMonitoringConsoleDeviceComponent component,
  210. TransformComponent xform,
  211. EntityUid gridUid,
  212. [NotNullWhen(true)] out AtmosDeviceNavMapData? device)
  213. {
  214. device = null;
  215. if (component.NavMapBlip == null)
  216. return false;
  217. if (xform.GridUid != gridUid)
  218. return false;
  219. if (!xform.Anchored)
  220. return false;
  221. var direction = xform.LocalRotation.GetCardinalDir();
  222. if (!TryGettingFirstPipeNode(uid, out var _, out var netId))
  223. netId = -1;
  224. var color = Color.White;
  225. if (TryComp<AtmosPipeColorComponent>(uid, out var atmosPipeColor))
  226. color = atmosPipeColor.Color;
  227. device = new AtmosDeviceNavMapData(GetNetEntity(uid), GetNetCoordinates(xform.Coordinates), netId.Value, component.NavMapBlip.Value, direction, color);
  228. return true;
  229. }
  230. #endregion
  231. #region Pipe net functions
  232. private void RebuildAtmosPipeGrid(EntityUid gridUid, MapGridComponent grid)
  233. {
  234. var allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
  235. // Adds all atmos pipes to the nav map via bit mask chunks
  236. var queryPipes = AllEntityQuery<AtmosPipeColorComponent, NodeContainerComponent, TransformComponent>();
  237. while (queryPipes.MoveNext(out var ent, out var entAtmosPipeColor, out var entNodeContainer, out var entXform))
  238. {
  239. if (entXform.GridUid != gridUid)
  240. continue;
  241. if (!entXform.Anchored)
  242. continue;
  243. var tile = _sharedMapSystem.GetTileRef(gridUid, grid, entXform.Coordinates);
  244. var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize);
  245. var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize);
  246. if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
  247. {
  248. chunk = new AtmosPipeChunk(chunkOrigin);
  249. allChunks[chunkOrigin] = chunk;
  250. }
  251. UpdateAtmosPipeChunk(ent, entNodeContainer, entAtmosPipeColor, GetTileIndex(relative), ref chunk);
  252. }
  253. // Add or update the chunks on the associated grid
  254. _gridAtmosPipeChunks[gridUid] = allChunks;
  255. // Update the consoles that are on the same grid
  256. var queryConsoles = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
  257. while (queryConsoles.MoveNext(out var ent, out var entConsole, out var entXform))
  258. {
  259. if (gridUid != entXform.GridUid)
  260. continue;
  261. entConsole.AtmosPipeChunks = allChunks;
  262. Dirty(ent, entConsole);
  263. }
  264. }
  265. private void RebuildSingleTileOfPipeNetwork(EntityUid gridUid, MapGridComponent grid, EntityCoordinates coords)
  266. {
  267. if (!_gridAtmosPipeChunks.TryGetValue(gridUid, out var allChunks))
  268. allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
  269. var tile = _sharedMapSystem.GetTileRef(gridUid, grid, coords);
  270. var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.GridIndices, ChunkSize);
  271. var relative = SharedMapSystem.GetChunkRelative(tile.GridIndices, ChunkSize);
  272. var tileIdx = GetTileIndex(relative);
  273. if (!allChunks.TryGetValue(chunkOrigin, out var chunk))
  274. chunk = new AtmosPipeChunk(chunkOrigin);
  275. // Remove all stale values for the tile
  276. foreach (var (index, atmosPipeData) in chunk.AtmosPipeData)
  277. {
  278. var mask = (ulong)SharedNavMapSystem.AllDirMask << tileIdx * SharedNavMapSystem.Directions;
  279. chunk.AtmosPipeData[index] = atmosPipeData & ~mask;
  280. }
  281. // Rebuild the tile's pipe data
  282. foreach (var ent in _sharedMapSystem.GetAnchoredEntities(gridUid, grid, coords))
  283. {
  284. if (!TryComp<AtmosPipeColorComponent>(ent, out var entAtmosPipeColor))
  285. continue;
  286. if (!TryComp<NodeContainerComponent>(ent, out var entNodeContainer))
  287. continue;
  288. UpdateAtmosPipeChunk(ent, entNodeContainer, entAtmosPipeColor, tileIdx, ref chunk);
  289. }
  290. // Add or update the chunk on the associated grid
  291. // Only the modified chunk will be sent to the client
  292. chunk.LastUpdate = _gameTiming.CurTick;
  293. allChunks[chunkOrigin] = chunk;
  294. _gridAtmosPipeChunks[gridUid] = allChunks;
  295. // Update the components of the monitoring consoles that are attached to the same grid
  296. var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
  297. while (query.MoveNext(out var ent, out var entConsole, out var entXform))
  298. {
  299. if (gridUid != entXform.GridUid)
  300. continue;
  301. entConsole.AtmosPipeChunks = allChunks;
  302. Dirty(ent, entConsole);
  303. }
  304. }
  305. private void UpdateAtmosPipeChunk(EntityUid uid, NodeContainerComponent nodeContainer, AtmosPipeColorComponent pipeColor, int tileIdx, ref AtmosPipeChunk chunk)
  306. {
  307. // Entities that are actively being deleted are not to be drawn
  308. if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating)
  309. return;
  310. foreach ((var id, var node) in nodeContainer.Nodes)
  311. {
  312. if (node is not PipeNode)
  313. continue;
  314. var pipeNode = (PipeNode)node;
  315. var netId = GetPipeNodeNetId(pipeNode);
  316. var pipeDirection = pipeNode.CurrentPipeDirection;
  317. chunk.AtmosPipeData.TryGetValue((netId, pipeColor.Color.ToHex()), out var atmosPipeData);
  318. atmosPipeData |= (ulong)pipeDirection << tileIdx * SharedNavMapSystem.Directions;
  319. chunk.AtmosPipeData[(netId, pipeColor.Color.ToHex())] = atmosPipeData;
  320. }
  321. }
  322. private bool TryGettingFirstPipeNode(EntityUid uid, [NotNullWhen(true)] out PipeNode? pipeNode, [NotNullWhen(true)] out int? netId)
  323. {
  324. pipeNode = null;
  325. netId = null;
  326. if (!TryComp<NodeContainerComponent>(uid, out var nodeContainer))
  327. return false;
  328. foreach (var node in nodeContainer.Nodes.Values)
  329. {
  330. if (node is PipeNode)
  331. {
  332. pipeNode = (PipeNode)node;
  333. netId = GetPipeNodeNetId(pipeNode);
  334. return true;
  335. }
  336. }
  337. return false;
  338. }
  339. private int GetPipeNodeNetId(PipeNode pipeNode)
  340. {
  341. if (pipeNode.NodeGroup is BaseNodeGroup)
  342. {
  343. var nodeGroup = (BaseNodeGroup)pipeNode.NodeGroup;
  344. return nodeGroup.NetId;
  345. }
  346. return -1;
  347. }
  348. #endregion
  349. #region Initialization functions
  350. private void InitializeAtmosMonitoringConsole(EntityUid uid, AtmosMonitoringConsoleComponent component)
  351. {
  352. var xform = Transform(uid);
  353. if (xform.GridUid == null)
  354. return;
  355. var grid = xform.GridUid.Value;
  356. if (!TryComp<MapGridComponent>(grid, out var map))
  357. return;
  358. component.AtmosDevices = GetAllAtmosDeviceNavMapData(grid);
  359. if (!_gridAtmosPipeChunks.TryGetValue(grid, out var chunks))
  360. {
  361. RebuildAtmosPipeGrid(grid, map);
  362. }
  363. else
  364. {
  365. component.AtmosPipeChunks = chunks;
  366. Dirty(uid, component);
  367. }
  368. }
  369. private void InitializeAtmosMonitoringDevice(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component)
  370. {
  371. // Rebuild tile
  372. var xform = Transform(uid);
  373. var gridUid = xform.GridUid;
  374. if (gridUid != null && TryComp<MapGridComponent>(gridUid, out var grid))
  375. RebuildSingleTileOfPipeNetwork(gridUid.Value, grid, xform.Coordinates);
  376. // Update blips on affected consoles
  377. if (component.NavMapBlip == null)
  378. return;
  379. var netEntity = EntityManager.GetNetEntity(uid);
  380. var query = AllEntityQuery<AtmosMonitoringConsoleComponent, TransformComponent>();
  381. while (query.MoveNext(out var ent, out var entConsole, out var entXform))
  382. {
  383. var isDirty = entConsole.AtmosDevices.Remove(netEntity);
  384. if (gridUid != null &&
  385. gridUid == entXform.GridUid &&
  386. xform.Anchored &&
  387. TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
  388. {
  389. entConsole.AtmosDevices.Add(netEntity, data.Value);
  390. isDirty = true;
  391. }
  392. if (isDirty)
  393. Dirty(ent, entConsole);
  394. }
  395. }
  396. private void ShutDownAtmosMonitoringEntity(EntityUid uid, AtmosMonitoringConsoleDeviceComponent component)
  397. {
  398. // Rebuild tile
  399. var xform = Transform(uid);
  400. var gridUid = xform.GridUid;
  401. if (gridUid != null && TryComp<MapGridComponent>(gridUid, out var grid))
  402. RebuildSingleTileOfPipeNetwork(gridUid.Value, grid, xform.Coordinates);
  403. // Update blips on affected consoles
  404. if (component.NavMapBlip == null)
  405. return;
  406. var netEntity = EntityManager.GetNetEntity(uid);
  407. var query = AllEntityQuery<AtmosMonitoringConsoleComponent>();
  408. while (query.MoveNext(out var ent, out var entConsole))
  409. {
  410. if (entConsole.AtmosDevices.Remove(netEntity))
  411. Dirty(ent, entConsole);
  412. }
  413. }
  414. #endregion
  415. private int GetTileIndex(Vector2i relativeTile)
  416. {
  417. return relativeTile.X * ChunkSize + relativeTile.Y;
  418. }
  419. }