ECSatize AlertsSystem (#5559)

This commit is contained in:
Acruid
2022-01-05 00:19:23 -08:00
committed by GitHub
parent 36d4de5e61
commit 5b1cd2dd96
59 changed files with 1069 additions and 1038 deletions

View File

@@ -0,0 +1,16 @@
namespace Content.Shared.Alert;
/// <summary>
/// Every category of alert. Corresponds to category field in alert prototypes defined in YML
/// </summary>
public enum AlertCategory
{
Pressure,
Temperature,
Breathing,
Buckled,
Health,
Piloting,
Hunger,
Thirst
}

View File

@@ -0,0 +1,62 @@
using System;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
namespace Content.Shared.Alert;
/// <summary>
/// Key for an alert which is unique (for equality and hashcode purposes) w.r.t category semantics.
/// I.e., entirely defined by the category, if a category was specified, otherwise
/// falls back to the id.
/// </summary>
[Serializable, NetSerializable]
public struct AlertKey : ISerializationHooks, IPopulateDefaultValues
{
public AlertType? AlertType { get; private set; }
public readonly AlertCategory? AlertCategory;
/// NOTE: if the alert has a category you must pass the category for this to work
/// properly as a key. I.e. if the alert has a category and you pass only the alert type, and you
/// compare this to another AlertKey that has both the category and the same alert type, it will not consider them equal.
public AlertKey(AlertType? alertType, AlertCategory? alertCategory)
{
AlertCategory = alertCategory;
AlertType = alertType;
}
public bool Equals(AlertKey other)
{
// compare only on alert category if we have one
if (AlertCategory.HasValue)
{
return other.AlertCategory == AlertCategory;
}
return AlertType == other.AlertType && AlertCategory == other.AlertCategory;
}
public override bool Equals(object? obj)
{
return obj is AlertKey other && Equals(other);
}
public override int GetHashCode()
{
// use only alert category if we have one
if (AlertCategory.HasValue) return AlertCategory.GetHashCode();
return AlertType.GetHashCode();
}
public void PopulateDefaultValues()
{
AlertType = Alert.AlertType.Error;
}
/// <param name="category">alert category, must not be null</param>
/// <returns>An alert key for the provided alert category. This must only be used for
/// queries and never storage, as it is lacking an alert type.</returns>
public static AlertKey ForCategory(AlertCategory category)
{
return new(null, category);
}
}

View File

@@ -1,41 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
namespace Content.Shared.Alert
{
/// <summary>
/// Provides access to all configured alerts by alert type.
/// </summary>
public class AlertManager
{
[Dependency]
private readonly IPrototypeManager _prototypeManager = default!;
private readonly Dictionary<AlertType, AlertPrototype> _typeToAlert = new();
public void Initialize()
{
foreach (var alert in _prototypeManager.EnumeratePrototypes<AlertPrototype>())
{
if (!_typeToAlert.TryAdd(alert.AlertType, alert))
{
Logger.ErrorS("alert",
"Found alert with duplicate alertType {0} - all alerts must have" +
" a unique alerttype, this one will be skipped", alert.AlertType);
}
}
}
/// <summary>
/// Tries to get the alert of the indicated type
/// </summary>
/// <returns>true if found</returns>
public bool TryGet(AlertType alertType, [NotNullWhen(true)] out AlertPrototype? alert)
{
return _typeToAlert.TryGetValue(alertType, out alert);
}
}
}

View File

@@ -3,7 +3,6 @@ using System.Globalization;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
@@ -142,61 +141,4 @@ namespace Content.Shared.Alert
}
}
}
/// <summary>
/// Key for an alert which is unique (for equality and hashcode purposes) w.r.t category semantics.
/// I.e., entirely defined by the category, if a category was specified, otherwise
/// falls back to the id.
/// </summary>
[Serializable, NetSerializable]
public struct AlertKey : ISerializationHooks, IPopulateDefaultValues
{
public AlertType? AlertType { get; private set; }
public readonly AlertCategory? AlertCategory;
/// NOTE: if the alert has a category you must pass the category for this to work
/// properly as a key. I.e. if the alert has a category and you pass only the alert type, and you
/// compare this to another AlertKey that has both the category and the same alert type, it will not consider them equal.
public AlertKey(AlertType? alertType, AlertCategory? alertCategory)
{
AlertCategory = alertCategory;
AlertType = alertType;
}
public bool Equals(AlertKey other)
{
// compare only on alert category if we have one
if (AlertCategory.HasValue)
{
return other.AlertCategory == AlertCategory;
}
return AlertType == other.AlertType && AlertCategory == other.AlertCategory;
}
public override bool Equals(object? obj)
{
return obj is AlertKey other && Equals(other);
}
public override int GetHashCode()
{
// use only alert category if we have one
if (AlertCategory.HasValue) return AlertCategory.GetHashCode();
return AlertType.GetHashCode();
}
public void PopulateDefaultValues()
{
AlertType = Alert.AlertType.Error;
}
/// <param name="category">alert category, must not be null</param>
/// <returns>An alert key for the provided alert category. This must only be used for
/// queries and never storage, as it is lacking an alert type.</returns>
public static AlertKey ForCategory(AlertCategory category)
{
return new(null, category);
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
using Robust.Shared.Serialization;
namespace Content.Shared.Alert;
[Serializable, NetSerializable]
public struct AlertState
{
public short? Severity;
public (TimeSpan, TimeSpan)? Cooldown;
public AlertType Type;
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.GameObjects;
namespace Content.Shared.Alert;
/// <summary>
/// Raised when the AlertSystem needs alert sources to recalculate their alert states and set them.
/// </summary>
public class AlertSyncEvent : EntityEventArgs
{
public EntityUid Euid { get; }
public AlertSyncEvent(EntityUid euid)
{
Euid = euid;
}
}

View File

@@ -1,20 +1,5 @@
namespace Content.Shared.Alert
{
/// <summary>
/// Every category of alert. Corresponds to category field in alert prototypes defined in YML
/// </summary>
public enum AlertCategory
{
Pressure,
Temperature,
Breathing,
Buckled,
Health,
Piloting,
Hunger,
Thirst
}
/// <summary>
/// Every kind of alert. Corresponds to alertType field in alert prototypes defined in YML
/// NOTE: Using byte for a compact encoding when sending this in messages, can upgrade

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Alert;
/// <summary>
/// Handles the icons on the right side of the screen.
/// Should only be used for player-controlled entities.
/// </summary>
[RegisterComponent]
[NetworkedComponent]
[ComponentProtoName("Alerts")]
public class AlertsComponent : Component
{
[ViewVariables] public Dictionary<AlertKey, AlertState> Alerts = new();
}

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.Alert;
[Serializable, NetSerializable]
public class AlertsComponentState : ComponentState
{
public Dictionary<AlertKey, AlertState> Alerts;
public AlertsComponentState(Dictionary<AlertKey, AlertState> alerts)
{
Alerts = alerts;
}
}

View File

@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
namespace Content.Shared.Alert;
public abstract class AlertsSystem : EntitySystem
{
[Dependency]
private readonly IPrototypeManager _prototypeManager = default!;
private readonly Dictionary<AlertType, AlertPrototype> _typeToAlert = new();
public IReadOnlyDictionary<AlertKey, AlertState>? GetActiveAlerts(EntityUid euid)
{
return EntityManager.TryGetComponent(euid, out AlertsComponent comp)
? comp.Alerts
: null;
}
public bool IsShowingAlert(EntityUid euid, AlertType alertType)
{
if (!EntityManager.TryGetComponent(euid, out AlertsComponent alertsComponent))
return false;
if (TryGet(alertType, out var alert))
{
return alertsComponent.Alerts.ContainsKey(alert.AlertKey);
}
Logger.DebugS("alert", "unknown alert type {0}", alertType);
return false;
}
/// <returns>true iff an alert of the indicated alert category is currently showing</returns>
public bool IsShowingAlertCategory(EntityUid euid, AlertCategory alertCategory)
{
return EntityManager.TryGetComponent(euid, out AlertsComponent alertsComponent)
&& alertsComponent.Alerts.ContainsKey(AlertKey.ForCategory(alertCategory));
}
public bool TryGetAlertState(EntityUid euid, AlertKey key, out AlertState alertState)
{
if (EntityManager.TryGetComponent(euid, out AlertsComponent alertsComponent))
return alertsComponent.Alerts.TryGetValue(key, out alertState);
alertState = default;
return false;
}
/// <summary>
/// Shows the alert. If the alert or another alert of the same category is already showing,
/// it will be updated / replaced with the specified values.
/// </summary>
/// <param name="euid"></param>
/// <param name="alertType">type of the alert to set</param>
/// <param name="severity">severity, if supported by the alert</param>
/// <param name="cooldown">cooldown start and end, if null there will be no cooldown (and it will
/// be erased if there is currently a cooldown for the alert)</param>
public void ShowAlert(EntityUid euid, AlertType alertType, short? severity = null, (TimeSpan, TimeSpan)? cooldown = null)
{
if (!EntityManager.TryGetComponent(euid, out AlertsComponent alertsComponent))
return;
if (TryGet(alertType, out var alert))
{
// Check whether the alert category we want to show is already being displayed, with the same type,
// severity, and cooldown.
if (alertsComponent.Alerts.TryGetValue(alert.AlertKey, out var alertStateCallback) &&
alertStateCallback.Type == alertType &&
alertStateCallback.Severity == severity &&
alertStateCallback.Cooldown == cooldown)
{
return;
}
// In the case we're changing the alert type but not the category, we need to remove it first.
alertsComponent.Alerts.Remove(alert.AlertKey);
alertsComponent.Alerts[alert.AlertKey] = new AlertState
{ Cooldown = cooldown, Severity = severity, Type = alertType };
AfterShowAlert(alertsComponent);
alertsComponent.Dirty();
}
else
{
Logger.ErrorS("alert", "Unable to show alert {0}, please ensure this alertType has" +
" a corresponding YML alert prototype",
alertType);
}
}
/// <summary>
/// Clear the alert with the given category, if one is currently showing.
/// </summary>
public void ClearAlertCategory(EntityUid euid, AlertCategory category)
{
if(!EntityManager.TryGetComponent(euid, out AlertsComponent alertsComponent))
return;
var key = AlertKey.ForCategory(category);
if (!alertsComponent.Alerts.Remove(key))
{
return;
}
AfterClearAlert(alertsComponent);
alertsComponent.Dirty();
}
/// <summary>
/// Clear the alert of the given type if it is currently showing.
/// </summary>
public void ClearAlert(EntityUid euid, AlertType alertType)
{
if (!EntityManager.TryGetComponent(euid, out AlertsComponent alertsComponent))
return;
if (TryGet(alertType, out var alert))
{
if (!alertsComponent.Alerts.Remove(alert.AlertKey))
{
return;
}
AfterClearAlert(alertsComponent);
alertsComponent.Dirty();
}
else
{
Logger.ErrorS("alert", "unable to clear alert, unknown alertType {0}", alertType);
}
}
/// <summary>
/// Invoked after showing an alert prior to dirtying the component
/// </summary>
/// <param name="alertsComponent"></param>
protected virtual void AfterShowAlert(AlertsComponent alertsComponent) { }
/// <summary>
/// Invoked after clearing an alert prior to dirtying the component
/// </summary>
/// <param name="alertsComponent"></param>
protected virtual void AfterClearAlert(AlertsComponent alertsComponent) { }
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AlertsComponent, ComponentStartup>((uid, _, _) => RaiseLocalEvent(uid, new AlertSyncEvent(uid)));
SubscribeLocalEvent<AlertsComponent, ComponentShutdown>((uid, _, _) => HandleComponentShutdown(uid));
SubscribeLocalEvent<AlertsComponent, ComponentGetState>(ClientAlertsGetState);
SubscribeNetworkEvent<ClickAlertEvent>(HandleClickAlert);
LoadPrototypes();
_prototypeManager.PrototypesReloaded += HandlePrototypesReloaded;
}
protected virtual void HandleComponentShutdown(EntityUid uid)
{
RaiseLocalEvent(uid, new AlertSyncEvent(uid));
}
public override void Shutdown()
{
_prototypeManager.PrototypesReloaded -= HandlePrototypesReloaded;
base.Shutdown();
}
private void HandlePrototypesReloaded(PrototypesReloadedEventArgs obj)
{
LoadPrototypes();
}
protected virtual void LoadPrototypes()
{
_typeToAlert.Clear();
foreach (var alert in _prototypeManager.EnumeratePrototypes<AlertPrototype>())
{
if (!_typeToAlert.TryAdd(alert.AlertType, alert))
{
Logger.ErrorS("alert",
"Found alert with duplicate alertType {0} - all alerts must have" +
" a unique alerttype, this one will be skipped", alert.AlertType);
}
}
}
/// <summary>
/// Tries to get the alert of the indicated type
/// </summary>
/// <returns>true if found</returns>
public bool TryGet(AlertType alertType, [NotNullWhen(true)] out AlertPrototype? alert)
{
return _typeToAlert.TryGetValue(alertType, out alert);
}
private void HandleClickAlert(ClickAlertEvent msg, EntitySessionEventArgs args)
{
var player = args.SenderSession.AttachedEntity;
if (player is null || !EntityManager.TryGetComponent<AlertsComponent>(player, out var alertComp)) return;
if (!IsShowingAlert(player.Value, msg.Type))
{
Logger.DebugS("alert", "user {0} attempted to" +
" click alert {1} which is not currently showing for them",
EntityManager.GetComponent<MetaDataComponent>(player.Value).EntityName, msg.Type);
return;
}
if (!TryGet(msg.Type, out var alert))
{
Logger.WarningS("alert", "unrecognized encoded alert {0}", msg.Type);
return;
}
alert.OnClick?.AlertClicked(player.Value);
}
private static void ClientAlertsGetState(EntityUid uid, AlertsComponent component, ref ComponentGetState args)
{
args.State = new AlertsComponentState(component.Alerts);
}
}

View File

@@ -0,0 +1,19 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.Alert;
/// <summary>
/// A message that calls the click interaction on a alert
/// </summary>
[Serializable, NetSerializable]
public class ClickAlertEvent : EntityEventArgs
{
public readonly AlertType Type;
public ClickAlertEvent(AlertType alertType)
{
Type = alertType;
}
}

View File

@@ -1,5 +1,4 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects;
namespace Content.Shared.Alert
{
@@ -11,25 +10,7 @@ namespace Content.Shared.Alert
/// <summary>
/// Invoked on server side when user clicks an alert.
/// </summary>
/// <param name="args"></param>
void AlertClicked(ClickAlertEventArgs args);
}
public class ClickAlertEventArgs : EventArgs
{
/// <summary>
/// Player clicking the alert
/// </summary>
public readonly EntityUid Player;
/// <summary>
/// Alert that was clicked
/// </summary>
public readonly AlertPrototype Alert;
public ClickAlertEventArgs(EntityUid player, AlertPrototype alert)
{
Player = player;
Alert = alert;
}
/// <param name="player"></param>
void AlertClicked(EntityUid player);
}
}

View File

@@ -1,204 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Players;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Shared.Alert
{
/// <summary>
/// Handles the icons on the right side of the screen.
/// Should only be used for player-controlled entities.
/// </summary>
[NetworkedComponent()]
public abstract class SharedAlertsComponent : Component
{
[Dependency]
protected readonly AlertManager AlertManager = default!;
public override string Name => "Alerts";
[ViewVariables] private Dictionary<AlertKey, AlertState> _alerts = new();
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not AlertsComponentState state)
{
return;
}
_alerts = state.Alerts;
}
public override ComponentState GetComponentState()
{
return new AlertsComponentState(_alerts);
}
/// <returns>true iff an alert of the indicated alert category is currently showing</returns>
public bool IsShowingAlertCategory(AlertCategory alertCategory)
{
return IsShowingAlert(AlertKey.ForCategory(alertCategory));
}
/// <returns>true iff an alert of the indicated id is currently showing</returns>
public bool IsShowingAlert(AlertType alertType)
{
if (AlertManager.TryGet(alertType, out var alert))
{
return IsShowingAlert(alert.AlertKey);
}
Logger.DebugS("alert", "unknown alert type {0}", alertType);
return false;
}
/// <returns>true iff an alert of the indicated key is currently showing</returns>
protected bool IsShowingAlert(AlertKey alertKey)
{
return _alerts.ContainsKey(alertKey);
}
protected IEnumerable<KeyValuePair<AlertKey, AlertState>> EnumerateAlertStates()
{
return _alerts;
}
protected bool TryGetAlertState(AlertKey key, out AlertState alertState)
{
return _alerts.TryGetValue(key, out alertState);
}
/// <summary>
/// Shows the alert. If the alert or another alert of the same category is already showing,
/// it will be updated / replaced with the specified values.
/// </summary>
/// <param name="alertType">type of the alert to set</param>
/// <param name="severity">severity, if supported by the alert</param>
/// <param name="cooldown">cooldown start and end, if null there will be no cooldown (and it will
/// be erased if there is currently a cooldown for the alert)</param>
public void ShowAlert(AlertType alertType, short? severity = null, (TimeSpan, TimeSpan)? cooldown = null)
{
if (AlertManager.TryGet(alertType, out var alert))
{
// Check whether the alert category we want to show is already being displayed, with the same type,
// severity, and cooldown.
if (_alerts.TryGetValue(alert.AlertKey, out var alertStateCallback) &&
alertStateCallback.Type == alertType &&
alertStateCallback.Severity == severity &&
alertStateCallback.Cooldown == cooldown)
{
return;
}
// In the case we're changing the alert type but not the category, we need to remove it first.
_alerts.Remove(alert.AlertKey);
_alerts[alert.AlertKey] = new AlertState
{Cooldown = cooldown, Severity = severity, Type=alertType};
AfterShowAlert();
Dirty();
}
else
{
Logger.ErrorS("alert", "Unable to show alert {0}, please ensure this alertType has" +
" a corresponding YML alert prototype",
alertType);
}
}
/// <summary>
/// Clear the alert with the given category, if one is currently showing.
/// </summary>
public void ClearAlertCategory(AlertCategory category)
{
var key = AlertKey.ForCategory(category);
if (!_alerts.Remove(key))
{
return;
}
AfterClearAlert();
Dirty();
}
/// <summary>
/// Clear the alert of the given type if it is currently showing.
/// </summary>
public void ClearAlert(AlertType alertType)
{
if (AlertManager.TryGet(alertType, out var alert))
{
if (!_alerts.Remove(alert.AlertKey))
{
return;
}
AfterClearAlert();
Dirty();
}
else
{
Logger.ErrorS("alert", "unable to clear alert, unknown alertType {0}", alertType);
}
}
/// <summary>
/// Invoked after showing an alert prior to dirtying the component
/// </summary>
protected virtual void AfterShowAlert() { }
/// <summary>
/// Invoked after clearing an alert prior to dirtying the component
/// </summary>
protected virtual void AfterClearAlert() { }
}
[Serializable, NetSerializable]
public class AlertsComponentState : ComponentState
{
public Dictionary<AlertKey, AlertState> Alerts;
public AlertsComponentState(Dictionary<AlertKey, AlertState> alerts)
{
Alerts = alerts;
}
}
/// <summary>
/// A message that calls the click interaction on a alert
/// </summary>
[Serializable, NetSerializable]
#pragma warning disable 618
public class ClickAlertMessage : ComponentMessage
#pragma warning restore 618
{
public readonly AlertType Type;
public ClickAlertMessage(AlertType alertType)
{
Directed = true;
Type = alertType;
}
}
[Serializable, NetSerializable]
public struct AlertState
{
public short? Severity;
public (TimeSpan, TimeSpan)? Cooldown;
public AlertType Type;
}
}