1
0

AtmosAlertsComputerWindow.xaml.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. using Content.Client.Message;
  2. using Content.Client.Pinpointer.UI;
  3. using Content.Client.Stylesheets;
  4. using Content.Client.UserInterface.Controls;
  5. using Content.Shared.Atmos.Components;
  6. using Content.Shared.Atmos.Monitor;
  7. using Content.Shared.Pinpointer;
  8. using Robust.Client.AutoGenerated;
  9. using Robust.Client.GameObjects;
  10. using Robust.Client.UserInterface;
  11. using Robust.Client.UserInterface.Controls;
  12. using Robust.Client.UserInterface.XAML;
  13. using Robust.Shared.Map;
  14. using Robust.Shared.Timing;
  15. using Robust.Shared.Utility;
  16. using System.Diagnostics.CodeAnalysis;
  17. using System.Linq;
  18. namespace Content.Client.Atmos.Consoles;
  19. [GenerateTypedNameReferences]
  20. public sealed partial class AtmosAlertsComputerWindow : FancyWindow
  21. {
  22. private readonly IEntityManager _entManager;
  23. private readonly SpriteSystem _spriteSystem;
  24. private readonly SharedNavMapSystem _navMapSystem;
  25. private EntityUid? _owner;
  26. private NetEntity? _trackedEntity;
  27. private AtmosAlertsComputerEntry[]? _airAlarms = null;
  28. private AtmosAlertsComputerEntry[]? _fireAlarms = null;
  29. private IEnumerable<AtmosAlertsComputerEntry>? _allAlarms = null;
  30. private IEnumerable<AtmosAlertsComputerEntry>? _activeAlarms = null;
  31. private Dictionary<NetEntity, float> _deviceSilencingProgress = new();
  32. public event Action<NetEntity?>? SendFocusChangeMessageAction;
  33. public event Action<NetEntity, bool>? SendDeviceSilencedMessageAction;
  34. private bool _autoScrollActive = false;
  35. private bool _autoScrollAwaitsUpdate = false;
  36. private const float SilencingDuration = 2.5f;
  37. // Colors
  38. private Color _wallColor = new Color(64, 64, 64);
  39. private Color _tileColor = new Color(28, 28, 28);
  40. private Color _monitorBlipColor = Color.Cyan;
  41. private Color _untrackedEntColor = Color.DimGray;
  42. private Color _regionBaseColor = new Color(154, 154, 154);
  43. private Color _inactiveColor = StyleNano.DisabledFore;
  44. private Color _statusTextColor = StyleNano.GoodGreenFore;
  45. private Color _goodColor = Color.LimeGreen;
  46. private Color _warningColor = new Color(255, 182, 72);
  47. private Color _dangerColor = new Color(255, 67, 67);
  48. public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner)
  49. {
  50. RobustXamlLoader.Load(this);
  51. _entManager = IoCManager.Resolve<IEntityManager>();
  52. _spriteSystem = _entManager.System<SpriteSystem>();
  53. _navMapSystem = _entManager.System<SharedNavMapSystem>();
  54. // Pass the owner to nav map
  55. _owner = owner;
  56. NavMap.Owner = _owner;
  57. // Set nav map colors
  58. NavMap.WallColor = _wallColor;
  59. NavMap.TileColor = _tileColor;
  60. // Set nav map grid uid
  61. var stationName = Loc.GetString("atmos-alerts-window-unknown-location");
  62. if (_entManager.TryGetComponent<TransformComponent>(owner, out var xform))
  63. {
  64. NavMap.MapUid = xform.GridUid;
  65. // Assign station name
  66. if (_entManager.TryGetComponent<MetaDataComponent>(xform.GridUid, out var stationMetaData))
  67. stationName = stationMetaData.EntityName;
  68. var msg = new FormattedMessage();
  69. msg.TryAddMarkup(Loc.GetString("atmos-alerts-window-station-name", ("stationName", stationName)), out _);
  70. StationName.SetMessage(msg);
  71. }
  72. else
  73. {
  74. StationName.SetMessage(stationName);
  75. NavMap.Visible = false;
  76. }
  77. // Set trackable entity selected action
  78. NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
  79. // Update nav map
  80. NavMap.ForceNavMapUpdate();
  81. // Set tab container headers
  82. MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts"));
  83. MasterTabContainer.SetTabTitle(1, Loc.GetString("atmos-alerts-window-tab-air-alarms"));
  84. MasterTabContainer.SetTabTitle(2, Loc.GetString("atmos-alerts-window-tab-fire-alarms"));
  85. // Set UI toggles
  86. ShowInactiveAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowInactiveAlarms, AtmosAlarmType.Invalid);
  87. ShowNormalAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowNormalAlarms, AtmosAlarmType.Normal);
  88. ShowWarningAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowWarningAlarms, AtmosAlarmType.Warning);
  89. ShowDangerAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowDangerAlarms, AtmosAlarmType.Danger);
  90. // Set atmos monitoring message action
  91. SendFocusChangeMessageAction += userInterface.SendFocusChangeMessage;
  92. SendDeviceSilencedMessageAction += userInterface.SendDeviceSilencedMessage;
  93. }
  94. #region Toggle handling
  95. private void OnShowAlarmsToggled(CheckBox toggle, AtmosAlarmType toggledAlarmState)
  96. {
  97. if (_owner == null)
  98. return;
  99. if (!_entManager.TryGetComponent<AtmosAlertsComputerComponent>(_owner.Value, out var console))
  100. return;
  101. foreach (var device in console.AtmosDevices)
  102. {
  103. var alarmState = GetAlarmState(device.NetEntity);
  104. if (toggledAlarmState != alarmState)
  105. continue;
  106. if (toggle.Pressed)
  107. AddTrackedEntityToNavMap(device, alarmState);
  108. else
  109. NavMap.TrackedEntities.Remove(device.NetEntity);
  110. }
  111. }
  112. private void OnSilenceAlertsToggled(NetEntity netEntity, bool toggleState)
  113. {
  114. if (!_entManager.TryGetComponent<AtmosAlertsComputerComponent>(_owner, out var console))
  115. return;
  116. if (toggleState)
  117. _deviceSilencingProgress[netEntity] = SilencingDuration;
  118. else
  119. _deviceSilencingProgress.Remove(netEntity);
  120. foreach (AtmosAlarmEntryContainer entryContainer in AlertsTable.Children)
  121. {
  122. if (entryContainer.NetEntity == netEntity)
  123. entryContainer.SilenceAlarmProgressBar.Visible = toggleState;
  124. }
  125. SendDeviceSilencedMessageAction?.Invoke(netEntity, toggleState);
  126. }
  127. #endregion
  128. public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[] airAlarms, AtmosAlertsComputerEntry[] fireAlarms, AtmosAlertsFocusDeviceData? focusData)
  129. {
  130. if (_owner == null)
  131. return;
  132. if (!_entManager.TryGetComponent<AtmosAlertsComputerComponent>(_owner.Value, out var console))
  133. return;
  134. if (_trackedEntity != focusData?.NetEntity)
  135. {
  136. SendFocusChangeMessageAction?.Invoke(_trackedEntity);
  137. focusData = null;
  138. }
  139. // Retain alarm data for use inbetween updates
  140. _airAlarms = airAlarms;
  141. _fireAlarms = fireAlarms;
  142. _allAlarms = airAlarms.Concat(fireAlarms);
  143. var silenced = console.SilencedDevices;
  144. _activeAlarms = _allAlarms.Where(x => x.AlarmState > AtmosAlarmType.Normal &&
  145. (!silenced.Contains(x.NetEntity) || _deviceSilencingProgress.ContainsKey(x.NetEntity)));
  146. // Reset nav map data
  147. NavMap.TrackedCoordinates.Clear();
  148. NavMap.TrackedEntities.Clear();
  149. // Add tracked entities to the nav map
  150. foreach (var device in console.AtmosDevices)
  151. {
  152. if (!device.NetEntity.Valid)
  153. continue;
  154. if (!NavMap.Visible)
  155. continue;
  156. var alarmState = GetAlarmState(device.NetEntity);
  157. if (_trackedEntity != device.NetEntity)
  158. {
  159. // Skip air alarms if the appropriate overlay is off
  160. if (!ShowInactiveAlarms.Pressed && alarmState == AtmosAlarmType.Invalid)
  161. continue;
  162. if (!ShowNormalAlarms.Pressed && alarmState == AtmosAlarmType.Normal)
  163. continue;
  164. if (!ShowWarningAlarms.Pressed && alarmState == AtmosAlarmType.Warning)
  165. continue;
  166. if (!ShowDangerAlarms.Pressed && alarmState == AtmosAlarmType.Danger)
  167. continue;
  168. }
  169. AddTrackedEntityToNavMap(device, alarmState);
  170. }
  171. // Show the monitor location
  172. var consoleUid = _entManager.GetNetEntity(_owner);
  173. if (consoleCoords != null && consoleUid != null)
  174. {
  175. var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
  176. var blip = new NavMapBlip(consoleCoords.Value, texture, _monitorBlipColor, true, false);
  177. NavMap.TrackedEntities[consoleUid.Value] = blip;
  178. }
  179. // Update the nav map
  180. NavMap.ForceNavMapUpdate();
  181. // Clear excess children from the tables
  182. var activeAlarmCount = _activeAlarms.Count();
  183. while (AlertsTable.ChildCount > activeAlarmCount)
  184. AlertsTable.RemoveChild(AlertsTable.GetChild(AlertsTable.ChildCount - 1));
  185. while (AirAlarmsTable.ChildCount > airAlarms.Length)
  186. AirAlarmsTable.RemoveChild(AirAlarmsTable.GetChild(AirAlarmsTable.ChildCount - 1));
  187. while (FireAlarmsTable.ChildCount > fireAlarms.Length)
  188. FireAlarmsTable.RemoveChild(FireAlarmsTable.GetChild(FireAlarmsTable.ChildCount - 1));
  189. // Update all entries in each table
  190. for (int index = 0; index < _activeAlarms.Count(); index++)
  191. {
  192. var entry = _activeAlarms.ElementAt(index);
  193. UpdateUIEntry(entry, index, AlertsTable, console, focusData);
  194. }
  195. for (int index = 0; index < airAlarms.Count(); index++)
  196. {
  197. var entry = airAlarms.ElementAt(index);
  198. UpdateUIEntry(entry, index, AirAlarmsTable, console, focusData);
  199. }
  200. for (int index = 0; index < fireAlarms.Count(); index++)
  201. {
  202. var entry = fireAlarms.ElementAt(index);
  203. UpdateUIEntry(entry, index, FireAlarmsTable, console, focusData);
  204. }
  205. // If no alerts are active, display a message
  206. if (MasterTabContainer.CurrentTab == 0 && activeAlarmCount == 0)
  207. {
  208. var label = new RichTextLabel()
  209. {
  210. HorizontalExpand = true,
  211. VerticalExpand = true,
  212. HorizontalAlignment = HAlignment.Center,
  213. VerticalAlignment = VAlignment.Center,
  214. };
  215. label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", _statusTextColor.ToHexNoAlpha())));
  216. AlertsTable.AddChild(label);
  217. }
  218. // Update the alerts tab with the number of active alerts
  219. if (activeAlarmCount == 0)
  220. MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts"));
  221. else
  222. MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount)));
  223. // Update sensor regions
  224. NavMap.RegionOverlays.Clear();
  225. var prioritizedRegionOverlays = new Dictionary<NavMapRegionOverlay, int>();
  226. if (_owner != null &&
  227. _entManager.TryGetComponent<TransformComponent>(_owner, out var xform) &&
  228. _entManager.TryGetComponent<NavMapComponent>(xform.GridUid, out var navMap))
  229. {
  230. var regionOverlays = _navMapSystem.GetNavMapRegionOverlays(_owner.Value, navMap, AtmosAlertsComputerUiKey.Key);
  231. foreach (var (regionOwner, regionOverlay) in regionOverlays)
  232. {
  233. var alarmState = GetAlarmState(regionOwner);
  234. if (!TryGetSensorRegionColor(regionOwner, alarmState, out var regionColor))
  235. continue;
  236. regionOverlay.Color = regionColor;
  237. var priority = (_trackedEntity == regionOwner) ? 999 : (int)alarmState;
  238. prioritizedRegionOverlays.Add(regionOverlay, priority);
  239. }
  240. // Sort overlays according to their priority
  241. var sortedOverlays = prioritizedRegionOverlays.OrderBy(x => x.Value).Select(x => x.Key).ToList();
  242. NavMap.RegionOverlays = sortedOverlays;
  243. }
  244. // Auto-scroll re-enable
  245. if (_autoScrollAwaitsUpdate)
  246. {
  247. _autoScrollActive = true;
  248. _autoScrollAwaitsUpdate = false;
  249. }
  250. }
  251. private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, AtmosAlarmType alarmState)
  252. {
  253. var data = GetBlipTexture(alarmState);
  254. if (data == null)
  255. return;
  256. var texture = data.Value.Item1;
  257. var color = data.Value.Item2;
  258. var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
  259. if (_trackedEntity != null && _trackedEntity != metaData.NetEntity)
  260. color *= _untrackedEntColor;
  261. var selectable = true;
  262. var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable);
  263. NavMap.TrackedEntities[metaData.NetEntity] = blip;
  264. }
  265. private bool TryGetSensorRegionColor(NetEntity regionOwner, AtmosAlarmType alarmState, out Color color)
  266. {
  267. color = Color.White;
  268. var blip = GetBlipTexture(alarmState);
  269. if (blip == null)
  270. return false;
  271. // Color the region based on alarm state and entity tracking
  272. color = blip.Value.Item2 * _regionBaseColor;
  273. if (_trackedEntity != null && _trackedEntity != regionOwner)
  274. color *= _untrackedEntColor;
  275. return true;
  276. }
  277. private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null)
  278. {
  279. // Make new UI entry if required
  280. if (index >= table.ChildCount)
  281. {
  282. var newEntryContainer = new AtmosAlarmEntryContainer(entry.NetEntity, _entManager.GetCoordinates(entry.Coordinates));
  283. // On click
  284. newEntryContainer.FocusButton.OnButtonUp += args =>
  285. {
  286. if (_trackedEntity == newEntryContainer.NetEntity)
  287. {
  288. _trackedEntity = null;
  289. }
  290. else
  291. {
  292. _trackedEntity = newEntryContainer.NetEntity;
  293. if (newEntryContainer.Coordinates != null)
  294. NavMap.CenterToCoordinates(newEntryContainer.Coordinates.Value);
  295. }
  296. // Send message to console that the focus has changed
  297. SendFocusChangeMessageAction?.Invoke(_trackedEntity);
  298. // Update affected UI elements across all tables
  299. UpdateConsoleTable(console, AlertsTable, _trackedEntity);
  300. UpdateConsoleTable(console, AirAlarmsTable, _trackedEntity);
  301. UpdateConsoleTable(console, FireAlarmsTable, _trackedEntity);
  302. };
  303. // On toggling the silence check box
  304. newEntryContainer.SilenceCheckBox.OnToggled += _ => OnSilenceAlertsToggled(newEntryContainer.NetEntity, newEntryContainer.SilenceCheckBox.Pressed);
  305. // Add the entry to the current table
  306. table.AddChild(newEntryContainer);
  307. }
  308. // Update values and UI elements
  309. var tableChild = table.GetChild(index);
  310. if (tableChild is not AtmosAlarmEntryContainer)
  311. {
  312. table.RemoveChild(tableChild);
  313. UpdateUIEntry(entry, index, table, console, focusData);
  314. return;
  315. }
  316. var entryContainer = (AtmosAlarmEntryContainer)tableChild;
  317. entryContainer.UpdateEntry(entry, entry.NetEntity == _trackedEntity, focusData);
  318. if (_trackedEntity != entry.NetEntity)
  319. {
  320. var silenced = console.SilencedDevices;
  321. entryContainer.SilenceCheckBox.Pressed = (silenced.Contains(entry.NetEntity) || _deviceSilencingProgress.ContainsKey(entry.NetEntity));
  322. }
  323. entryContainer.SilenceAlarmProgressBar.Visible = (table == AlertsTable && _deviceSilencingProgress.ContainsKey(entry.NetEntity));
  324. }
  325. private void UpdateConsoleTable(AtmosAlertsComputerComponent console, Control table, NetEntity? currTrackedEntity)
  326. {
  327. foreach (var tableChild in table.Children)
  328. {
  329. if (tableChild is not AtmosAlarmEntryContainer)
  330. continue;
  331. var entryContainer = (AtmosAlarmEntryContainer)tableChild;
  332. if (entryContainer.NetEntity != currTrackedEntity)
  333. entryContainer.RemoveAsFocus();
  334. else if (entryContainer.NetEntity == currTrackedEntity)
  335. entryContainer.SetAsFocus();
  336. }
  337. }
  338. private void SetTrackedEntityFromNavMap(NetEntity? netEntity)
  339. {
  340. if (netEntity == null)
  341. return;
  342. if (!_entManager.TryGetComponent<AtmosAlertsComputerComponent>(_owner, out var console))
  343. return;
  344. _trackedEntity = netEntity;
  345. if (netEntity != null)
  346. {
  347. // Tab switching
  348. if (MasterTabContainer.CurrentTab != 0 || _activeAlarms?.Any(x => x.NetEntity == netEntity) == false)
  349. {
  350. var device = console.AtmosDevices.FirstOrNull(x => x.NetEntity == netEntity);
  351. switch (device?.Group)
  352. {
  353. case AtmosAlertsComputerGroup.AirAlarm:
  354. MasterTabContainer.CurrentTab = 1; break;
  355. case AtmosAlertsComputerGroup.FireAlarm:
  356. MasterTabContainer.CurrentTab = 2; break;
  357. }
  358. }
  359. // Get the scroll position of the selected entity on the selected button the UI
  360. ActivateAutoScrollToFocus();
  361. }
  362. // Send message to console that the focus has changed
  363. SendFocusChangeMessageAction?.Invoke(_trackedEntity);
  364. }
  365. protected override void FrameUpdate(FrameEventArgs args)
  366. {
  367. AutoScrollToFocus();
  368. // Device silencing update
  369. foreach ((var device, var remainingTime) in _deviceSilencingProgress)
  370. {
  371. var t = remainingTime - args.DeltaSeconds;
  372. if (t <= 0)
  373. {
  374. _deviceSilencingProgress.Remove(device);
  375. if (device == _trackedEntity)
  376. _trackedEntity = null;
  377. }
  378. else
  379. _deviceSilencingProgress[device] = t;
  380. }
  381. }
  382. private void ActivateAutoScrollToFocus()
  383. {
  384. _autoScrollActive = false;
  385. _autoScrollAwaitsUpdate = true;
  386. }
  387. private void AutoScrollToFocus()
  388. {
  389. if (!_autoScrollActive)
  390. return;
  391. var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
  392. if (scroll == null)
  393. return;
  394. if (!TryGetNextScrollPosition(out float? nextScrollPosition))
  395. return;
  396. scroll.VScrollTarget = nextScrollPosition.Value;
  397. if (MathHelper.CloseToPercent(scroll.VScroll, scroll.VScrollTarget))
  398. _autoScrollActive = false;
  399. }
  400. private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
  401. {
  402. nextScrollPosition = null;
  403. var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
  404. if (scroll == null)
  405. return false;
  406. var container = scroll.Children.ElementAt(0) as BoxContainer;
  407. if (container == null || container.Children.Count() == 0)
  408. return false;
  409. // Exit if the heights of the children haven't been initialized yet
  410. if (!container.Children.Any(x => x.Height > 0))
  411. return false;
  412. nextScrollPosition = 0;
  413. foreach (var control in container.Children)
  414. {
  415. if (control == null || control is not AtmosAlarmEntryContainer)
  416. continue;
  417. if (((AtmosAlarmEntryContainer)control).NetEntity == _trackedEntity)
  418. return true;
  419. nextScrollPosition += control.Height;
  420. }
  421. // Failed to find control
  422. nextScrollPosition = null;
  423. return false;
  424. }
  425. private AtmosAlarmType GetAlarmState(NetEntity netEntity)
  426. {
  427. var alarmState = _allAlarms?.FirstOrNull(x => x.NetEntity == netEntity)?.AlarmState;
  428. if (alarmState == null)
  429. return AtmosAlarmType.Invalid;
  430. return alarmState.Value;
  431. }
  432. private (SpriteSpecifier.Texture, Color)? GetBlipTexture(AtmosAlarmType alarmState)
  433. {
  434. (SpriteSpecifier.Texture, Color)? output = null;
  435. switch (alarmState)
  436. {
  437. case AtmosAlarmType.Invalid:
  438. output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _inactiveColor); break;
  439. case AtmosAlarmType.Normal:
  440. output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _goodColor); break;
  441. case AtmosAlarmType.Warning:
  442. output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), _warningColor); break;
  443. case AtmosAlarmType.Danger:
  444. output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), _dangerColor); break;
  445. }
  446. return output;
  447. }
  448. }