1
0

AlertsSystem.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. using System.Collections.Frozen;
  2. using System.Diagnostics.CodeAnalysis;
  3. using Robust.Shared.Player;
  4. using Robust.Shared.Prototypes;
  5. using Robust.Shared.Timing;
  6. namespace Content.Shared.Alert;
  7. public abstract class AlertsSystem : EntitySystem
  8. {
  9. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  10. [Dependency] private readonly IGameTiming _timing = default!;
  11. private FrozenDictionary<ProtoId<AlertPrototype>, AlertPrototype> _typeToAlert = default!;
  12. public IReadOnlyDictionary<AlertKey, AlertState>? GetActiveAlerts(EntityUid euid)
  13. {
  14. return EntityManager.TryGetComponent(euid, out AlertsComponent? comp)
  15. ? comp.Alerts
  16. : null;
  17. }
  18. public short GetSeverityRange(ProtoId<AlertPrototype> alertType)
  19. {
  20. var minSeverity = _typeToAlert[alertType].MinSeverity;
  21. return (short)MathF.Max(minSeverity,_typeToAlert[alertType].MaxSeverity - minSeverity);
  22. }
  23. public short GetMaxSeverity(ProtoId<AlertPrototype> alertType)
  24. {
  25. return _typeToAlert[alertType].MaxSeverity;
  26. }
  27. public short GetMinSeverity(ProtoId<AlertPrototype> alertType)
  28. {
  29. return _typeToAlert[alertType].MinSeverity;
  30. }
  31. public bool IsShowingAlert(EntityUid euid, ProtoId<AlertPrototype> alertType)
  32. {
  33. if (!EntityManager.TryGetComponent(euid, out AlertsComponent? alertsComponent))
  34. return false;
  35. if (TryGet(alertType, out var alert))
  36. {
  37. return alertsComponent.Alerts.ContainsKey(alert.AlertKey);
  38. }
  39. Log.Debug("Unknown alert type {0}", alertType);
  40. return false;
  41. }
  42. /// <returns>true iff an alert of the indicated alert category is currently showing</returns>
  43. public bool IsShowingAlertCategory(EntityUid euid, ProtoId<AlertCategoryPrototype> alertCategory)
  44. {
  45. return EntityManager.TryGetComponent(euid, out AlertsComponent? alertsComponent)
  46. && alertsComponent.Alerts.ContainsKey(AlertKey.ForCategory(alertCategory));
  47. }
  48. public bool TryGetAlertState(EntityUid euid, AlertKey key, out AlertState alertState)
  49. {
  50. if (EntityManager.TryGetComponent(euid, out AlertsComponent? alertsComponent))
  51. return alertsComponent.Alerts.TryGetValue(key, out alertState);
  52. alertState = default;
  53. return false;
  54. }
  55. /// <summary>
  56. /// Shows the alert. If the alert or another alert of the same category is already showing,
  57. /// it will be updated / replaced with the specified values.
  58. /// </summary>
  59. /// <param name="euid"></param>
  60. /// <param name="alertType">type of the alert to set</param>
  61. /// <param name="severity">severity, if supported by the alert</param>
  62. /// <param name="cooldown">cooldown start and end, if null there will be no cooldown (and it will
  63. /// be erased if there is currently a cooldown for the alert)</param>
  64. /// <param name="autoRemove">if true, the alert will be removed at the end of the cooldown</param>
  65. /// <param name="showCooldown">if true, the cooldown will be visibly shown over the alert icon</param>
  66. public void ShowAlert(EntityUid euid, ProtoId<AlertPrototype> alertType, short? severity = null, (TimeSpan, TimeSpan)? cooldown = null, bool autoRemove = false, bool showCooldown = true )
  67. {
  68. // This should be handled as part of networking.
  69. if (_timing.ApplyingState)
  70. return;
  71. if (!TryComp(euid, out AlertsComponent? alertsComponent))
  72. return;
  73. if (TryGet(alertType, out var alert))
  74. {
  75. // Check whether the alert category we want to show is already being displayed, with the same type,
  76. // severity, and cooldown.
  77. if (alertsComponent.Alerts.TryGetValue(alert.AlertKey, out var alertStateCallback) &&
  78. alertStateCallback.Type == alertType &&
  79. alertStateCallback.Severity == severity &&
  80. alertStateCallback.Cooldown == cooldown &&
  81. alertStateCallback.AutoRemove == autoRemove &&
  82. alertStateCallback.ShowCooldown == showCooldown)
  83. {
  84. return;
  85. }
  86. // In the case we're changing the alert type but not the category, we need to remove it first.
  87. alertsComponent.Alerts.Remove(alert.AlertKey);
  88. var state = new AlertState
  89. { Cooldown = cooldown, Severity = severity, Type = alertType, AutoRemove = autoRemove, ShowCooldown = showCooldown};
  90. alertsComponent.Alerts[alert.AlertKey] = state;
  91. // Keeping a list of AutoRemove alerts, so Update() doesn't need to check every alert
  92. if (autoRemove)
  93. {
  94. var autoComp = EnsureComp<AlertAutoRemoveComponent>(euid);
  95. if (!autoComp.AlertKeys.Contains(alert.AlertKey))
  96. autoComp.AlertKeys.Add(alert.AlertKey);
  97. }
  98. AfterShowAlert((euid, alertsComponent));
  99. Dirty(euid, alertsComponent);
  100. }
  101. else
  102. {
  103. Log.Error("Unable to show alert {0}, please ensure this alertType has" +
  104. " a corresponding YML alert prototype",
  105. alertType);
  106. }
  107. }
  108. /// <summary>
  109. /// Clear the alert with the given category, if one is currently showing.
  110. /// </summary>
  111. public void ClearAlertCategory(EntityUid euid, ProtoId<AlertCategoryPrototype> category)
  112. {
  113. if(!TryComp(euid, out AlertsComponent? alertsComponent))
  114. return;
  115. var key = AlertKey.ForCategory(category);
  116. if (!alertsComponent.Alerts.Remove(key))
  117. {
  118. return;
  119. }
  120. AfterClearAlert((euid, alertsComponent));
  121. Dirty(euid, alertsComponent);
  122. }
  123. /// <summary>
  124. /// Clear the alert of the given type if it is currently showing.
  125. /// </summary>
  126. public void ClearAlert(EntityUid euid, ProtoId<AlertPrototype> alertType)
  127. {
  128. if (_timing.ApplyingState)
  129. return;
  130. if (!EntityManager.TryGetComponent(euid, out AlertsComponent? alertsComponent))
  131. return;
  132. if (TryGet(alertType, out var alert))
  133. {
  134. if (!alertsComponent.Alerts.Remove(alert.AlertKey))
  135. {
  136. return;
  137. }
  138. AfterClearAlert((euid, alertsComponent));
  139. Dirty(euid, alertsComponent);
  140. }
  141. else
  142. {
  143. Log.Error("Unable to clear alert, unknown alertType {0}", alertType);
  144. }
  145. }
  146. /// <summary>
  147. /// Invoked after showing an alert prior to dirtying the component
  148. /// </summary>
  149. protected virtual void AfterShowAlert(Entity<AlertsComponent> alerts) { }
  150. /// <summary>
  151. /// Invoked after clearing an alert prior to dirtying the component
  152. /// </summary>
  153. protected virtual void AfterClearAlert(Entity<AlertsComponent> alerts) { }
  154. public override void Initialize()
  155. {
  156. base.Initialize();
  157. SubscribeLocalEvent<AlertsComponent, ComponentStartup>(HandleComponentStartup);
  158. SubscribeLocalEvent<AlertsComponent, ComponentShutdown>(HandleComponentShutdown);
  159. SubscribeLocalEvent<AlertsComponent, PlayerAttachedEvent>(OnPlayerAttached);
  160. SubscribeLocalEvent<AlertAutoRemoveComponent, EntityUnpausedEvent>(OnAutoRemoveUnPaused);
  161. SubscribeAllEvent<ClickAlertEvent>(HandleClickAlert);
  162. SubscribeLocalEvent<PrototypesReloadedEventArgs>(HandlePrototypesReloaded);
  163. LoadPrototypes();
  164. }
  165. private void OnAutoRemoveUnPaused(EntityUid uid, AlertAutoRemoveComponent comp, EntityUnpausedEvent args)
  166. {
  167. if (!TryComp<AlertsComponent>(uid, out var alertComp))
  168. {
  169. return;
  170. }
  171. var dirty = false;
  172. foreach (var alert in alertComp.Alerts)
  173. {
  174. if (alert.Value.Cooldown is null)
  175. continue;
  176. var cooldown = (alert.Value.Cooldown.Value.Item1, alert.Value.Cooldown.Value.Item2 + args.PausedTime);
  177. var state = new AlertState
  178. {
  179. Severity = alert.Value.Severity,
  180. Cooldown = cooldown,
  181. ShowCooldown = alert.Value.ShowCooldown,
  182. AutoRemove = alert.Value.AutoRemove,
  183. Type = alert.Value.Type
  184. };
  185. alertComp.Alerts[alert.Key] = state;
  186. dirty = true;
  187. }
  188. if (dirty)
  189. Dirty(uid, comp);
  190. }
  191. public override void Update(float frameTime)
  192. {
  193. base.Update(frameTime);
  194. var query = EntityQueryEnumerator<AlertAutoRemoveComponent>();
  195. while (query.MoveNext(out var uid, out var autoComp))
  196. {
  197. var dirtyComp = false;
  198. if (autoComp.AlertKeys.Count <= 0 || !TryComp<AlertsComponent>(uid, out var alertComp))
  199. {
  200. RemCompDeferred(uid, autoComp);
  201. continue;
  202. }
  203. var removeList = new List<AlertKey>();
  204. foreach (var alertKey in autoComp.AlertKeys)
  205. {
  206. alertComp.Alerts.TryGetValue(alertKey, out var alertState);
  207. if (alertState.Cooldown is null || alertState.Cooldown.Value.Item2 >= _timing.CurTime)
  208. continue;
  209. removeList.Add(alertKey);
  210. alertComp.Alerts.Remove(alertKey);
  211. dirtyComp = true;
  212. }
  213. foreach (var alertKey in removeList)
  214. {
  215. autoComp.AlertKeys.Remove(alertKey);
  216. }
  217. if (dirtyComp)
  218. Dirty(uid, alertComp);
  219. }
  220. }
  221. protected virtual void HandleComponentShutdown(EntityUid uid, AlertsComponent component, ComponentShutdown args)
  222. {
  223. RaiseLocalEvent(uid, new AlertSyncEvent(uid), true);
  224. }
  225. private void HandleComponentStartup(EntityUid uid, AlertsComponent component, ComponentStartup args)
  226. {
  227. RaiseLocalEvent(uid, new AlertSyncEvent(uid), true);
  228. }
  229. private void HandlePrototypesReloaded(PrototypesReloadedEventArgs obj)
  230. {
  231. if (obj.WasModified<AlertPrototype>())
  232. LoadPrototypes();
  233. }
  234. protected virtual void LoadPrototypes()
  235. {
  236. var dict = new Dictionary<ProtoId<AlertPrototype>, AlertPrototype>();
  237. foreach (var alert in _prototypeManager.EnumeratePrototypes<AlertPrototype>())
  238. {
  239. if (!dict.TryAdd(alert.ID, alert))
  240. {
  241. Log.Error("Found alert with duplicate alertType {0} - all alerts must have" +
  242. " a unique alertType, this one will be skipped", alert.ID);
  243. }
  244. }
  245. _typeToAlert = dict.ToFrozenDictionary();
  246. }
  247. /// <summary>
  248. /// Tries to get the alert of the indicated type
  249. /// </summary>
  250. /// <returns>true if found</returns>
  251. public bool TryGet(ProtoId<AlertPrototype> alertType, [NotNullWhen(true)] out AlertPrototype? alert)
  252. {
  253. return _typeToAlert.TryGetValue(alertType, out alert);
  254. }
  255. private void HandleClickAlert(ClickAlertEvent msg, EntitySessionEventArgs args)
  256. {
  257. var player = args.SenderSession.AttachedEntity;
  258. if (player is null || !EntityManager.HasComponent<AlertsComponent>(player))
  259. return;
  260. if (!IsShowingAlert(player.Value, msg.Type))
  261. {
  262. Log.Debug("User {0} attempted to" +
  263. " click alert {1} which is not currently showing for them",
  264. EntityManager.GetComponent<MetaDataComponent>(player.Value).EntityName, msg.Type);
  265. return;
  266. }
  267. if (!TryGet(msg.Type, out var alert))
  268. {
  269. Log.Warning("Unrecognized encoded alert {0}", msg.Type);
  270. return;
  271. }
  272. ActivateAlert(player.Value, alert);
  273. }
  274. public bool ActivateAlert(EntityUid user, AlertPrototype alert)
  275. {
  276. if (alert.ClickEvent is not { } clickEvent)
  277. return false;
  278. clickEvent.Handled = false;
  279. clickEvent.User = user;
  280. clickEvent.AlertId = alert.ID;
  281. RaiseLocalEvent(user, (object) clickEvent, true);
  282. return clickEvent.Handled;
  283. }
  284. private void OnPlayerAttached(EntityUid uid, AlertsComponent component, PlayerAttachedEvent args)
  285. {
  286. Dirty(uid, component);
  287. }
  288. }