diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs index 1ef8dfba2d..700f6b6d26 100644 --- a/Content.Client/Popups/PopupSystem.cs +++ b/Content.Client/Popups/PopupSystem.cs @@ -1,18 +1,20 @@ using System.Linq; +using Content.Shared.Containers; using Content.Shared.Examine; using Content.Shared.GameTicking; using Content.Shared.Popups; +using JetBrains.Annotations; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Player; using Robust.Client.UserInterface; +using Robust.Shared.Collections; using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Replays; using Robust.Shared.Timing; -using Robust.Shared.Utility; namespace Content.Client.Popups { @@ -29,11 +31,11 @@ namespace Content.Client.Popups [Dependency] private readonly ExamineSystemShared _examine = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; - public IReadOnlyList WorldLabels => _aliveWorldLabels; - public IReadOnlyList CursorLabels => _aliveCursorLabels; + public IReadOnlyCollection WorldLabels => _aliveWorldLabels.Values; + public IReadOnlyCollection CursorLabels => _aliveCursorLabels.Values; - private readonly List _aliveWorldLabels = new(); - private readonly List _aliveCursorLabels = new(); + private readonly Dictionary _aliveWorldLabels = new(); + private readonly Dictionary _aliveCursorLabels = new(); public const float MinimumPopupLifetime = 0.7f; public const float MaximumPopupLifetime = 5f; @@ -65,6 +67,15 @@ namespace Content.Client.Popups .RemoveOverlay(); } + private void WrapAndRepeatPopup(PopupLabel existingLabel, string popupMessage) + { + existingLabel.TotalTime = 0; + existingLabel.Repeats += 1; + existingLabel.Text = Loc.GetString("popup-system-repeated-popup-stacking-wrap", + ("popup-message", popupMessage), + ("count", existingLabel.Repeats)); + } + private void PopupMessage(string? message, PopupType type, EntityCoordinates coordinates, EntityUid? entity, bool recordReplay) { if (message == null) @@ -78,13 +89,20 @@ namespace Content.Client.Popups _replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates))); } + var popupData = new WorldPopupData(message, type, coordinates, entity); + if (_aliveWorldLabels.TryGetValue(popupData, out var existingLabel)) + { + WrapAndRepeatPopup(existingLabel, popupData.Message); + return; + } + var label = new WorldPopupLabel(coordinates) { Text = message, Type = type, }; - _aliveWorldLabels.Add(label); + _aliveWorldLabels.Add(popupData, label); } #region Abstract Method Implementations @@ -113,13 +131,20 @@ namespace Content.Client.Popups if (recordReplay && _replayRecording.IsRecording) _replayRecording.RecordClientMessage(new PopupCursorEvent(message, type)); + var popupData = new CursorPopupData(message, type); + if (_aliveCursorLabels.TryGetValue(popupData, out var existingLabel)) + { + WrapAndRepeatPopup(existingLabel, popupData.Message); + return; + } + var label = new CursorPopupLabel(_inputManager.MouseScreenPosition) { Text = message, Type = type, }; - _aliveCursorLabels.Add(label); + _aliveCursorLabels.Add(popupData, label); } public override void PopupCursor(string? message, PopupType type = PopupType.Small) @@ -249,27 +274,37 @@ namespace Content.Client.Popups if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0) return; - for (var i = 0; i < _aliveWorldLabels.Count; i++) + if (_aliveWorldLabels.Count > 0) { - var label = _aliveWorldLabels[i]; - label.TotalTime += frameTime; - - if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId)) + var aliveWorldToRemove = new ValueList(); + foreach (var (data, label) in _aliveWorldLabels) { - _aliveWorldLabels.RemoveSwap(i); - i--; + label.TotalTime += frameTime; + if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId)) + { + aliveWorldToRemove.Add(data); + } + } + foreach (var data in aliveWorldToRemove) + { + _aliveWorldLabels.Remove(data); } } - for (var i = 0; i < _aliveCursorLabels.Count; i++) + if (_aliveCursorLabels.Count > 0) { - var label = _aliveCursorLabels[i]; - label.TotalTime += frameTime; - - if (label.TotalTime > GetPopupLifetime(label)) + var aliveCursorToRemove = new ValueList(); + foreach (var (data, label) in _aliveCursorLabels) { - _aliveCursorLabels.RemoveSwap(i); - i--; + label.TotalTime += frameTime; + if (label.TotalTime > GetPopupLifetime(label)) + { + aliveCursorToRemove.Add(data); + } + } + foreach (var data in aliveCursorToRemove) + { + _aliveCursorLabels.Remove(data); } } } @@ -279,29 +314,32 @@ namespace Content.Client.Popups public PopupType Type = PopupType.Small; public string Text { get; set; } = string.Empty; public float TotalTime { get; set; } + public int Repeats = 1; } - public sealed class CursorPopupLabel : PopupLabel - { - public ScreenCoordinates InitialPos; - - public CursorPopupLabel(ScreenCoordinates screenCoords) - { - InitialPos = screenCoords; - } - } - - public sealed class WorldPopupLabel : PopupLabel + public sealed class WorldPopupLabel(EntityCoordinates coordinates) : PopupLabel { /// /// The original EntityCoordinates of the label. /// - public EntityCoordinates InitialPos; - - public WorldPopupLabel(EntityCoordinates coordinates) - { - InitialPos = coordinates; - } + public EntityCoordinates InitialPos = coordinates; } + + public sealed class CursorPopupLabel(ScreenCoordinates screenCoords) : PopupLabel + { + public ScreenCoordinates InitialPos = screenCoords; + } + + [UsedImplicitly] + private record struct WorldPopupData( + string Message, + PopupType Type, + EntityCoordinates Coordinates, + EntityUid? Entity); + + [UsedImplicitly] + private record struct CursorPopupData( + string Message, + PopupType Type); } } diff --git a/Resources/Locale/en-US/popup/popup.ftl b/Resources/Locale/en-US/popup/popup.ftl new file mode 100644 index 0000000000..4bc677c608 --- /dev/null +++ b/Resources/Locale/en-US/popup/popup.ftl @@ -0,0 +1 @@ +popup-system-repeated-popup-stacking-wrap = {$popup-message} x{$count}