From 964aebb0317c0a8a74575cb9e10ed23b710ee1a3 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:42:08 +0200 Subject: [PATCH] Self-predicting popups (#44524) * Popup prediction refactor Co-authored-by: EmoGarbage404 <98561806+EmoGarbage404@users.noreply.github.com> * fix popupcoordinates prediction * review * ProtoMan --------- Co-authored-by: EmoGarbage404 <98561806+EmoGarbage404@users.noreply.github.com> --- Content.Client/Popups/PopupSystem.cs | 709 +++++++++--------- .../Pointing/EntitySystems/PointingSystem.cs | 4 +- Content.Server/Popups/PopupSystem.cs | 227 ++---- .../Shuttles/Systems/DockingSystem.cs | 14 +- .../Inventory/InventorySystem.Equip.cs | 16 +- Content.Shared/Popups/SharedPopupSystem.cs | 515 +++++++------ .../Strip/SharedStrippableSystem.cs | 22 +- .../Weapons/Ranged/Systems/SharedGunSystem.cs | 2 +- 8 files changed, 770 insertions(+), 739 deletions(-) diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs index 90bd0f862b..11d5b02a5c 100644 --- a/Content.Client/Popups/PopupSystem.cs +++ b/Content.Client/Popups/PopupSystem.cs @@ -12,358 +12,369 @@ using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Replays; -using Robust.Shared.Timing; -namespace Content.Client.Popups +namespace Content.Client.Popups; + +public sealed partial class PopupSystem : SharedPopupSystem { - public sealed partial class PopupSystem : SharedPopupSystem + [Dependency] private IConfigurationManager _configManager = default!; + [Dependency] private IInputManager _inputManager = default!; + [Dependency] private IOverlayManager _overlay = default!; + [Dependency] private IPlayerManager _playerManager = default!; + [Dependency] private IUserInterfaceManager _uiManager = default!; + [Dependency] private IReplayRecordingManager _replayRecording = default!; + [Dependency] private ExamineSystemShared _examine = default!; + [Dependency] private SharedTransformSystem _transform = default!; + + public IReadOnlyCollection WorldLabels => _aliveWorldLabels.Values; + public IReadOnlyCollection CursorLabels => _aliveCursorLabels.Values; + + private readonly Dictionary _aliveWorldLabels = new(); + private readonly Dictionary _aliveCursorLabels = new(); + + /// + /// List of popups that have been predicted by the client. + /// If a popup is received from the server that matches one of these, it will be ignored to prevent duplicates. + /// + private readonly List _predictionInstances = new(); + + public const float MinimumPopupLifetime = 0.7f; + public const float MaximumPopupLifetime = 5f; + public const float PopupLifetimePerCharacter = 0.04f; + + public override void Initialize() { - [Dependency] private IConfigurationManager _configManager = default!; - [Dependency] private IInputManager _inputManager = default!; - [Dependency] private IOverlayManager _overlay = default!; - [Dependency] private IPlayerManager _playerManager = default!; - [Dependency] private IGameTiming _timing = default!; - [Dependency] private IUserInterfaceManager _uiManager = default!; - [Dependency] private IReplayRecordingManager _replayRecording = default!; - [Dependency] private ExamineSystemShared _examine = default!; - [Dependency] private SharedTransformSystem _transform = default!; + base.Initialize(); - 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, - ProtoMan, - _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); - } - - public override void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small) - { - if (recipient != null && _timing.IsFirstTimePredicted) - PopupCoordinates(message, coordinates, recipient.Value, type); - } - - 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) - { - if (!_timing.IsFirstTimePredicted) - return; - - 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 PopupPredictedCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small) - { - PopupCursor(message, recipient, type); - } - - public override void PopupPredictedCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small) - { - PopupCursor(message, recipient, 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? message, EntityUid uid, EntityUid? recipient, Filter filter, bool recordReplay, 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); + _overlay.AddOverlay(new PopupOverlay( + _configManager, + EntityManager, + _playerManager, + ProtoMan, + _uiManager, + _uiManager.GetUIController(), + _examine, + _transform, + this)); } + + public override void Shutdown() + { + base.Shutdown(); + + _overlay.RemoveOverlay(); + } + + /// + /// If the same popup is repeated, this will make show x2, x3, x4, ... at the end of the message instead of creating a new, overlapping popup. + /// + 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)); + } + + /// + /// Interal implementation for both coordinates and entity popups. + /// + private void PopupInternal(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, Timing.CurTick, GetNetEntity(entity.Value))); + else + _replayRecording.RecordClientMessage(new PopupCoordinatesEvent(message, type, Timing.CurTick, GetNetCoordinates(coordinates), 0)); + } + + 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); + } + + /// + /// Internal implementation for cursor popups. + /// + private void PopupCursorInternal(string? message, PopupType type, bool recordReplay) + { + if (message == null) + return; + + if (recordReplay && _replayRecording.IsRecording) + _replayRecording.RecordClientMessage(new PopupCursorEvent(message, type, Timing.CurTick)); + + 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); + } + + #region Abstract Method Implementations + + /// + /// Shows a popup at the local user's cursor. + /// + /// + /// This overload only exists on the client. If you want to use this in Shared you should use the overload that takes a recipient, session or filter instead. + /// We do not add a virtual method to the shared system because that will cause problems if the shared code is run in a non-predicted way. + /// + /// The message to display. + /// Used to customize how this popup should appear visually. + public void PopupCursor(string? message, PopupType type = PopupType.Small) + { + if (!Timing.IsFirstTimePredicted || message is null) + return; + + _predictionInstances.Add(new PopupCursorEvent.PredictionInstance(message, type, Timing.CurTick)); + PopupCursorInternal(message, type, true); + } + + public override void PopupCursor(string? message, EntityUid? recipient, PopupType type = PopupType.Small) + { + if (_playerManager.LocalEntity == recipient) + PopupCursor(message, type); + } + + 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, Filter filter, bool recordReplay, PopupType type = PopupType.Small) + { + if (filter.Recipients.Contains(_playerManager.LocalSession)) + PopupCursor(message, type); + } + + public override void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small, int predictionKey = 0) + { + if (!Timing.IsFirstTimePredicted || message is null) + return; + + _predictionInstances.Add(new PopupCoordinatesEvent.PredictionInstance(message, type, Timing.CurTick, predictionKey)); + PopupInternal(message, type, coordinates, null, true); + } + + public override void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small, int predictionKey = 0) + { + if (_playerManager.LocalSession == recipient) + PopupCoordinates(message, coordinates, type, predictionKey); + } + + public override void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small, int predictionKey = 0) + { + if (_playerManager.LocalEntity == recipient) + PopupCoordinates(message, coordinates, type, predictionKey); + } + + public override void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool replayRecord, PopupType type = PopupType.Small, int predictionKey = 0) + { + if (filter.Recipients.Contains(_playerManager.LocalSession)) + PopupCoordinates(message, coordinates, type, predictionKey); + } + + public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small) + { + if (!Timing.IsFirstTimePredicted || message is null) + return; + + if (!TryComp(uid, out TransformComponent? transform)) + return; + + _predictionInstances.Add(new PopupEntityEvent.PredictionInstance(message, type, Timing.CurTick, GetNetEntity(uid))); + PopupInternal(message, type, transform.Coordinates, uid, true); + } + + 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)) + PopupEntity(message, uid, type); + } + + #endregion + + #region Network Event Handlers + + [SubscribeNetworkEvent] + private void OnPopupCursorEvent(PopupCursorEvent ev) + { + var instance = new PopupCursorEvent.PredictionInstance(ev.Message, ev.Type, ev.Tick); + if (_predictionInstances.Remove(instance)) + return; + + PopupCursorInternal(ev.Message, ev.Type, false); + } + + [SubscribeNetworkEvent] + private void OnPopupCoordinatesEvent(PopupCoordinatesEvent ev) + { + var instance = new PopupCoordinatesEvent.PredictionInstance(ev.Message, ev.Type, ev.Tick, ev.PredictionKey); + if (_predictionInstances.Remove(instance)) + return; + + PopupInternal(ev.Message, ev.Type, GetCoordinates(ev.Coordinates), null, false); + } + + [SubscribeNetworkEvent] + private void OnPopupEntityEvent(PopupEntityEvent ev) + { + var instance = new PopupEntityEvent.PredictionInstance(ev.Message, ev.Type, ev.Tick, ev.Uid); + if (_predictionInstances.Remove(instance)) + return; + + var entity = GetEntity(ev.Uid); + + if (TryComp(entity, out TransformComponent? transform)) + PopupInternal(ev.Message, ev.Type, transform.Coordinates, entity, false); + } + + [SubscribeNetworkEvent] + private void OnRoundRestart(RoundRestartCleanupEvent ev) + { + _aliveCursorLabels.Clear(); + _aliveWorldLabels.Clear(); + _predictionInstances.Clear(); + } + + #endregion + + /// + /// Calculates the lifetime of a popup based on its text length. + /// + public static float GetPopupLifetime(PopupLabel label) + { + return Math.Clamp(PopupLifetimePerCharacter * label.Text.Length, + MinimumPopupLifetime, + MaximumPopupLifetime); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!Timing.IsFirstTimePredicted) + return; // We only need to clean up once per tick. + + + // We only keep track of prediction instances for a short amount of time to prevent memory leaks. + // We can safely assume that if a popup hasn't been received from the server within a 10 seconds, it will never be received. + var deleteTickCount = Timing.TickRate * 10; + if (_predictionInstances.Count != 0) + { + _predictionInstances.RemoveAll(p => (int)Timing.CurTick.Value - (int)p.Tick.Value > deleteTickCount); + } + } + + 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); } diff --git a/Content.Server/Pointing/EntitySystems/PointingSystem.cs b/Content.Server/Pointing/EntitySystems/PointingSystem.cs index 1c062ace16..450bf5e31d 100644 --- a/Content.Server/Pointing/EntitySystems/PointingSystem.cs +++ b/Content.Server/Pointing/EntitySystems/PointingSystem.cs @@ -103,10 +103,10 @@ namespace Content.Server.Pointing.EntitySystems // Someone pointing at YOU is slightly more important var popupType = viewerEntity == pointed ? PopupType.Medium : PopupType.Small; - RaiseNetworkEvent(new PopupEntityEvent(message, popupType, netSource), viewerEntity); + RaiseNetworkEvent(new PopupEntityEvent(message, popupType, _gameTiming.CurTick, netSource), viewerEntity); // TODO: Make this use the popup system API } - _replay.RecordServerMessage(new PopupEntityEvent(viewerMessage, PopupType.Small, netSource)); + _replay.RecordServerMessage(new PopupEntityEvent(viewerMessage, PopupType.Small, _gameTiming.CurTick, netSource)); } public bool InRange(EntityUid pointer, EntityCoordinates coordinates) diff --git a/Content.Server/Popups/PopupSystem.cs b/Content.Server/Popups/PopupSystem.cs index 56221a3092..e798e9a536 100644 --- a/Content.Server/Popups/PopupSystem.cs +++ b/Content.Server/Popups/PopupSystem.cs @@ -1,181 +1,108 @@ using Content.Shared.Popups; -using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Player; -namespace Content.Server.Popups +namespace Content.Server.Popups; + +public sealed partial class PopupSystem : SharedPopupSystem { - public sealed partial class PopupSystem : SharedPopupSystem + [Dependency] private IPlayerManager _player = default!; + [Dependency] private IConfigurationManager _cfg = default!; + [Dependency] private SharedTransformSystem _transform = default!; + + public override void PopupCursor(string? message, EntityUid? recipient, PopupType type = PopupType.Small) { - [Dependency] private IPlayerManager _player = default!; - [Dependency] private IConfigurationManager _cfg = default!; - [Dependency] private SharedTransformSystem _transform = default!; + if (message == null) + return; - public override void PopupCursor(string? message, PopupType type = PopupType.Small) - { - // No local user. - } + if (TryComp(recipient, out ActorComponent? actor)) + RaiseNetworkEvent(new PopupCursorEvent(message, type, Timing.CurTick), actor.PlayerSession); + } - public override void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small) - { - if (message == null) - return; + public override void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small) + { + if (message == null) + return; - RaiseNetworkEvent(new PopupCursorEvent(message, type), recipient); - } + RaiseNetworkEvent(new PopupCursorEvent(message, type, Timing.CurTick), recipient); + } - public override void PopupCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small) - { - if (message == null) - return; + public override void PopupCursor(string? message, Filter filter, bool recordReplay, PopupType type = PopupType.Small) + { + if (message == null) + return; - if (TryComp(recipient, out ActorComponent? actor)) - RaiseNetworkEvent(new PopupCursorEvent(message, type), actor.PlayerSession); - } + RaiseNetworkEvent(new PopupCursorEvent(message, type, Timing.CurTick), filter, recordReplay); + } - public override void PopupPredictedCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small) - { - // Do nothing, since the client already predicted the popup. - } + public override void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small, int predictionKey = 0) + { + if (message == null) + return; - public override void PopupPredictedCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small) - { - // Do nothing, since the client already predicted the popup. - } + var mapPos = _transform.ToMapCoordinates(coordinates); + var filter = Filter.Empty().AddPlayersByPvs(mapPos, entManager: EntityManager, playerMan: _player, cfgMan: _cfg); + RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, Timing.CurTick, GetNetCoordinates(coordinates), predictionKey), filter); + } - public override void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool replayRecord, PopupType type = PopupType.Small) - { - if (message == null) - return; + public override void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small, int predictionKey = 0) + { + if (message == null) + return; - RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)), filter, replayRecord); - } + if (TryComp(recipient, out ActorComponent? actor)) + RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, Timing.CurTick, GetNetCoordinates(coordinates), predictionKey), actor.PlayerSession); + } - public override void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small) - { - if (message == null) - return; - var mapPos = _transform.ToMapCoordinates(coordinates); - var filter = Filter.Empty().AddPlayersByPvs(mapPos, entManager: EntityManager, playerMan: _player, cfgMan: _cfg); - RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)), filter); - } + public override void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small, int predictionKey = 0) + { + if (message == null) + return; - public override void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small) - { - if (message == null) - return; + RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, Timing.CurTick, GetNetCoordinates(coordinates), predictionKey), recipient); + } - RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)), recipient); - } + public override void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool recordReplay, PopupType type = PopupType.Small, int predictionKey = 0) + { + if (message == null) + return; - public override void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid recipient, PopupType type = PopupType.Small) - { - if (message == null) - return; + RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, Timing.CurTick, GetNetCoordinates(coordinates), predictionKey), filter, recordReplay); + } - if (TryComp(recipient, out ActorComponent? actor)) - RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)), actor.PlayerSession); - } + public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small) + { + if (message == null) + return; - public override void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small) - { - if (message == null) - return; + var filter = Filter.Empty().AddPlayersByPvs(uid, entityManager: EntityManager, playerMan: _player, cfgMan: _cfg); + RaiseNetworkEvent(new PopupEntityEvent(message, type, Timing.CurTick, GetNetEntity(uid)), filter); + } - var mapPos = _transform.ToMapCoordinates(coordinates); - var filter = Filter.Empty().AddPlayersByPvs(mapPos, entManager: EntityManager, playerMan: _player, cfgMan: _cfg); - if (recipient != null) - { - // Don't send to recipient, since they predicted it locally - filter = filter.RemovePlayerByAttachedEntity(recipient.Value); - } - RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)), filter); - } + public override void PopupEntity(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) + { + if (message == null) + return; - public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small) - { - if (message == null) - return; + if (TryComp(recipient, out ActorComponent? actor)) + RaiseNetworkEvent(new PopupEntityEvent(message, type, Timing.CurTick, GetNetEntity(uid)), actor.PlayerSession); + } - var filter = Filter.Empty().AddPlayersByPvs(uid, entityManager: EntityManager, playerMan: _player, cfgMan: _cfg); - RaiseNetworkEvent(new PopupEntityEvent(message, type, GetNetEntity(uid)), filter); - } + public override void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small) + { + if (message == null) + return; - public override void PopupEntity(string? message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small) - { - if (message == null) - return; + RaiseNetworkEvent(new PopupEntityEvent(message, type, Timing.CurTick, GetNetEntity(uid)), recipient); + } - if (TryComp(recipient, out ActorComponent? actor)) - RaiseNetworkEvent(new PopupEntityEvent(message, type, GetNetEntity(uid)), actor.PlayerSession); - } + public override void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small) + { + if (message == null) + return; - public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small) - { - } - - public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) - { - // do nothing duh its for client only - } - - public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small) - { - } - - public override void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small) - { - if (message == null) - return; - - RaiseNetworkEvent(new PopupEntityEvent(message, type, GetNetEntity(uid)), recipient); - } - - public override void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small) - { - if (message == null) - return; - - RaiseNetworkEvent(new PopupEntityEvent(message, type, GetNetEntity(uid)), filter, recordReplay); - } - - public override void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) - { - if (message == null) - return; - - if (recipient != null) - { - // Don't send to recipient, since they predicted it locally - var filter = Filter.PvsExcept(recipient.Value, entityManager: EntityManager); - RaiseNetworkEvent(new PopupEntityEvent(message, type, GetNetEntity(uid)), filter); - } - else - { - // With no recipient, send to everyone (in PVS range) - RaiseNetworkEvent(new PopupEntityEvent(message, type, GetNetEntity(uid))); - } - } - - public override void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, Filter filter, bool recordReplay, PopupType type = PopupType.Small) - { - if (message == null) - return; - - if (recipient != null) - { - // Don't send to recipient, since they predicted it locally - filter = filter.RemovePlayerByAttachedEntity(recipient.Value); - } - - RaiseNetworkEvent(new PopupEntityEvent(message, type, GetNetEntity(uid)), filter, recordReplay); - } - - public override void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) - { - PopupPredicted(othersMessage, uid, recipient, type); - } + RaiseNetworkEvent(new PopupEntityEvent(message, type, Timing.CurTick, GetNetEntity(uid)), filter, recordReplay); } } diff --git a/Content.Server/Shuttles/Systems/DockingSystem.cs b/Content.Server/Shuttles/Systems/DockingSystem.cs index 5c29de00b0..e1bab7b07d 100644 --- a/Content.Server/Shuttles/Systems/DockingSystem.cs +++ b/Content.Server/Shuttles/Systems/DockingSystem.cs @@ -353,7 +353,7 @@ namespace Content.Server.Shuttles.Systems if (!TryGetEntity(args.DockEntity, out var dockEnt) || !_dockingQuery.TryComp(dockEnt, out var dockComp)) { - _popup.PopupCursor(Loc.GetString("shuttle-console-undock-fail")); + _popup.PopupCursor(Loc.GetString("shuttle-console-undock-fail"), args.Actor); return; } @@ -361,7 +361,7 @@ namespace Content.Server.Shuttles.Systems if (!CanUndock(dock)) { - _popup.PopupCursor(Loc.GetString("shuttle-console-undock-fail")); + _popup.PopupCursor(Loc.GetString("shuttle-console-undock-fail"), args.Actor); return; } @@ -374,7 +374,7 @@ namespace Content.Server.Shuttles.Systems if (console == null) { - _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail")); + _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"), args.Actor); return; } @@ -382,7 +382,7 @@ namespace Content.Server.Shuttles.Systems if (!CanShuttleDock(shuttleUid)) { - _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail")); + _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"), args.Actor); return; } @@ -391,7 +391,7 @@ namespace Content.Server.Shuttles.Systems !_dockingQuery.TryComp(ourDock, out var ourDockComp) || !_dockingQuery.TryComp(targetDock, out var targetDockComp)) { - _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail")); + _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"), args.Actor); return; } @@ -399,7 +399,7 @@ namespace Content.Server.Shuttles.Systems if (!TryComp(ourDock, out TransformComponent? xformA) || xformA.GridUid != shuttleUid) { - _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail")); + _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"), args.Actor); return; } @@ -407,7 +407,7 @@ namespace Content.Server.Shuttles.Systems // Also need to check preventpilot + enabled / dockedwith if (!CanDock((ourDock.Value, ourDockComp), (targetDock.Value, targetDockComp))) { - _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail")); + _popup.PopupCursor(Loc.GetString("shuttle-console-dock-fail"), args.Actor); return; } diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs index f766bf8c36..757589623f 100644 --- a/Content.Shared/Inventory/InventorySystem.Equip.cs +++ b/Content.Shared/Inventory/InventorySystem.Equip.cs @@ -112,7 +112,7 @@ public abstract partial class InventorySystem // before we drop the item, check that it can be equipped in the first place. if (!CanEquip(actor, held.Value, ev.Slot, out var reason)) { - _popup.PopupCursor(Loc.GetString(reason)); + _popup.PopupCursor(Loc.GetString(reason), actor); return; } @@ -134,7 +134,7 @@ public abstract partial class InventorySystem if (!Resolve(target, ref inventory, false)) { if (!silent) - _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot")); + _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot"), actor); return false; } @@ -145,14 +145,14 @@ public abstract partial class InventorySystem if (!TryGetSlotContainer(target, slot, out var slotContainer, out var slotDefinition, inventory)) { if (!silent) - _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot")); + _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot"), actor); return false; } if (!force && !CanEquip(actor, target, itemUid, slot, out var reason, slotDefinition, inventory, clothing)) { if (!silent) - _popup.PopupCursor(Loc.GetString(reason)); + _popup.PopupCursor(Loc.GetString(reason), actor); return false; } @@ -190,7 +190,7 @@ public abstract partial class InventorySystem if (!_containerSystem.Insert(itemUid, slotContainer)) { if (!silent) - _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot")); + _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"), actor); return false; } @@ -409,14 +409,14 @@ public abstract partial class InventorySystem if (!Resolve(target, ref inventory, false)) { if (!silent) - _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot")); + _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"), actor); return false; } if (!TryGetSlotContainer(target, slot, out var slotContainer, out var slotDefinition, inventory)) { if (!silent) - _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot")); + _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"), actor); return false; } @@ -428,7 +428,7 @@ public abstract partial class InventorySystem if (!force && !CanUnequip(actor, target, slot, out var reason, slotContainer, slotDefinition, inventory)) { if (!silent) - _popup.PopupCursor(Loc.GetString(reason)); + _popup.PopupCursor(Loc.GetString(reason), actor); return false; } diff --git a/Content.Shared/Popups/SharedPopupSystem.cs b/Content.Shared/Popups/SharedPopupSystem.cs index b2be7509f5..e374cf834c 100644 --- a/Content.Shared/Popups/SharedPopupSystem.cs +++ b/Content.Shared/Popups/SharedPopupSystem.cs @@ -1,233 +1,326 @@ using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Serialization; +using Robust.Shared.Timing; -namespace Content.Shared.Popups +namespace Content.Shared.Popups; + +/// +/// System for displaying small text popups on users' screens. +/// +public abstract partial class SharedPopupSystem : EntitySystem { - /// - /// System for displaying small text popups on users' screens. - /// - public abstract class SharedPopupSystem : EntitySystem - { - /// - /// Shows a popup at the local users' cursor. Does nothing on the server. - /// - /// The message to display. - /// Used to customize how this popup should appear visually. - public abstract void PopupCursor(string? message, PopupType type = PopupType.Small); - - /// - /// Shows a popup at a users' cursor. - /// - /// The message to display. - /// Client that will see this popup. - /// Used to customize how this popup should appear visually. - public abstract void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small); - - /// - /// Shows a popup at a users' cursor. - /// - /// The message to display. - /// Client that will see this popup. - /// Used to customize how this popup should appear visually. - public abstract void PopupCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small); - - /// - /// Variant of for use with prediction. - /// The local client will show the popup to the recipient. Does nothing on the server. - /// - public abstract void PopupPredictedCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small); - - /// - /// Variant of for use with prediction. - /// The local client will show the popup to the recipient. Does nothing on the server. - /// - public abstract void PopupPredictedCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small); - - /// - /// Shows a popup at a world location to every entity in PVS range. - /// - /// The message to display. - /// The coordinates where to display the message. - /// Used to customize how this popup should appear visually. - public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small); - - /// - /// Filtered variant of , which should only be used - /// if the filtering has to be more specific than simply PVS range based. - /// - /// Filter for the players that will see the popup. - /// If true, this pop-up will be considered as a globally visible pop-up that gets shown during replays. - public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool recordReplay, PopupType type = PopupType.Small); - - /// - /// Variant of that sends a pop-up to the player attached to some entity. - /// - public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid recipient, PopupType type = PopupType.Small); - - /// - /// Variant of that sends a pop-up to a specific player. - /// - public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small); - - /// - /// Variant of for use with prediction. The local client will - /// the popup to the recipient, and the server will show it to every other player in PVS range. If recipient is null, the local - /// client will do nothing and the server will show the message to every player in PVS range. - /// - public abstract void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small); - - /// - /// Shows a popup above an entity for every player in pvs range. - /// - /// The message to display. - /// The UID of the entity. - /// Used to customize how this popup should appear visually. - public abstract void PopupEntity(string? message, EntityUid uid, PopupType type=PopupType.Small); - - /// - /// Variant of that shows the popup only to some specific client. - /// - public abstract void PopupEntity(string? message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small); - - /// - /// Variant of that shows the popup only to some specific client. - /// - public abstract void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small); - - /// - /// Filtered variant of , which should only be used - /// if the filtering has to be more specific than simply PVS range based. - /// - public abstract void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small); - - /// - /// Variant of that only runs on the client, outside of prediction. - /// Useful for shared code that is always ran by both sides to avoid duplicate popups. - /// - public abstract void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small); - - /// - /// Variant of that only runs on the client, outside of prediction. - /// Useful for shared code that is always ran by both sides to avoid duplicate popups. - /// - public abstract void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small); - - /// - /// Variant of that only runs on the client, outside of prediction. - /// Useful for shared code that is always ran by both sides to avoid duplicate popups. - /// - public abstract void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small); - - /// - /// Variant of for use with prediction. The local client will show - /// the popup to the recipient, and the server will show it to every other player in PVS range. If recipient is null, the local client - /// will do nothing and the server will show the message to every player in PVS range. - /// - public abstract void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small); - - /// - /// Variant of for use with prediction. - /// The local client will show the popup to the recipient, and the server will show it to players in the filter. - /// If recipient is null, the local client will do nothing and the server will show the message to players in the filter. - /// - /// The message to display. - /// The entity to display the popup above. - /// The client that will see this popup locally during prediction. - /// Filter for players that will see the popup from the server. - /// If true, this pop-up will be considered as a globally visible pop-up that gets shown during replays. - /// Used to customize how this popup should appear visually. See: . - public abstract void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, Filter filter, bool recordReplay, PopupType type = PopupType.Small); - - /// - /// Variant of that displays - /// to the recipient and to everyone else in PVS range. - /// - public abstract void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small); - } + [Dependency] protected IGameTiming Timing = default!; /// - /// Common base for all popup network events. + /// Shows a popup at a user's cursor. /// - [Serializable, NetSerializable] - public abstract class PopupEvent : EntityEventArgs - { - public string Message { get; } - - public PopupType Type { get; } - - protected PopupEvent(string message, PopupType type) - { - Message = message; - Type = type; - } - } + /// The message to display. + /// The entity whose attached player will see the popup. + /// Used to customize how this popup should appear visually. + public abstract void PopupCursor(string? message, EntityUid? recipient, PopupType type = PopupType.Small); /// - /// Network event for displaying a popup on the user's cursor. + /// Shows a popup at a user's cursor. /// - [Serializable, NetSerializable] - public sealed class PopupCursorEvent : PopupEvent - { - public PopupCursorEvent(string message, PopupType type) : base(message, type) - { - } - } + /// The message to display. + /// The player session that will see the popup. + /// Used to customize how this popup should appear visually. + public abstract void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small); /// - /// Network event for displaying a popup at a world location. + /// Shows a popup at some users' cursors. /// - [Serializable, NetSerializable] - public sealed class PopupCoordinatesEvent : PopupEvent - { - public NetCoordinates Coordinates { get; } - - public PopupCoordinatesEvent(string message, PopupType type, NetCoordinates coordinates) : base(message, type) - { - Coordinates = coordinates; - } - } + /// The message to display. + /// Filter for the clients that will see this popup. + /// If true, this pop-up will be considered as a globally visible pop-up that gets shown during replays. + /// Used to customize how this popup should appear visually. + public abstract void PopupCursor(string? message, Filter filter, bool recordReplay, PopupType type = PopupType.Small); /// - /// Network event for displaying a popup above an entity. - /// - [Serializable, NetSerializable] - public sealed class PopupEntityEvent : PopupEvent - { - public NetEntity Uid { get; } - - public PopupEntityEvent(string message, PopupType type, NetEntity uid) : base(message, type) - { - Uid = uid; - } - } - - /// - /// Used to determine how a popup should appear visually to the client. Caution variants simply have a red color. + /// Shows a popup at a world location to every entity in PVS range. /// + /// The message to display. + /// The coordinates where to display the message. + /// Used to customize how this popup should appear visually. + /// Additional key used to uniquely identify this popup event for prediction purposes. /// - /// Actions which can fail or succeed should use a smaller popup for failure and a larger popup for success. - /// Actions which have different popups for the user vs. others should use a larger popup for the user and a smaller popup for others. - /// Actions which result in harm or are otherwise dangerous should always show as the caution variant. + /// In case your popup is predicted and may show up multiple times at different locations in a single tick, for each popup you should use a different prediction key, + /// which server and client have to agree on. Otherwise, the client may ignore some of the popups. + /// This is needed because the coordinates themselves cannot be part of the PredictionInstance, since they slightly differ between server and client due to + /// floating point precision issues and predictive movement, so two popups at different locations with the same message in the same tick would be considered identical. + /// The most common use case for this is when you delete an entity and spawn a popup at its location. For this you can use the NetEntity ID of the deleted entity as the prediction key. /// - [Serializable, NetSerializable] - public enum PopupType : byte + public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small, int predictionKey = 0); + + /// + /// Variant of that sends a popup to the player attached to some entity. + /// + /// The message to display. + /// The coordinates where to display the message. + /// The entity whose attached player will see the popup. + /// Used to customize how this popup should appear visually. + /// Additional key used to uniquely identify this popup event for prediction purposes. + public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small, int predictionKey = 0); + + /// + /// Variant of that sends a popup to a specific player. + /// + /// The message to display. + /// The coordinates where to display the message. + /// The player session that will see the popup. + /// Used to customize how this popup should appear visually. + /// Additional key used to uniquely identify this popup event for prediction purposes. + public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small, int predictionKey = 0); + + /// + /// Filtered variant of , which should only be used + /// if the filtering has to be more specific than simply PVS range based. + /// + /// The message to display. + /// The coordinates where to display the message. + /// Filter for the clients that will see this popup. + /// If true, this pop-up will be considered as a globally visible pop-up that gets shown during replays. + /// Used to customize how this popup should appear visually. + /// Additional key used to uniquely identify this popup event for prediction purposes. + public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool recordReplay, PopupType type = PopupType.Small, int predictionKey = 0); + + /// + /// Shows a popup above an entity for every player in PVS range. + /// + /// The message to display. + /// The entity above which to display the popup. + /// Used to customize how this popup should appear visually. + public abstract void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small); + + /// + /// Variant of that shows the popup only to some specific client. + /// + /// The message to display. + /// The entity above which to display the popup. + /// The entity whose attached player will see the popup.Used to customize how this popup should appear visually. + public abstract void PopupEntity(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small); + + /// + /// Variant of that shows the popup only to some specific client. + /// + /// The message to display. + /// The entity above which to display the popup. + /// The player session that will see the popup. + /// Used to customize how this popup should appear visually. + public abstract void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small); + + /// + /// Filtered variant of , which should only be used + /// if the filtering has to be more specific than simply PVS range based. + /// + /// The message to display. + /// The entity above which to display the popup. + /// Filter for the clients that will see this popup. + /// If true, this pop-up will be considered as a globally visible pop-up that gets shown during replays. + /// Used to customize how this popup should appear visually. + public abstract void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small); + + /// + /// Variant of that displays + /// to the recipient and to everyone else in PVS range. + /// + /// The message to display to the recipient. + /// The message to display to everyone else in PVS range. + /// The entity above which to display the popup. + /// The entity whose attached player will see the recipient message. + /// Used to customize how this popup should appear visually. - /// Small popups are the default, and denote actions that may be spammable or are otherwise unimportant. - /// - Small, - SmallCaution, - /// - /// Medium popups should be used for actions which are not spammable but may not be particularly important. - /// - Medium, - MediumCaution, - /// - /// Large popups should be used for actions which may be important or very important to one or more users, - /// but is not life-threatening. - /// - Large, - LargeCaution + if (recipient.HasValue) + { + PopupEntity(othersMessage, uid, Filter.PvsExcept(recipient.Value), true, type); + PopupEntity(recipientMessage, uid, recipient.Value, type); + } + else + { + PopupEntity(othersMessage, uid, type); + } + } + + [Obsolete("Popups are automatically predicted now, just call PopupCursor and the client will handle prediction.")] + public void PopupPredictedCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small) + { + PopupCursor(message, recipient, type); + } + + [Obsolete("Popups are automatically predicted now, just call PopupCursor and the client will handle prediction.")] + public void PopupPredictedCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small) + { + PopupCursor(message, recipient, type); + } + + [Obsolete("Popups are automatically predicted now, just call PopupCoordinates and the client will handle prediction.")] + public void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small) + { + PopupCoordinates(message, coordinates, type); // The recipent was only used for prediction reasons, not as a filter, so we ignore it here. + } + + [Obsolete("Popups are automatically predicted now, just call PopupEntity and the client will handle prediction.")] + public void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small) + { + if (recipient == null) + return; + + PopupEntity(message, recipient.Value, recipient, type); // Only show the popup to the recipient, since this was the original behavior. + } + + [Obsolete("Popups are automatically predicted now, just call PopupEntity and the client will handle prediction.")] + public void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) + { + PopupEntity(message, uid, recipient, type); // Only show the popup to the recipient, since this was the original behavior. + } + + [Obsolete("Popups are automatically predicted now, just call PopupCoordinates and the client will handle prediction.")] + public void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small) + { + PopupCoordinates(message, coordinates, recipient, type); // Only show the popup to the recipient, since this was the original behavior. + } + + [Obsolete("Popups are automatically predicted now, just call PopupEntity and the client will handle prediction.")] + public void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) + { + PopupEntity(message, uid, type); // The recipent was only used for prediction reasons, not as a filter, so we ignore it here. + } + + [Obsolete("Popups are automatically predicted now, just call PopupEntity and the client will handle prediction.")] + public void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, Filter filter, bool recordReplay, PopupType type = PopupType.Small) + { + PopupEntity(message, uid, filter, recordReplay, type); // The recipent was only used for prediction reasons, not as a filter, so we ignore it here. + } + + [Obsolete("Popups are automatically predicted now, just call PopupEntity and the client will handle prediction.")] + public void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) + { + PopupEntity(recipientMessage, othersMessage, uid, recipient, type); } } + +/// +/// Common base for all popup network events. +/// +[Serializable, NetSerializable] +public abstract class PopupEvent(string message, PopupType type, GameTick tick) : EntityEventArgs +{ + /// + /// The message to display. + /// + public string Message = message; + + /// + /// The type of the popup. + /// + public PopupType Type = type; + + /// + /// The game tick at which the popup was created. + /// + public GameTick Tick = tick; +} + +/// +/// Interface for a prediction instance of a popup event. +/// Used to keep track if a popup has already been predicted and displayed on the client side, to avoid duplicate popups. +/// +public interface IPopupPredictionInstance +{ + /// + /// The game tick at which the popup was created. + /// + GameTick Tick { get; } +} + +/// +/// Network event for displaying a popup on the user's cursor. +/// +[Serializable, NetSerializable] +public sealed class PopupCursorEvent(string message, PopupType type, GameTick tick) : PopupEvent(message, type, tick) +{ + /// + /// Creates a new prediction instance for this popup event. + /// + public readonly record struct PredictionInstance(string Message, PopupType Type, GameTick Tick) : IPopupPredictionInstance; +} + +/// +/// Network event for displaying a popup at a world location. +/// +[Serializable, NetSerializable] +public sealed class PopupCoordinatesEvent(string message, PopupType type, GameTick tick, NetCoordinates coordinates, int predictionKey) : PopupEvent(message, type, tick) +{ + /// + /// The coordinates where the popup should be displayed. + /// + public NetCoordinates Coordinates = coordinates; + + /// + /// The key used to identify this popup event for prediction purposes. + /// + public int PredictionKey = predictionKey; + + /// + /// Creates a new prediction instance for this popup event. + /// + /// + /// TODO: remove coords, as they are not used for prediction. + /// + public readonly record struct PredictionInstance(string Message, PopupType Type, GameTick Tick, int PredictionKey) : IPopupPredictionInstance; +} + +/// +/// Network event for displaying a popup above an entity. +/// +[Serializable, NetSerializable] +public sealed class PopupEntityEvent(string message, PopupType type, GameTick tick, NetEntity uid) : PopupEvent(message, type, tick) +{ + /// + /// The entity above which the popup should be displayed. + /// + public NetEntity Uid = uid; + + /// + /// Creates a new prediction instance for this popup event. + /// + public readonly record struct PredictionInstance(string Message, PopupType Type, GameTick Tick, NetEntity Uid) : IPopupPredictionInstance; +} + +/// +/// Used to determine how a popup should appear visually to the client. Caution variants simply have a red color. +/// +/// +/// Actions which can fail or succeed should use a smaller popup for failure and a larger popup for success. +/// Actions which have different popups for the user vs. others should use a larger popup for the user and a smaller popup for others. +/// Actions which result in harm or are otherwise dangerous should always show as the caution variant. +/// +[Serializable, NetSerializable] +public enum PopupType : byte +{ + /// + /// Small popups are the default, and denote actions that may be spammable or are otherwise unimportant. + /// + Small, + SmallCaution, + + /// + /// Medium popups should be used for actions which are not spammable but may not be particularly important. + /// + Medium, + MediumCaution, + + /// + /// Large popups should be used for actions which may be important or very important to one or more users, + /// but is not life-threatening. + /// + Large, + LargeCaution +} diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs index 767cb70580..b44e9a52a2 100644 --- a/Content.Shared/Strip/SharedStrippableSystem.cs +++ b/Content.Shared/Strip/SharedStrippableSystem.cs @@ -158,7 +158,7 @@ public abstract partial class SharedStrippableSystem : EntitySystem if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHandId!)) { - _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop")); + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user); return false; } @@ -166,13 +166,13 @@ public abstract partial class SharedStrippableSystem : EntitySystem if (_inventorySystem.TryGetSlotEntity(target, slot, out _)) { - _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-occupied", ("owner", targetIdentity))); + _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-occupied", ("owner", targetIdentity)), user); return false; } if (!_inventorySystem.CanEquip(user, target, held, slot, out _)) { - _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-equip-message", ("owner", targetIdentity))); + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-equip-message", ("owner", targetIdentity)), user); return false; } @@ -261,7 +261,7 @@ public abstract partial class SharedStrippableSystem : EntitySystem { if (!_inventorySystem.TryGetSlotEntity(target, slot, out var slotItem)) { - _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", Identity.Entity(target, EntityManager)))); + _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", Identity.Entity(target, EntityManager))), user); return false; } @@ -270,7 +270,7 @@ public abstract partial class SharedStrippableSystem : EntitySystem if (!_inventorySystem.CanUnequip(user, target, slot, out var reason)) { - _popupSystem.PopupCursor(Loc.GetString(reason)); + _popupSystem.PopupCursor(Loc.GetString(reason), user); return false; } @@ -375,13 +375,13 @@ public abstract partial class SharedStrippableSystem : EntitySystem if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHandId!)) { - _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop")); + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user); return false; } if (!_handsSystem.CanPickupToHand(target, activeItem.Value, handName, checkActionBlocker: false, handsComp: target.Comp)) { - _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-put-message", ("owner", Identity.Entity(target, EntityManager)))); + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-put-message", ("owner", Identity.Entity(target, EntityManager))), user); return false; } @@ -476,7 +476,7 @@ public abstract partial class SharedStrippableSystem : EntitySystem if (!_handsSystem.TryGetHand(target, handName, out _)) { - _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", Identity.Entity(target, EntityManager)))); + _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", Identity.Entity(target, EntityManager))), user); return false; } @@ -491,7 +491,7 @@ public abstract partial class SharedStrippableSystem : EntitySystem if (!_handsSystem.CanDropHeld(target, handName, false)) { - _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop-message", ("owner", Identity.Entity(target, EntityManager)))); + _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop-message", ("owner", Identity.Entity(target, EntityManager))), user); return false; } @@ -581,7 +581,7 @@ public abstract partial class SharedStrippableSystem : EntitySystem if (ev.Event.InventoryOrHand) { - if ( ev.Event.InsertOrRemove && !CanStripInsertInventory((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) || + if (ev.Event.InsertOrRemove && !CanStripInsertInventory((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) || !ev.Event.InsertOrRemove && !CanStripRemoveInventory(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName)) { ev.Cancel(); @@ -589,7 +589,7 @@ public abstract partial class SharedStrippableSystem : EntitySystem } else { - if ( ev.Event.InsertOrRemove && !CanStripInsertHand((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) || + if (ev.Event.InsertOrRemove && !CanStripInsertHand((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) || !ev.Event.InsertOrRemove && !CanStripRemoveHand(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName)) { ev.Cancel(); diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index 7ac556a240..252bfc32b4 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -376,7 +376,7 @@ public abstract partial class SharedGunSystem : EntitySystem // If they're firing an existing clip then don't play anything. if (shots > 0) { - PopupSystem.PopupCursor(ev.Reason ?? Loc.GetString("gun-magazine-fired-empty")); + PopupSystem.PopupCursor(ev.Reason ?? Loc.GetString("gun-magazine-fired-empty"), user); // Don't spam safety sounds at gun fire rate, play it at a reduced rate. // May cause prediction issues? Needs more tweaking