PopupSystem.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. using System.Linq;
  2. using Content.Shared.Containers;
  3. using Content.Shared.Examine;
  4. using Content.Shared.GameTicking;
  5. using Content.Shared.Popups;
  6. using JetBrains.Annotations;
  7. using Robust.Client.Graphics;
  8. using Robust.Client.Input;
  9. using Robust.Client.Player;
  10. using Robust.Client.UserInterface;
  11. using Robust.Shared.Collections;
  12. using Robust.Shared.Configuration;
  13. using Robust.Shared.Map;
  14. using Robust.Shared.Player;
  15. using Robust.Shared.Prototypes;
  16. using Robust.Shared.Replays;
  17. using Robust.Shared.Timing;
  18. namespace Content.Client.Popups
  19. {
  20. public sealed class PopupSystem : SharedPopupSystem
  21. {
  22. [Dependency] private readonly IConfigurationManager _configManager = default!;
  23. [Dependency] private readonly IInputManager _inputManager = default!;
  24. [Dependency] private readonly IOverlayManager _overlay = default!;
  25. [Dependency] private readonly IPlayerManager _playerManager = default!;
  26. [Dependency] private readonly IPrototypeManager _prototype = default!;
  27. [Dependency] private readonly IGameTiming _timing = default!;
  28. [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
  29. [Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
  30. [Dependency] private readonly ExamineSystemShared _examine = default!;
  31. [Dependency] private readonly SharedTransformSystem _transform = default!;
  32. public IReadOnlyCollection<WorldPopupLabel> WorldLabels => _aliveWorldLabels.Values;
  33. public IReadOnlyCollection<CursorPopupLabel> CursorLabels => _aliveCursorLabels.Values;
  34. private readonly Dictionary<WorldPopupData, WorldPopupLabel> _aliveWorldLabels = new();
  35. private readonly Dictionary<CursorPopupData, CursorPopupLabel> _aliveCursorLabels = new();
  36. public const float MinimumPopupLifetime = 0.7f;
  37. public const float MaximumPopupLifetime = 5f;
  38. public const float PopupLifetimePerCharacter = 0.04f;
  39. public override void Initialize()
  40. {
  41. SubscribeNetworkEvent<PopupCursorEvent>(OnPopupCursorEvent);
  42. SubscribeNetworkEvent<PopupCoordinatesEvent>(OnPopupCoordinatesEvent);
  43. SubscribeNetworkEvent<PopupEntityEvent>(OnPopupEntityEvent);
  44. SubscribeNetworkEvent<RoundRestartCleanupEvent>(OnRoundRestart);
  45. _overlay
  46. .AddOverlay(new PopupOverlay(
  47. _configManager,
  48. EntityManager,
  49. _playerManager,
  50. _prototype,
  51. _uiManager,
  52. _uiManager.GetUIController<PopupUIController>(),
  53. _examine,
  54. _transform,
  55. this));
  56. }
  57. public override void Shutdown()
  58. {
  59. base.Shutdown();
  60. _overlay
  61. .RemoveOverlay<PopupOverlay>();
  62. }
  63. private void WrapAndRepeatPopup(PopupLabel existingLabel, string popupMessage)
  64. {
  65. existingLabel.TotalTime = 0;
  66. existingLabel.Repeats += 1;
  67. existingLabel.Text = Loc.GetString("popup-system-repeated-popup-stacking-wrap",
  68. ("popup-message", popupMessage),
  69. ("count", existingLabel.Repeats));
  70. }
  71. private void PopupMessage(string? message, PopupType type, EntityCoordinates coordinates, EntityUid? entity, bool recordReplay)
  72. {
  73. if (message == null)
  74. return;
  75. if (recordReplay && _replayRecording.IsRecording)
  76. {
  77. if (entity != null)
  78. _replayRecording.RecordClientMessage(new PopupEntityEvent(message, type, GetNetEntity(entity.Value)));
  79. else
  80. _replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)));
  81. }
  82. var popupData = new WorldPopupData(message, type, coordinates, entity);
  83. if (_aliveWorldLabels.TryGetValue(popupData, out var existingLabel))
  84. {
  85. WrapAndRepeatPopup(existingLabel, popupData.Message);
  86. return;
  87. }
  88. var label = new WorldPopupLabel(coordinates)
  89. {
  90. Text = message,
  91. Type = type,
  92. };
  93. _aliveWorldLabels.Add(popupData, label);
  94. }
  95. #region Abstract Method Implementations
  96. public override void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small)
  97. {
  98. PopupMessage(message, type, coordinates, null, true);
  99. }
  100. public override void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small)
  101. {
  102. if (_playerManager.LocalSession == recipient)
  103. PopupMessage(message, type, coordinates, null, true);
  104. }
  105. public override void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid recipient, PopupType type = PopupType.Small)
  106. {
  107. if (_playerManager.LocalEntity == recipient)
  108. PopupMessage(message, type, coordinates, null, true);
  109. }
  110. public override void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
  111. {
  112. if (recipient != null && _timing.IsFirstTimePredicted)
  113. PopupCoordinates(message, coordinates, recipient.Value, type);
  114. }
  115. private void PopupCursorInternal(string? message, PopupType type, bool recordReplay)
  116. {
  117. if (message == null)
  118. return;
  119. if (recordReplay && _replayRecording.IsRecording)
  120. _replayRecording.RecordClientMessage(new PopupCursorEvent(message, type));
  121. var popupData = new CursorPopupData(message, type);
  122. if (_aliveCursorLabels.TryGetValue(popupData, out var existingLabel))
  123. {
  124. WrapAndRepeatPopup(existingLabel, popupData.Message);
  125. return;
  126. }
  127. var label = new CursorPopupLabel(_inputManager.MouseScreenPosition)
  128. {
  129. Text = message,
  130. Type = type,
  131. };
  132. _aliveCursorLabels.Add(popupData, label);
  133. }
  134. public override void PopupCursor(string? message, PopupType type = PopupType.Small)
  135. {
  136. if (!_timing.IsFirstTimePredicted)
  137. return;
  138. PopupCursorInternal(message, type, true);
  139. }
  140. public override void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small)
  141. {
  142. if (_playerManager.LocalSession == recipient)
  143. PopupCursor(message, type);
  144. }
  145. public override void PopupCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small)
  146. {
  147. if (_playerManager.LocalEntity == recipient)
  148. PopupCursor(message, type);
  149. }
  150. public override void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool replayRecord, PopupType type = PopupType.Small)
  151. {
  152. PopupCoordinates(message, coordinates, type);
  153. }
  154. public override void PopupEntity(string? message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small)
  155. {
  156. if (_playerManager.LocalEntity == recipient)
  157. PopupEntity(message, uid, type);
  158. }
  159. public override void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small)
  160. {
  161. if (_playerManager.LocalSession == recipient)
  162. PopupEntity(message, uid, type);
  163. }
  164. public override void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small)
  165. {
  166. if (!filter.Recipients.Contains(_playerManager.LocalSession))
  167. return;
  168. PopupEntity(message, uid, type);
  169. }
  170. public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small)
  171. {
  172. if (recipient == null)
  173. return;
  174. if (_timing.IsFirstTimePredicted)
  175. PopupCursor(message, recipient.Value, type);
  176. }
  177. public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
  178. {
  179. if (recipient == null)
  180. return;
  181. if (_timing.IsFirstTimePredicted)
  182. PopupEntity(message, uid, recipient.Value, type);
  183. }
  184. public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
  185. {
  186. if (recipient == null)
  187. return;
  188. if (_timing.IsFirstTimePredicted)
  189. PopupCoordinates(message, coordinates, recipient.Value, type);
  190. }
  191. public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small)
  192. {
  193. if (TryComp(uid, out TransformComponent? transform))
  194. PopupMessage(message, type, transform.Coordinates, uid, true);
  195. }
  196. public override void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
  197. {
  198. if (recipient != null && _timing.IsFirstTimePredicted)
  199. PopupEntity(message, uid, recipient.Value, type);
  200. }
  201. public override void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
  202. {
  203. if (recipient != null && _timing.IsFirstTimePredicted)
  204. PopupEntity(recipientMessage, uid, recipient.Value, type);
  205. }
  206. #endregion
  207. #region Network Event Handlers
  208. private void OnPopupCursorEvent(PopupCursorEvent ev)
  209. {
  210. PopupCursorInternal(ev.Message, ev.Type, false);
  211. }
  212. private void OnPopupCoordinatesEvent(PopupCoordinatesEvent ev)
  213. {
  214. PopupMessage(ev.Message, ev.Type, GetCoordinates(ev.Coordinates), null, false);
  215. }
  216. private void OnPopupEntityEvent(PopupEntityEvent ev)
  217. {
  218. var entity = GetEntity(ev.Uid);
  219. if (TryComp(entity, out TransformComponent? transform))
  220. PopupMessage(ev.Message, ev.Type, transform.Coordinates, entity, false);
  221. }
  222. private void OnRoundRestart(RoundRestartCleanupEvent ev)
  223. {
  224. _aliveCursorLabels.Clear();
  225. _aliveWorldLabels.Clear();
  226. }
  227. #endregion
  228. public static float GetPopupLifetime(PopupLabel label)
  229. {
  230. return Math.Clamp(PopupLifetimePerCharacter * label.Text.Length,
  231. MinimumPopupLifetime,
  232. MaximumPopupLifetime);
  233. }
  234. public override void FrameUpdate(float frameTime)
  235. {
  236. if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0)
  237. return;
  238. if (_aliveWorldLabels.Count > 0)
  239. {
  240. var aliveWorldToRemove = new ValueList<WorldPopupData>();
  241. foreach (var (data, label) in _aliveWorldLabels)
  242. {
  243. label.TotalTime += frameTime;
  244. if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId))
  245. {
  246. aliveWorldToRemove.Add(data);
  247. }
  248. }
  249. foreach (var data in aliveWorldToRemove)
  250. {
  251. _aliveWorldLabels.Remove(data);
  252. }
  253. }
  254. if (_aliveCursorLabels.Count > 0)
  255. {
  256. var aliveCursorToRemove = new ValueList<CursorPopupData>();
  257. foreach (var (data, label) in _aliveCursorLabels)
  258. {
  259. label.TotalTime += frameTime;
  260. if (label.TotalTime > GetPopupLifetime(label))
  261. {
  262. aliveCursorToRemove.Add(data);
  263. }
  264. }
  265. foreach (var data in aliveCursorToRemove)
  266. {
  267. _aliveCursorLabels.Remove(data);
  268. }
  269. }
  270. }
  271. public abstract class PopupLabel
  272. {
  273. public PopupType Type = PopupType.Small;
  274. public string Text { get; set; } = string.Empty;
  275. public float TotalTime { get; set; }
  276. public int Repeats = 1;
  277. }
  278. public sealed class WorldPopupLabel(EntityCoordinates coordinates) : PopupLabel
  279. {
  280. /// <summary>
  281. /// The original EntityCoordinates of the label.
  282. /// </summary>
  283. public EntityCoordinates InitialPos = coordinates;
  284. }
  285. public sealed class CursorPopupLabel(ScreenCoordinates screenCoords) : PopupLabel
  286. {
  287. public ScreenCoordinates InitialPos = screenCoords;
  288. }
  289. [UsedImplicitly]
  290. private record struct WorldPopupData(
  291. string Message,
  292. PopupType Type,
  293. EntityCoordinates Coordinates,
  294. EntityUid? Entity);
  295. [UsedImplicitly]
  296. private record struct CursorPopupData(
  297. string Message,
  298. PopupType Type);
  299. }
  300. }