mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
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>
This commit is contained in:
parent
5dd2f3f43d
commit
964aebb031
8 changed files with 770 additions and 739 deletions
|
|
@ -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<WorldPopupLabel> WorldLabels => _aliveWorldLabels.Values;
|
||||
public IReadOnlyCollection<CursorPopupLabel> CursorLabels => _aliveCursorLabels.Values;
|
||||
|
||||
private readonly Dictionary<WorldPopupData, WorldPopupLabel> _aliveWorldLabels = new();
|
||||
private readonly Dictionary<CursorPopupData, CursorPopupLabel> _aliveCursorLabels = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private readonly List<IPopupPredictionInstance> _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<WorldPopupLabel> WorldLabels => _aliveWorldLabels.Values;
|
||||
public IReadOnlyCollection<CursorPopupLabel> CursorLabels => _aliveCursorLabels.Values;
|
||||
|
||||
private readonly Dictionary<WorldPopupData, WorldPopupLabel> _aliveWorldLabels = new();
|
||||
private readonly Dictionary<CursorPopupData, CursorPopupLabel> _aliveCursorLabels = new();
|
||||
|
||||
public const float MinimumPopupLifetime = 0.7f;
|
||||
public const float MaximumPopupLifetime = 5f;
|
||||
public const float PopupLifetimePerCharacter = 0.04f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeNetworkEvent<PopupCursorEvent>(OnPopupCursorEvent);
|
||||
SubscribeNetworkEvent<PopupCoordinatesEvent>(OnPopupCoordinatesEvent);
|
||||
SubscribeNetworkEvent<PopupEntityEvent>(OnPopupEntityEvent);
|
||||
SubscribeNetworkEvent<RoundRestartCleanupEvent>(OnRoundRestart);
|
||||
_overlay
|
||||
.AddOverlay(new PopupOverlay(
|
||||
_configManager,
|
||||
EntityManager,
|
||||
_playerManager,
|
||||
ProtoMan,
|
||||
_uiManager,
|
||||
_uiManager.GetUIController<PopupUIController>(),
|
||||
_examine,
|
||||
_transform,
|
||||
this));
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_overlay
|
||||
.RemoveOverlay<PopupOverlay>();
|
||||
}
|
||||
|
||||
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<WorldPopupData>();
|
||||
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<CursorPopupData>();
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The original EntityCoordinates of the label.
|
||||
/// </summary>
|
||||
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<PopupUIController>(),
|
||||
_examine,
|
||||
_transform,
|
||||
this));
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
_overlay.RemoveOverlay<PopupOverlay>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interal implementation for both coordinates and entity popups.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal implementation for cursor popups.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Shows a popup at the local user's cursor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the lifetime of a popup based on its text length.
|
||||
/// </summary>
|
||||
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<WorldPopupData>();
|
||||
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<CursorPopupData>();
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The original EntityCoordinates of the label.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// System for displaying small text popups on users' screens.
|
||||
/// </summary>
|
||||
public abstract partial class SharedPopupSystem : EntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// System for displaying small text popups on users' screens.
|
||||
/// </summary>
|
||||
public abstract class SharedPopupSystem : EntitySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows a popup at the local users' cursor. Does nothing on the server.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupCursor(string? message, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a popup at a users' cursor.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="recipient">Client that will see this popup.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a popup at a users' cursor.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="recipient">Client that will see this popup.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupCursor(string?, ICommonSession, PopupType)"/> for use with prediction.
|
||||
/// The local client will show the popup to the recipient. Does nothing on the server.
|
||||
/// </summary>
|
||||
public abstract void PopupPredictedCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupCursor(string?, EntityUid, PopupType)"/> for use with prediction.
|
||||
/// The local client will show the popup to the recipient. Does nothing on the server.
|
||||
/// </summary>
|
||||
public abstract void PopupPredictedCursor(string? message, EntityUid recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a popup at a world location to every entity in PVS range.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="coordinates">The coordinates where to display the message.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Filtered variant of <see cref="PopupCoordinates(string, EntityCoordinates, PopupType)"/>, which should only be used
|
||||
/// if the filtering has to be more specific than simply PVS range based.
|
||||
/// </summary>
|
||||
/// <param name="filter">Filter for the players that will see the popup.</param>
|
||||
/// <param name="recordReplay">If true, this pop-up will be considered as a globally visible pop-up that gets shown during replays.</param>
|
||||
public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool recordReplay, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupCoordinates(string, EntityCoordinates, PopupType)"/> that sends a pop-up to the player attached to some entity.
|
||||
/// </summary>
|
||||
public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupCoordinates(string, EntityCoordinates, PopupType)"/> that sends a pop-up to a specific player.
|
||||
/// </summary>
|
||||
public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupCoordinates(string, EntityCoordinates, PopupType)"/> 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.
|
||||
/// </summary>
|
||||
public abstract void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a popup above an entity for every player in pvs range.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="uid">The UID of the entity.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupEntity(string? message, EntityUid uid, PopupType type=PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupEntity(string, EntityUid, PopupType)"/> that shows the popup only to some specific client.
|
||||
/// </summary>
|
||||
public abstract void PopupEntity(string? message, EntityUid uid, EntityUid recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupEntity(string, EntityUid, PopupType)"/> that shows the popup only to some specific client.
|
||||
/// </summary>
|
||||
public abstract void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Filtered variant of <see cref="PopupEntity(string, EntityUid, PopupType)"/>, which should only be used
|
||||
/// if the filtering has to be more specific than simply PVS range based.
|
||||
/// </summary>
|
||||
public abstract void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupCursor(string, EntityUid, PopupType)"/> that only runs on the client, outside of prediction.
|
||||
/// Useful for shared code that is always ran by both sides to avoid duplicate popups.
|
||||
/// </summary>
|
||||
public abstract void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupEntity(string, EntityUid, EntityUid, PopupType)"/> that only runs on the client, outside of prediction.
|
||||
/// Useful for shared code that is always ran by both sides to avoid duplicate popups.
|
||||
/// </summary>
|
||||
public abstract void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupCoordinates(string, EntityCoordinates, PopupType)"/> that only runs on the client, outside of prediction.
|
||||
/// Useful for shared code that is always ran by both sides to avoid duplicate popups.
|
||||
/// </summary>
|
||||
public abstract void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupEntity(string, EntityUid, EntityUid, PopupType)"/> 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.
|
||||
/// </summary>
|
||||
public abstract void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupEntity(string, EntityUid, Filter, bool, PopupType)"/> 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.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="uid">The entity to display the popup above.</param>
|
||||
/// <param name="recipient">The client that will see this popup locally during prediction.</param>
|
||||
/// <param name="filter">Filter for players that will see the popup from the server.</param>
|
||||
/// <param name="recordReplay">If true, this pop-up will be considered as a globally visible pop-up that gets shown during replays.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually. See: <see cref="PopupType"/>.</param>
|
||||
public abstract void PopupPredicted(string? message, EntityUid uid, EntityUid? recipient, Filter filter, bool recordReplay, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupPredicted(string?, EntityUid, EntityUid?, PopupType)"/> that displays <paramref name="recipientMessage"/>
|
||||
/// to the recipient and <paramref name="othersMessage"/> to everyone else in PVS range.
|
||||
/// </summary>
|
||||
public abstract void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small);
|
||||
}
|
||||
[Dependency] protected IGameTiming Timing = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Common base for all popup network events.
|
||||
/// Shows a popup at a user's cursor.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
}
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="recipient">The entity whose attached player will see the popup.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupCursor(string? message, EntityUid? recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Network event for displaying a popup on the user's cursor.
|
||||
/// Shows a popup at a user's cursor.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PopupCursorEvent : PopupEvent
|
||||
{
|
||||
public PopupCursorEvent(string message, PopupType type) : base(message, type)
|
||||
{
|
||||
}
|
||||
}
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="recipient">The player session that will see the popup.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupCursor(string? message, ICommonSession recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Network event for displaying a popup at a world location.
|
||||
/// Shows a popup at some users' cursors.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PopupCoordinatesEvent : PopupEvent
|
||||
{
|
||||
public NetCoordinates Coordinates { get; }
|
||||
|
||||
public PopupCoordinatesEvent(string message, PopupType type, NetCoordinates coordinates) : base(message, type)
|
||||
{
|
||||
Coordinates = coordinates;
|
||||
}
|
||||
}
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="filter">Filter for the clients that will see this popup.</param>
|
||||
/// <param name="recordReplay">If true, this pop-up will be considered as a globally visible pop-up that gets shown during replays.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupCursor(string? message, Filter filter, bool recordReplay, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Network event for displaying a popup above an entity.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PopupEntityEvent : PopupEvent
|
||||
{
|
||||
public NetEntity Uid { get; }
|
||||
|
||||
public PopupEntityEvent(string message, PopupType type, NetEntity uid) : base(message, type)
|
||||
{
|
||||
Uid = uid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="coordinates">The coordinates where to display the message.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
/// <param name="predictionKey">Additional key used to uniquely identify this popup event for prediction purposes.</param>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
[Serializable, NetSerializable]
|
||||
public enum PopupType : byte
|
||||
public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, PopupType type = PopupType.Small, int predictionKey = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupCoordinates(string, EntityCoordinates, PopupType)"/> that sends a popup to the player attached to some entity.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="coordinates">The coordinates where to display the message.</param>
|
||||
/// <param name="recipient">The entity whose attached player will see the popup.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
/// <param name="predictionKey">Additional key used to uniquely identify this popup event for prediction purposes.</param>
|
||||
public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small, int predictionKey = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupCoordinates(string, EntityCoordinates, PopupType)"/> that sends a popup to a specific player.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="coordinates">The coordinates where to display the message.</param>
|
||||
/// <param name="recipient">The player session that will see the popup.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
/// <param name="predictionKey">Additional key used to uniquely identify this popup event for prediction purposes.</param>
|
||||
public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, ICommonSession recipient, PopupType type = PopupType.Small, int predictionKey = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Filtered variant of <see cref="PopupCoordinates(string, EntityCoordinates, PopupType)"/>, which should only be used
|
||||
/// if the filtering has to be more specific than simply PVS range based.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="coordinates">The coordinates where to display the message.</param>
|
||||
/// <param name="filter">Filter for the clients that will see this popup.</param>
|
||||
/// <param name="recordReplay">If true, this pop-up will be considered as a globally visible pop-up that gets shown during replays.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
/// <param name="predictionKey">Additional key used to uniquely identify this popup event for prediction purposes.</param>
|
||||
public abstract void PopupCoordinates(string? message, EntityCoordinates coordinates, Filter filter, bool recordReplay, PopupType type = PopupType.Small, int predictionKey = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a popup above an entity for every player in PVS range.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="uid">The entity above which to display the popup.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupEntity(string, EntityUid, PopupType)"/> that shows the popup only to some specific client.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="uid">The entity above which to display the popup.</param>
|
||||
/// <param name="recipient">The entity whose attached player will see the popup.</param
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupEntity(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupEntity(string, EntityUid, PopupType)"/> that shows the popup only to some specific client.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="uid">The entity above which to display the popup.</param>
|
||||
/// <param name="recipient">The player session that will see the popup.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Filtered variant of <see cref="PopupEntity(string, EntityUid, PopupType)"/>, which should only be used
|
||||
/// if the filtering has to be more specific than simply PVS range based.
|
||||
/// </summary>
|
||||
/// <param name="message">The message to display.</param>
|
||||
/// <param name="uid">The entity above which to display the popup.</param>
|
||||
/// <param name="filter">Filter for the clients that will see this popup.</param>
|
||||
/// <param name="recordReplay">If true, this pop-up will be considered as a globally visible pop-up that gets shown during replays.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param>
|
||||
public abstract void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small);
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="PopupEntity(string?, EntityUid, PopupType)"/> that displays <paramref name="recipientMessage"/>
|
||||
/// to the recipient and <paramref name="othersMessage"/> to everyone else in PVS range.
|
||||
/// </summary>
|
||||
/// <param name="recipientMessage">The message to display to the recipient.</param>
|
||||
/// <param name="othersMessage">The message to display to everyone else in PVS range.</param>
|
||||
/// <param name="uid">The entity above which to display the popup.</param>
|
||||
/// <param name="recipient">The entity whose attached player will see the recipient message.</param>
|
||||
/// <param name="type">Used to customize how this popup should appear visually.</param
|
||||
public void PopupEntity(string? recipientMessage,
|
||||
string? othersMessage,
|
||||
EntityUid uid,
|
||||
EntityUid? recipient,
|
||||
PopupType type = PopupType.Small)
|
||||
{
|
||||
/// <summary>
|
||||
/// Small popups are the default, and denote actions that may be spammable or are otherwise unimportant.
|
||||
/// </summary>
|
||||
Small,
|
||||
SmallCaution,
|
||||
/// <summary>
|
||||
/// Medium popups should be used for actions which are not spammable but may not be particularly important.
|
||||
/// </summary>
|
||||
Medium,
|
||||
MediumCaution,
|
||||
/// <summary>
|
||||
/// Large popups should be used for actions which may be important or very important to one or more users,
|
||||
/// but is not life-threatening.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Common base for all popup network events.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public abstract class PopupEvent(string message, PopupType type, GameTick tick) : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The message to display.
|
||||
/// </summary>
|
||||
public string Message = message;
|
||||
|
||||
/// <summary>
|
||||
/// The type of the popup.
|
||||
/// </summary>
|
||||
public PopupType Type = type;
|
||||
|
||||
/// <summary>
|
||||
/// The game tick at which the popup was created.
|
||||
/// </summary>
|
||||
public GameTick Tick = tick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IPopupPredictionInstance
|
||||
{
|
||||
/// <summary>
|
||||
/// The game tick at which the popup was created.
|
||||
/// </summary>
|
||||
GameTick Tick { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Network event for displaying a popup on the user's cursor.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PopupCursorEvent(string message, PopupType type, GameTick tick) : PopupEvent(message, type, tick)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new prediction instance for this popup event.
|
||||
/// </summary>
|
||||
public readonly record struct PredictionInstance(string Message, PopupType Type, GameTick Tick) : IPopupPredictionInstance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Network event for displaying a popup at a world location.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PopupCoordinatesEvent(string message, PopupType type, GameTick tick, NetCoordinates coordinates, int predictionKey) : PopupEvent(message, type, tick)
|
||||
{
|
||||
/// <summary>
|
||||
/// The coordinates where the popup should be displayed.
|
||||
/// </summary>
|
||||
public NetCoordinates Coordinates = coordinates;
|
||||
|
||||
/// <summary>
|
||||
/// The key used to identify this popup event for prediction purposes.
|
||||
/// </summary>
|
||||
public int PredictionKey = predictionKey;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new prediction instance for this popup event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// TODO: remove coords, as they are not used for prediction.
|
||||
/// </remarks>
|
||||
public readonly record struct PredictionInstance(string Message, PopupType Type, GameTick Tick, int PredictionKey) : IPopupPredictionInstance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Network event for displaying a popup above an entity.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PopupEntityEvent(string message, PopupType type, GameTick tick, NetEntity uid) : PopupEvent(message, type, tick)
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity above which the popup should be displayed.
|
||||
/// </summary>
|
||||
public NetEntity Uid = uid;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new prediction instance for this popup event.
|
||||
/// </summary>
|
||||
public readonly record struct PredictionInstance(string Message, PopupType Type, GameTick Tick, NetEntity Uid) : IPopupPredictionInstance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to determine how a popup should appear visually to the client. Caution variants simply have a red color.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
[Serializable, NetSerializable]
|
||||
public enum PopupType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Small popups are the default, and denote actions that may be spammable or are otherwise unimportant.
|
||||
/// </summary>
|
||||
Small,
|
||||
SmallCaution,
|
||||
|
||||
/// <summary>
|
||||
/// Medium popups should be used for actions which are not spammable but may not be particularly important.
|
||||
/// </summary>
|
||||
Medium,
|
||||
MediumCaution,
|
||||
|
||||
/// <summary>
|
||||
/// Large popups should be used for actions which may be important or very important to one or more users,
|
||||
/// but is not life-threatening.
|
||||
/// </summary>
|
||||
Large,
|
||||
LargeCaution
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue