AtmosMonitoringSystem.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. using Content.Server.Atmos.EntitySystems;
  2. using Content.Server.Atmos.Monitor.Components;
  3. using Content.Server.Atmos.Piping.Components;
  4. using Content.Server.Atmos.Piping.EntitySystems;
  5. using Content.Server.DeviceNetwork;
  6. using Content.Server.DeviceNetwork.Systems;
  7. using Content.Server.NodeContainer;
  8. using Content.Server.NodeContainer.EntitySystems;
  9. using Content.Server.NodeContainer.Nodes;
  10. using Content.Server.Power.Components;
  11. using Content.Server.Power.EntitySystems;
  12. using Content.Shared.Administration.Logs;
  13. using Content.Shared.Atmos;
  14. using Content.Shared.Atmos.Monitor;
  15. using Content.Shared.Atmos.Piping.Components;
  16. using Content.Shared.Database;
  17. using Content.Shared.DeviceNetwork;
  18. using Content.Shared.Power;
  19. using Content.Shared.Tag;
  20. using Robust.Shared.Prototypes;
  21. namespace Content.Server.Atmos.Monitor.Systems;
  22. // AtmosMonitorSystem. Grabs all the AtmosAlarmables connected
  23. // to it via local APC net, and starts sending updates of the
  24. // current atmosphere. Monitors fire (which always triggers as
  25. // a danger), and atmos (which triggers based on set thresholds).
  26. public sealed class AtmosMonitorSystem : EntitySystem
  27. {
  28. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  29. [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
  30. [Dependency] private readonly AtmosDeviceSystem _atmosDeviceSystem = default!;
  31. [Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
  32. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  33. [Dependency] private readonly NodeContainerSystem _nodeContainerSystem = default!;
  34. // Commands
  35. public const string AtmosMonitorSetThresholdCmd = "atmos_monitor_set_threshold";
  36. public const string AtmosMonitorSetAllThresholdsCmd = "atmos_monitor_set_all_thresholds";
  37. // Packet data
  38. public const string AtmosMonitorThresholdData = "atmos_monitor_threshold_data";
  39. public const string AtmosMonitorAllThresholdData = "atmos_monitor_all_threshold_data";
  40. public const string AtmosMonitorThresholdDataType = "atmos_monitor_threshold_type";
  41. public const string AtmosMonitorThresholdGasType = "atmos_monitor_threshold_gas";
  42. public override void Initialize()
  43. {
  44. SubscribeLocalEvent<AtmosMonitorComponent, ComponentStartup>(OnAtmosMonitorStartup);
  45. SubscribeLocalEvent<AtmosMonitorComponent, MapInitEvent>(OnMapInit);
  46. SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceUpdateEvent>(OnAtmosUpdate);
  47. SubscribeLocalEvent<AtmosMonitorComponent, TileFireEvent>(OnFireEvent);
  48. SubscribeLocalEvent<AtmosMonitorComponent, PowerChangedEvent>(OnPowerChangedEvent);
  49. SubscribeLocalEvent<AtmosMonitorComponent, BeforePacketSentEvent>(BeforePacketRecv);
  50. SubscribeLocalEvent<AtmosMonitorComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
  51. SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceDisabledEvent>(OnAtmosDeviceLeaveAtmosphere);
  52. SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceEnabledEvent>(OnAtmosDeviceEnterAtmosphere);
  53. }
  54. private void OnAtmosDeviceLeaveAtmosphere(EntityUid uid, AtmosMonitorComponent atmosMonitor, ref AtmosDeviceDisabledEvent args)
  55. {
  56. atmosMonitor.TileGas = null;
  57. }
  58. private void OnAtmosDeviceEnterAtmosphere(EntityUid uid, AtmosMonitorComponent atmosMonitor, ref AtmosDeviceEnabledEvent args)
  59. {
  60. if (atmosMonitor.MonitorsPipeNet && _nodeContainerSystem.TryGetNode<PipeNode>(uid, atmosMonitor.NodeNameMonitoredPipe, out var pipeNode))
  61. {
  62. atmosMonitor.TileGas = pipeNode.Air;
  63. return;
  64. }
  65. atmosMonitor.TileGas = _atmosphereSystem.GetContainingMixture(uid, true);
  66. }
  67. private void OnMapInit(EntityUid uid, AtmosMonitorComponent component, MapInitEvent args)
  68. {
  69. if (component.TemperatureThresholdId != null)
  70. {
  71. var proto = _prototypeManager.Index<AtmosAlarmThresholdPrototype>(component.TemperatureThresholdId);
  72. component.TemperatureThreshold ??= new(proto);
  73. }
  74. if (component.PressureThresholdId != null)
  75. {
  76. var proto = _prototypeManager.Index<AtmosAlarmThresholdPrototype>(component.PressureThresholdId);
  77. component.PressureThreshold ??= new(proto);
  78. }
  79. if (component.GasThresholdPrototypes == null)
  80. return;
  81. component.GasThresholds ??= new();
  82. foreach (var (gas, id) in component.GasThresholdPrototypes)
  83. {
  84. var proto = _prototypeManager.Index<AtmosAlarmThresholdPrototype>(id);
  85. component.GasThresholds.TryAdd(gas, new(proto));
  86. }
  87. }
  88. private void OnAtmosMonitorStartup(EntityUid uid, AtmosMonitorComponent component, ComponentStartup args)
  89. {
  90. if (!HasComp<ApcPowerReceiverComponent>(uid)
  91. && TryComp<AtmosDeviceComponent>(uid, out var atmosDeviceComponent))
  92. {
  93. _atmosDeviceSystem.LeaveAtmosphere((uid, atmosDeviceComponent));
  94. }
  95. }
  96. private void BeforePacketRecv(EntityUid uid, AtmosMonitorComponent component, BeforePacketSentEvent args)
  97. {
  98. if (!component.NetEnabled) args.Cancel();
  99. }
  100. private void OnPacketRecv(EntityUid uid, AtmosMonitorComponent component, DeviceNetworkPacketEvent args)
  101. {
  102. // sync the internal 'last alarm state' from
  103. // the other alarms, so that we can calculate
  104. // the highest network alarm state at any time
  105. if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd))
  106. {
  107. return;
  108. }
  109. switch (cmd)
  110. {
  111. case AtmosDeviceNetworkSystem.RegisterDevice:
  112. component.RegisteredDevices.Add(args.SenderAddress);
  113. break;
  114. case AtmosDeviceNetworkSystem.DeregisterDevice:
  115. component.RegisteredDevices.Remove(args.SenderAddress);
  116. break;
  117. case AtmosAlarmableSystem.ResetAll:
  118. Reset(uid);
  119. // Don't clear alarm states here.
  120. break;
  121. case AtmosMonitorSetThresholdCmd:
  122. if (args.Data.TryGetValue(AtmosMonitorThresholdData, out AtmosAlarmThreshold? thresholdData)
  123. && args.Data.TryGetValue(AtmosMonitorThresholdDataType, out AtmosMonitorThresholdType? thresholdType))
  124. {
  125. args.Data.TryGetValue(AtmosMonitorThresholdGasType, out Gas? gas);
  126. SetThreshold(uid, thresholdType.Value, thresholdData, gas);
  127. }
  128. break;
  129. case AtmosMonitorSetAllThresholdsCmd:
  130. if (args.Data.TryGetValue(AtmosMonitorAllThresholdData, out AtmosSensorData? allThresholdData))
  131. {
  132. SetAllThresholds(uid, allThresholdData);
  133. }
  134. break;
  135. case AtmosDeviceNetworkSystem.SyncData:
  136. var payload = new NetworkPayload();
  137. payload.Add(DeviceNetworkConstants.Command, AtmosDeviceNetworkSystem.SyncData);
  138. if (component.TileGas != null)
  139. {
  140. var gases = new Dictionary<Gas, float>();
  141. foreach (var gas in Enum.GetValues<Gas>())
  142. {
  143. gases.Add(gas, component.TileGas.GetMoles(gas));
  144. }
  145. payload.Add(AtmosDeviceNetworkSystem.SyncData, new AtmosSensorData(
  146. component.TileGas.Pressure,
  147. component.TileGas.Temperature,
  148. component.TileGas.TotalMoles,
  149. component.LastAlarmState,
  150. gases,
  151. component.PressureThreshold ?? new(),
  152. component.TemperatureThreshold ?? new(),
  153. component.GasThresholds ?? new()
  154. ));
  155. }
  156. _deviceNetSystem.QueuePacket(uid, args.SenderAddress, payload);
  157. Alert(uid, component.LastAlarmState);
  158. break;
  159. }
  160. }
  161. private void OnPowerChangedEvent(Entity<AtmosMonitorComponent> ent, ref PowerChangedEvent args)
  162. {
  163. if (TryComp<AtmosDeviceComponent>(ent, out var atmosDeviceComponent))
  164. {
  165. if (!args.Powered)
  166. {
  167. _atmosDeviceSystem.LeaveAtmosphere((ent, atmosDeviceComponent));
  168. }
  169. else
  170. {
  171. _atmosDeviceSystem.JoinAtmosphere((ent, atmosDeviceComponent));
  172. Alert(ent, ent.Comp.LastAlarmState);
  173. }
  174. }
  175. }
  176. private void OnFireEvent(EntityUid uid, AtmosMonitorComponent component, ref TileFireEvent args)
  177. {
  178. if (!this.IsPowered(uid, EntityManager))
  179. return;
  180. // if we're monitoring for atmos fire, then we make it similar to a smoke detector
  181. // and just outright trigger a danger event
  182. //
  183. // somebody else can reset it :sunglasses:
  184. if (component.MonitorFire
  185. && component.LastAlarmState != AtmosAlarmType.Danger)
  186. {
  187. component.TrippedThresholds.Add(AtmosMonitorThresholdType.Temperature);
  188. Alert(uid, AtmosAlarmType.Danger, null, component); // technically???
  189. }
  190. // only monitor state elevation so that stuff gets alarmed quicker during a fire,
  191. // let the atmos update loop handle when temperature starts to reach different
  192. // thresholds and different states than normal -> warning -> danger
  193. if (component.TemperatureThreshold != null
  194. && component.TemperatureThreshold.CheckThreshold(args.Temperature, out var temperatureState)
  195. && temperatureState > component.LastAlarmState)
  196. {
  197. component.TrippedThresholds.Add(AtmosMonitorThresholdType.Temperature);
  198. Alert(uid, AtmosAlarmType.Danger, null, component);
  199. }
  200. }
  201. private void OnAtmosUpdate(EntityUid uid, AtmosMonitorComponent component, ref AtmosDeviceUpdateEvent args)
  202. {
  203. if (!this.IsPowered(uid, EntityManager))
  204. return;
  205. if (args.Grid == null)
  206. return;
  207. // if we're not monitoring atmos, don't bother
  208. if (component.TemperatureThreshold == null
  209. && component.PressureThreshold == null
  210. && component.GasThresholds == null)
  211. return;
  212. // If monitoring a pipe network, get its most recent gas mixture
  213. if (component.MonitorsPipeNet && _nodeContainerSystem.TryGetNode<PipeNode>(uid, component.NodeNameMonitoredPipe, out var pipeNode))
  214. component.TileGas = pipeNode.Air;
  215. UpdateState(uid, component.TileGas, component);
  216. }
  217. // Update checks the current air if it exceeds thresholds of
  218. // any kind.
  219. //
  220. // If any threshold exceeds the other, that threshold
  221. // immediately replaces the current recorded state.
  222. //
  223. // If the threshold does not match the current state
  224. // of the monitor, it is set in the Alert call.
  225. private void UpdateState(EntityUid uid, GasMixture? air, AtmosMonitorComponent? monitor = null)
  226. {
  227. if (air == null) return;
  228. if (!Resolve(uid, ref monitor)) return;
  229. var state = AtmosAlarmType.Normal;
  230. HashSet<AtmosMonitorThresholdType> alarmTypes = new(monitor.TrippedThresholds);
  231. if (monitor.TemperatureThreshold != null
  232. && monitor.TemperatureThreshold.CheckThreshold(air.Temperature, out var temperatureState))
  233. {
  234. if (temperatureState > state)
  235. {
  236. state = temperatureState;
  237. alarmTypes.Add(AtmosMonitorThresholdType.Temperature);
  238. }
  239. else if (temperatureState == AtmosAlarmType.Normal)
  240. {
  241. alarmTypes.Remove(AtmosMonitorThresholdType.Temperature);
  242. }
  243. }
  244. if (monitor.PressureThreshold != null
  245. && monitor.PressureThreshold.CheckThreshold(air.Pressure, out var pressureState)
  246. )
  247. {
  248. if (pressureState > state)
  249. {
  250. state = pressureState;
  251. alarmTypes.Add(AtmosMonitorThresholdType.Pressure);
  252. }
  253. else if (pressureState == AtmosAlarmType.Normal)
  254. {
  255. alarmTypes.Remove(AtmosMonitorThresholdType.Pressure);
  256. }
  257. }
  258. if (monitor.GasThresholds != null)
  259. {
  260. var tripped = false;
  261. foreach (var (gas, threshold) in monitor.GasThresholds)
  262. {
  263. var gasRatio = air.GetMoles(gas) / air.TotalMoles;
  264. if (threshold.CheckThreshold(gasRatio, out var gasState)
  265. && gasState > state)
  266. {
  267. state = gasState;
  268. tripped = true;
  269. }
  270. }
  271. if (tripped)
  272. {
  273. alarmTypes.Add(AtmosMonitorThresholdType.Gas);
  274. }
  275. else
  276. {
  277. alarmTypes.Remove(AtmosMonitorThresholdType.Gas);
  278. }
  279. }
  280. // if the state of the current air doesn't match the last alarm state,
  281. // we update the state
  282. if (state != monitor.LastAlarmState || !alarmTypes.SetEquals(monitor.TrippedThresholds))
  283. {
  284. Alert(uid, state, alarmTypes, monitor);
  285. }
  286. }
  287. /// <summary>
  288. /// Alerts the network that the state of a monitor has changed.
  289. /// </summary>
  290. /// <param name="state">The alarm state to set this monitor to.</param>
  291. /// <param name="alarms">The alarms that caused this alarm state.</param>
  292. public void Alert(EntityUid uid, AtmosAlarmType state, HashSet<AtmosMonitorThresholdType>? alarms = null, AtmosMonitorComponent? monitor = null)
  293. {
  294. if (!Resolve(uid, ref monitor))
  295. return;
  296. monitor.LastAlarmState = state;
  297. monitor.TrippedThresholds = alarms ?? monitor.TrippedThresholds;
  298. BroadcastAlertPacket((uid, monitor));
  299. // TODO: Central system that grabs *all* alarms from wired network
  300. }
  301. /// <summary>
  302. /// Resets a single monitor's alarm.
  303. /// </summary>
  304. private void Reset(EntityUid uid)
  305. {
  306. Alert(uid, AtmosAlarmType.Normal);
  307. }
  308. /// <summary>
  309. /// Broadcasts an alert packet to all devices on the network,
  310. /// which consists of the current alarm types,
  311. /// the highest alarm currently cached by this monitor,
  312. /// and the current alarm state of the monitor (so other
  313. /// alarms can sync to it).
  314. /// </summary>
  315. /// <remarks>
  316. /// Alarmables use the highest alarm to ensure that a monitor's
  317. /// state doesn't override if the alarm is lower. The state
  318. /// is synced between monitors the moment a monitor sends out an alarm,
  319. /// or if it is explicitly synced (see ResetAll/Sync).
  320. /// </remarks>
  321. private void BroadcastAlertPacket(Entity<AtmosMonitorComponent> ent, TagComponent? tags = null)
  322. {
  323. var (owner, monitor) = ent;
  324. if (!monitor.NetEnabled)
  325. return;
  326. if (!Resolve(owner, ref tags, false))
  327. {
  328. return;
  329. }
  330. var payload = new NetworkPayload
  331. {
  332. [DeviceNetworkConstants.Command] = AtmosAlarmableSystem.AlertCmd,
  333. [DeviceNetworkConstants.CmdSetState] = monitor.LastAlarmState,
  334. [AtmosAlarmableSystem.AlertSource] = tags.Tags,
  335. [AtmosAlarmableSystem.AlertTypes] = monitor.TrippedThresholds
  336. };
  337. foreach (var addr in monitor.RegisteredDevices)
  338. {
  339. _deviceNetSystem.QueuePacket(owner, addr, payload);
  340. }
  341. }
  342. /// <summary>
  343. /// Set a monitor's threshold.
  344. /// </summary>
  345. /// <param name="type">The type of threshold to change.</param>
  346. /// <param name="threshold">Threshold data.</param>
  347. /// <param name="gas">Gas, if applicable.</param>
  348. public void SetThreshold(EntityUid uid, AtmosMonitorThresholdType type, AtmosAlarmThreshold threshold, Gas? gas = null, AtmosMonitorComponent? monitor = null)
  349. {
  350. if (!Resolve(uid, ref monitor))
  351. return;
  352. // Used for logging after the switch statement
  353. string logPrefix = "";
  354. string logValueSuffix = "";
  355. AtmosAlarmThreshold? logPreviousThreshold = null;
  356. switch (type)
  357. {
  358. case AtmosMonitorThresholdType.Pressure:
  359. logPrefix = "pressure";
  360. logValueSuffix = "kPa";
  361. logPreviousThreshold = monitor.PressureThreshold;
  362. monitor.PressureThreshold = threshold;
  363. break;
  364. case AtmosMonitorThresholdType.Temperature:
  365. logPrefix = "temperature";
  366. logValueSuffix = "K";
  367. logPreviousThreshold = monitor.TemperatureThreshold;
  368. monitor.TemperatureThreshold = threshold;
  369. break;
  370. case AtmosMonitorThresholdType.Gas:
  371. if (gas == null || monitor.GasThresholds == null)
  372. return;
  373. logPrefix = ((Gas) gas).ToString();
  374. logValueSuffix = "kPa";
  375. monitor.GasThresholds.TryGetValue((Gas) gas, out logPreviousThreshold);
  376. monitor.GasThresholds[(Gas) gas] = threshold;
  377. break;
  378. }
  379. // Admin log each change separately rather than logging the whole state
  380. if (logPreviousThreshold != null)
  381. {
  382. if (threshold.Ignore != logPreviousThreshold.Ignore)
  383. {
  384. string enabled = threshold.Ignore ? "disabled" : "enabled";
  385. _adminLogger.Add(
  386. LogType.AtmosDeviceSetting,
  387. LogImpact.Medium,
  388. $"{ToPrettyString(uid)} {logPrefix} thresholds {enabled}"
  389. );
  390. }
  391. foreach (var change in threshold.GetChanges(logPreviousThreshold))
  392. {
  393. if (change.Current.Enabled != change.Previous?.Enabled)
  394. {
  395. string enabled = change.Current.Enabled ? "enabled" : "disabled";
  396. _adminLogger.Add(
  397. LogType.AtmosDeviceSetting,
  398. LogImpact.Medium,
  399. $"{ToPrettyString(uid)} {logPrefix} {change.Type} {enabled}"
  400. );
  401. }
  402. if (change.Current.Value != change.Previous?.Value)
  403. {
  404. _adminLogger.Add(
  405. LogType.AtmosDeviceSetting,
  406. LogImpact.Medium,
  407. $"{ToPrettyString(uid)} {logPrefix} {change.Type} changed from {change.Previous?.Value} {logValueSuffix} to {change.Current.Value} {logValueSuffix}"
  408. );
  409. }
  410. }
  411. }
  412. }
  413. /// <summary>
  414. /// Sets all of a monitor's thresholds at once according to the incoming
  415. /// AtmosSensorData object's thresholds.
  416. /// </summary>
  417. /// <param name="uid">The entity's uid</param>
  418. /// <param name="allThresholdData">An AtmosSensorData object from which the thresholds will be loaded.</param>
  419. public void SetAllThresholds(EntityUid uid, AtmosSensorData allThresholdData)
  420. {
  421. SetThreshold(uid, AtmosMonitorThresholdType.Temperature, allThresholdData.TemperatureThreshold);
  422. SetThreshold(uid, AtmosMonitorThresholdType.Pressure, allThresholdData.PressureThreshold);
  423. foreach (var gas in Enum.GetValues<Gas>())
  424. {
  425. SetThreshold(uid, AtmosMonitorThresholdType.Gas, allThresholdData.GasThresholds[gas], gas);
  426. }
  427. }
  428. }