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; namespace Content.Client.Popups { public sealed class PopupSystem : SharedPopupSystem { [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IOverlayManager _overlay = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IUserInterfaceManager _uiManager = default!; [Dependency] private readonly IReplayRecordingManager _replayRecording = default!; [Dependency] private readonly ExamineSystemShared _examine = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; public IReadOnlyCollection WorldLabels => _aliveWorldLabels.Values; public IReadOnlyCollection CursorLabels => _aliveCursorLabels.Values; private readonly Dictionary _aliveWorldLabels = new(); private readonly Dictionary _aliveCursorLabels = new(); public const float MinimumPopupLifetime = 0.7f; public const float MaximumPopupLifetime = 5f; public const float PopupLifetimePerCharacter = 0.04f; public override void Initialize() { SubscribeNetworkEvent(OnPopupCursorEvent); SubscribeNetworkEvent(OnPopupCoordinatesEvent); SubscribeNetworkEvent(OnPopupEntityEvent); SubscribeNetworkEvent(OnRoundRestart); _overlay .AddOverlay(new PopupOverlay( _configManager, EntityManager, _playerManager, _prototype, _uiManager, _uiManager.GetUIController(), _examine, _transform, this)); } public override void Shutdown() { base.Shutdown(); _overlay .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) return; if (recordReplay && _replayRecording.IsRecording) { if (entity != null) _replayRecording.RecordClientMessage(new PopupEntityEvent(message, type, GetNetEntity(entity.Value))); else _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(popupData, label); } #region Abstract Method Implementations public override void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small) { PopupMessage(message, type, coordinates, null, true); } public override void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small) { if (_playerManager.LocalSession == recipient) PopupMessage(message, type, coordinates, null, true); } public override void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid recipient, PopupType type = PopupType.Small) { if (_playerManager.LocalEntity == recipient) PopupMessage(message, type, coordinates, null, true); } private void PopupCursorInternal(string? message, PopupType type, bool recordReplay) { if (message == null) return; 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(popupData, label); } public override void PopupCursor(string? message, PopupType type = PopupType.Small) => PopupCursorInternal(message, type, true); public override void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small) { if (_playerManager.LocalSession == recipient) PopupCursor(message, type); } public override void PopupCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small) { if (_playerManager.LocalEntity == recipient) PopupCursor(message, type); } public override void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool replayRecord, PopupType type = PopupType.Small) { PopupCoordinates(message, coordinates, type); } public override void PopupEntity(string? message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small) { if (_playerManager.LocalEntity == recipient) PopupEntity(message, uid, type); } public override void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small) { if (_playerManager.LocalSession == recipient) PopupEntity(message, uid, type); } public override void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small) { if (!filter.Recipients.Contains(_playerManager.LocalSession)) return; PopupEntity(message, uid, type); } public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small) { if (recipient == null) return; if (_timing.IsFirstTimePredicted) PopupCursor(message, recipient.Value, type); } public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) { if (recipient == null) return; if (_timing.IsFirstTimePredicted) PopupEntity(message, uid, recipient.Value, type); } public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small) { if (recipient == null) return; if (_timing.IsFirstTimePredicted) PopupCoordinates(message, coordinates, recipient.Value, type); } public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small) { if (TryComp(uid, out TransformComponent? transform)) PopupMessage(message, type, transform.Coordinates, uid, true); } public override void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) { if (recipient != null && _timing.IsFirstTimePredicted) PopupEntity(message, uid, recipient.Value, type); } public override void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) { if (recipient != null && _timing.IsFirstTimePredicted) PopupEntity(recipientMessage, uid, recipient.Value, type); } #endregion #region Network Event Handlers private void OnPopupCursorEvent(PopupCursorEvent ev) { PopupCursorInternal(ev.Message, ev.Type, false); } private void OnPopupCoordinatesEvent(PopupCoordinatesEvent ev) { PopupMessage(ev.Message, ev.Type, GetCoordinates(ev.Coordinates), null, false); } private void OnPopupEntityEvent(PopupEntityEvent ev) { var entity = GetEntity(ev.Uid); if (TryComp(entity, out TransformComponent? transform)) PopupMessage(ev.Message, ev.Type, transform.Coordinates, entity, false); } private void OnRoundRestart(RoundRestartCleanupEvent ev) { _aliveCursorLabels.Clear(); _aliveWorldLabels.Clear(); } #endregion public static float GetPopupLifetime(PopupLabel label) { return Math.Clamp(PopupLifetimePerCharacter * label.Text.Length, MinimumPopupLifetime, MaximumPopupLifetime); } public override void FrameUpdate(float frameTime) { if (_aliveWorldLabels.Count == 0 && _aliveCursorLabels.Count == 0) return; if (_aliveWorldLabels.Count > 0) { var aliveWorldToRemove = new ValueList(); foreach (var (data, label) in _aliveWorldLabels) { label.TotalTime += frameTime; if (label.TotalTime > GetPopupLifetime(label) || Deleted(label.InitialPos.EntityId)) { aliveWorldToRemove.Add(data); } } foreach (var data in aliveWorldToRemove) { _aliveWorldLabels.Remove(data); } } if (_aliveCursorLabels.Count > 0) { var aliveCursorToRemove = new ValueList(); foreach (var (data, label) in _aliveCursorLabels) { label.TotalTime += frameTime; if (label.TotalTime > GetPopupLifetime(label)) { aliveCursorToRemove.Add(data); } } foreach (var data in aliveCursorToRemove) { _aliveCursorLabels.Remove(data); } } } public abstract class PopupLabel { public PopupType Type = PopupType.Small; public string Text { get; set; } = string.Empty; public float TotalTime { get; set; } public int Repeats = 1; } public sealed class WorldPopupLabel(EntityCoordinates coordinates) : PopupLabel { /// /// The original EntityCoordinates of the label. /// 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); } }