Alerts System and UI (#2529)
* #272 add bordered panel for effects bar * #272 avoid mouse overlapping tooltip when near edges, change tooltip colors to match mockups * #272 WIP defining status effect states as YML and sending them as encoded integers * #272 refactor to use new alert system * #272 refactor to use new alert system * #272 fix various bugs with new alert system and update alerts to have color * #272 WIP * #272 rename status effects to alerts * #272 WIP reworking alert internals to avoid code dup and eliminate enum * #272 refactor alerts to use categories and fix various bugs * #272 more alert bugfixes * #272 alert ordering * #272 callback-based approach for alert clicks * #272 add debug commands for alerts * #272 utilize new GridContainer capabilities for sizing of alerts tab * #272 scale alerts height based on window size * #272 fix tooltip flicker * #272 transparent alert panel * #272 adjust styles to match injazz mockups more, add cooldown info in tooltip * #272 adjust styles to match injazz mockups more, add cooldown info in tooltip * #272 alert prototype tests * #272 alert manager tests * #272 alert order tests * #272 simple unit test for alerts component * #272 integration test for alerts * #272 rework alerts to use enums instead of id / category * #272 various cleanups for PR * #272 use byte for more compact alert messages * #272 rename StatusEffects folder to Alerts, add missing NetSerializable
This commit is contained in:
124
Content.Shared/Alert/AlertManager.cs
Normal file
124
Content.Shared/Alert/AlertManager.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Prototypes.Kitchen;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Alert
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to all configured alerts. Ability to encode/decode a given state
|
||||
/// to an int.
|
||||
/// </summary>
|
||||
public class AlertManager
|
||||
{
|
||||
[Dependency]
|
||||
private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private AlertPrototype[] _orderedAlerts;
|
||||
private Dictionary<AlertType, byte> _typeToIndex;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// order by type value so we can map between the id and an integer index and use
|
||||
// the index for compact alert change messages
|
||||
_orderedAlerts =
|
||||
_prototypeManager.EnumeratePrototypes<AlertPrototype>()
|
||||
.OrderBy(prototype => prototype.AlertType).ToArray();
|
||||
_typeToIndex = new Dictionary<AlertType, byte>();
|
||||
|
||||
for (var i = 0; i < _orderedAlerts.Length; i++)
|
||||
{
|
||||
if (i > byte.MaxValue)
|
||||
{
|
||||
Logger.ErrorS("alert", "too many alerts for byte encoding ({0})! encoding will need" +
|
||||
" to be changed to use a ushort rather than byte", _typeToIndex.Count);
|
||||
break;
|
||||
}
|
||||
if (!_typeToIndex.TryAdd(_orderedAlerts[i].AlertType, (byte) i))
|
||||
{
|
||||
Logger.ErrorS("alert",
|
||||
"Found alert with duplicate id {0}", _orderedAlerts[i].AlertType);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the alert of the indicated type
|
||||
/// </summary>
|
||||
/// <returns>true if found</returns>
|
||||
public bool TryGet(AlertType alertType, out AlertPrototype alert)
|
||||
{
|
||||
if (_typeToIndex.TryGetValue(alertType, out var idx))
|
||||
{
|
||||
alert = _orderedAlerts[idx];
|
||||
return true;
|
||||
}
|
||||
|
||||
alert = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the alert of the indicated type along with its encoding
|
||||
/// </summary>
|
||||
/// <returns>true if found</returns>
|
||||
public bool TryGetWithEncoded(AlertType alertType, out AlertPrototype alert, out byte encoded)
|
||||
{
|
||||
if (_typeToIndex.TryGetValue(alertType, out var idx))
|
||||
{
|
||||
alert = _orderedAlerts[idx];
|
||||
encoded = (byte) idx;
|
||||
return true;
|
||||
}
|
||||
|
||||
alert = null;
|
||||
encoded = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the compact encoded representation of this alert
|
||||
/// </summary>
|
||||
/// <returns>true if successful</returns>
|
||||
public bool TryEncode(AlertPrototype alert, out byte encoded)
|
||||
{
|
||||
return TryEncode(alert.AlertType, out encoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the compact encoded representation of the alert with
|
||||
/// the indicated id
|
||||
/// </summary>
|
||||
/// <returns>true if successful</returns>
|
||||
public bool TryEncode(AlertType alertType, out byte encoded)
|
||||
{
|
||||
if (_typeToIndex.TryGetValue(alertType, out var idx))
|
||||
{
|
||||
encoded = idx;
|
||||
return true;
|
||||
}
|
||||
|
||||
encoded = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the alert from the encoded representation
|
||||
/// </summary>
|
||||
/// <returns>true if successful</returns>
|
||||
public bool TryDecode(byte encodedAlert, out AlertPrototype alert)
|
||||
{
|
||||
if (encodedAlert >= _orderedAlerts.Length)
|
||||
{
|
||||
alert = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
alert = _orderedAlerts[encodedAlert];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
Content.Shared/Alert/AlertOrderPrototype.cs
Normal file
81
Content.Shared/Alert/AlertOrderPrototype.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Shared.Alert
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the order of alerts so they show up in a consistent order.
|
||||
/// </summary>
|
||||
[Prototype("alertOrder")]
|
||||
public class AlertOrderPrototype : IPrototype, IComparer<AlertPrototype>
|
||||
{
|
||||
private Dictionary<AlertType, int> _typeToIdx = new Dictionary<AlertType, int>();
|
||||
private Dictionary<AlertCategory, int> _categoryToIdx = new Dictionary<AlertCategory, int>();
|
||||
|
||||
public void LoadFrom(YamlMappingNode mapping)
|
||||
{
|
||||
if (!mapping.TryGetNode("order", out YamlSequenceNode orderMapping)) return;
|
||||
|
||||
int i = 0;
|
||||
foreach (var entryYaml in orderMapping)
|
||||
{
|
||||
var orderEntry = (YamlMappingNode) entryYaml;
|
||||
var serializer = YamlObjectSerializer.NewReader(orderEntry);
|
||||
if (serializer.TryReadDataField("category", out AlertCategory alertCategory))
|
||||
{
|
||||
_categoryToIdx[alertCategory] = i++;
|
||||
}
|
||||
else if (serializer.TryReadDataField("alertType", out AlertType alertType))
|
||||
{
|
||||
_typeToIdx[alertType] = i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int GetOrderIndex(AlertPrototype alert)
|
||||
{
|
||||
if (_typeToIdx.TryGetValue(alert.AlertType, out var idx))
|
||||
{
|
||||
return idx;
|
||||
}
|
||||
if (alert.Category != null &&
|
||||
_categoryToIdx.TryGetValue((AlertCategory) alert.Category, out idx))
|
||||
{
|
||||
return idx;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int Compare(AlertPrototype x, AlertPrototype y)
|
||||
{
|
||||
if ((x == null) && (y == null)) return 0;
|
||||
if (x == null) return 1;
|
||||
if (y == null) return -1;
|
||||
var idx = GetOrderIndex(x);
|
||||
var idy = GetOrderIndex(y);
|
||||
if (idx == -1 && idy == -1)
|
||||
{
|
||||
// break ties by type value
|
||||
return x.AlertType - y.AlertType;
|
||||
}
|
||||
|
||||
if (idx == -1) return 1;
|
||||
if (idy == -1) return -1;
|
||||
var result = idx - idy;
|
||||
// not strictly necessary (we don't care about ones that go at the same index)
|
||||
// but it makes the sort stable
|
||||
if (result == 0)
|
||||
{
|
||||
// break ties by type value
|
||||
return x.AlertType - y.AlertType;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
189
Content.Shared/Alert/AlertPrototype.cs
Normal file
189
Content.Shared/Alert/AlertPrototype.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Shared.Alert
|
||||
{
|
||||
/// <summary>
|
||||
/// An alert popup with associated icon, tooltip, and other data.
|
||||
/// </summary>
|
||||
[Prototype("alert")]
|
||||
public class AlertPrototype : IPrototype
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of alert, no 2 alert prototypes should have the same one.
|
||||
/// </summary>
|
||||
public AlertType AlertType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the icon (png) to show in alert bar. If severity levels are supported,
|
||||
/// this should be the path to the icon without the severity number
|
||||
/// (i.e. hot.png if there is hot1.png and hot2.png). Use <see cref="GetIconPath"/>
|
||||
/// to get the correct icon path for a particular severity level.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string IconPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name to show in tooltip window. Accepts formatting.
|
||||
/// </summary>
|
||||
public FormattedMessage Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Description to show in tooltip window. Accepts formatting.
|
||||
/// </summary>
|
||||
public FormattedMessage Description { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Category the alert belongs to. Only one alert of a given category
|
||||
/// can be shown at a time. If one is shown while another is already being shown,
|
||||
/// it will be replaced. This can be useful for categories of alerts which should naturally
|
||||
/// replace each other and are mutually exclusive, for example lowpressure / highpressure,
|
||||
/// hot / cold. If left unspecified, the alert will not replace or be replaced by any other alerts.
|
||||
/// </summary>
|
||||
public AlertCategory? Category { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Key which is unique w.r.t category semantics (alerts with same category have equal keys,
|
||||
/// alerts with no category have different keys).
|
||||
/// </summary>
|
||||
public AlertKey AlertKey { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// -1 (no effect) unless MaxSeverity is specified. Defaults to 1. Minimum severity level supported by this state.
|
||||
/// </summary>
|
||||
public short MinSeverity => MaxSeverity == -1 ? (short) -1 : _minSeverity;
|
||||
private short _minSeverity;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum severity level supported by this state. -1 (default) indicates
|
||||
/// no severity levels are supported by the state.
|
||||
/// </summary>
|
||||
public short MaxSeverity { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this state support severity levels
|
||||
/// </summary>
|
||||
public bool SupportsSeverity => MaxSeverity != -1;
|
||||
|
||||
public void LoadFrom(YamlMappingNode mapping)
|
||||
{
|
||||
var serializer = YamlObjectSerializer.NewReader(mapping);
|
||||
|
||||
serializer.DataField(this, x => x.IconPath, "icon", string.Empty);
|
||||
serializer.DataField(this, x => x.MaxSeverity, "maxSeverity", (short) -1);
|
||||
serializer.DataField(ref _minSeverity, "minSeverity", (short) 1);
|
||||
|
||||
serializer.DataReadFunction("name", string.Empty,
|
||||
s => Name = FormattedMessage.FromMarkup(s));
|
||||
serializer.DataReadFunction("description", string.Empty,
|
||||
s => Description = FormattedMessage.FromMarkup(s));
|
||||
|
||||
serializer.DataField(this, x => x.AlertType, "alertType", AlertType.Error);
|
||||
if (AlertType == AlertType.Error)
|
||||
{
|
||||
Logger.ErrorS("alert", "missing or invalid alertType for alert with name {0}", Name);
|
||||
}
|
||||
|
||||
if (serializer.TryReadDataField("category", out AlertCategory alertCategory))
|
||||
{
|
||||
Category = alertCategory;
|
||||
}
|
||||
AlertKey = new AlertKey(AlertType, Category);
|
||||
}
|
||||
|
||||
/// <param name="severity">severity level, if supported by this alert</param>
|
||||
/// <returns>the icon path to the texture for the provided severity level</returns>
|
||||
public string GetIconPath(short? severity = null)
|
||||
{
|
||||
if (!SupportsSeverity && severity != null)
|
||||
{
|
||||
Logger.WarningS("alert", "attempted to get icon path for severity level for alert {0}, but" +
|
||||
" this alert does not support severity levels", AlertType);
|
||||
}
|
||||
if (!SupportsSeverity) return IconPath;
|
||||
if (severity == null)
|
||||
{
|
||||
Logger.WarningS("alert", "attempted to get icon path without severity level for alert {0}," +
|
||||
" but this alert requires a severity level. Using lowest" +
|
||||
" valid severity level instead...", AlertType);
|
||||
severity = MinSeverity;
|
||||
}
|
||||
|
||||
if (severity < MinSeverity)
|
||||
{
|
||||
Logger.WarningS("alert", "attempted to get icon path with severity level {0} for alert {1}," +
|
||||
" but the minimum severity level for this alert is {2}. Using" +
|
||||
" lowest valid severity level instead...", severity, AlertType, MinSeverity);
|
||||
severity = MinSeverity;
|
||||
}
|
||||
if (severity > MaxSeverity)
|
||||
{
|
||||
Logger.WarningS("alert", "attempted to get icon path with severity level {0} for alert {1}," +
|
||||
" but the max severity level for this alert is {2}. Using" +
|
||||
" highest valid severity level instead...", severity, AlertType, MaxSeverity);
|
||||
severity = MaxSeverity;
|
||||
}
|
||||
|
||||
// split and add the severity number to the path
|
||||
var ext = IconPath.LastIndexOf('.');
|
||||
return IconPath.Substring(0, ext) + severity + IconPath.Substring(ext, IconPath.Length - ext);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
private readonly AlertType? _alertType;
|
||||
private 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 ID, and you
|
||||
/// compare this to another AlertKey that has both the category and the same ID, it will not consider them equal.
|
||||
public AlertKey(AlertType? alertType, AlertCategory? alertCategory)
|
||||
{
|
||||
// if there is a category, ignore the alerttype.
|
||||
if (alertCategory != null)
|
||||
{
|
||||
_alertCategory = alertCategory;
|
||||
_alertType = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_alertCategory = null;
|
||||
_alertType = alertType;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(AlertKey other)
|
||||
{
|
||||
return _alertType == other._alertType && _alertCategory == other._alertCategory;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is AlertKey other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(_alertType, _alertCategory);
|
||||
}
|
||||
|
||||
/// <param name="category">alert category, must not be null</param>
|
||||
/// <returns>An alert key for the provided alert category</returns>
|
||||
public static AlertKey ForCategory(AlertCategory category)
|
||||
{
|
||||
return new AlertKey(null, category);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Content.Shared/Alert/AlertType.cs
Normal file
52
Content.Shared/Alert/AlertType.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
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,
|
||||
Buckled,
|
||||
Health,
|
||||
Piloting,
|
||||
Hunger,
|
||||
Thirst
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every kind of alert. Corresponds to alertType field in alert prototypes defined in YML
|
||||
/// </summary>
|
||||
public enum AlertType
|
||||
{
|
||||
Error,
|
||||
LowPressure,
|
||||
HighPressure,
|
||||
Fire,
|
||||
Cold,
|
||||
Hot,
|
||||
Weightless,
|
||||
Stun,
|
||||
Handcuffed,
|
||||
Buckled,
|
||||
HumanCrit,
|
||||
HumanDead,
|
||||
HumanHealth,
|
||||
PilotingShuttle,
|
||||
Overfed,
|
||||
Peckish,
|
||||
Starving,
|
||||
Overhydrated,
|
||||
Thirsty,
|
||||
Parched,
|
||||
Pulled,
|
||||
Pulling,
|
||||
Debug1,
|
||||
Debug2,
|
||||
Debug3,
|
||||
Debug4,
|
||||
Debug5,
|
||||
Debug6
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user