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:
chairbender
2020-11-09 20:22:19 -08:00
committed by GitHub
parent c82199610d
commit 5f788c3318
86 changed files with 2305 additions and 598 deletions

View File

@@ -11,6 +11,7 @@ using Content.Client.UserInterface.AdminMenu;
using Content.Client.UserInterface.Stylesheets; using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility; using Content.Client.Utility;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Content.Shared.Alert;
using Robust.Shared.IoC; using Robust.Shared.IoC;
namespace Content.Client namespace Content.Client
@@ -35,6 +36,7 @@ namespace Content.Client
IoCManager.Register<IClickMapManager, ClickMapManager>(); IoCManager.Register<IClickMapManager, ClickMapManager>();
IoCManager.Register<IStationEventManager, StationEventManager>(); IoCManager.Register<IStationEventManager, StationEventManager>();
IoCManager.Register<IAdminMenuManager, AdminMenuManager>(); IoCManager.Register<IAdminMenuManager, AdminMenuManager>();
IoCManager.Register<AlertManager, AlertManager>();
} }
} }
} }

View File

@@ -22,6 +22,7 @@ using Content.Shared.GameObjects.Components.Power.AME;
using Content.Shared.GameObjects.Components.Research; using Content.Shared.GameObjects.Components.Research;
using Content.Shared.GameObjects.Components.VendingMachines; using Content.Shared.GameObjects.Components.VendingMachines;
using Content.Shared.Kitchen; using Content.Shared.Kitchen;
using Content.Shared.Alert;
using Robust.Client; using Robust.Client;
using Robust.Client.Interfaces; using Robust.Client.Interfaces;
using Robust.Client.Interfaces.Graphics.Overlays; using Robust.Client.Interfaces.Graphics.Overlays;
@@ -150,6 +151,7 @@ namespace Content.Client
IoCManager.Resolve<IClientPreferencesManager>().Initialize(); IoCManager.Resolve<IClientPreferencesManager>().Initialize();
IoCManager.Resolve<IStationEventManager>().Initialize(); IoCManager.Resolve<IStationEventManager>().Initialize();
IoCManager.Resolve<IAdminMenuManager>().Initialize(); IoCManager.Resolve<IAdminMenuManager>().Initialize();
IoCManager.Resolve<AlertManager>().Initialize();
_baseClient.RunLevelChanged += (sender, args) => _baseClient.RunLevelChanged += (sender, args) =>
{ {

View File

@@ -0,0 +1,93 @@
#nullable enable
using System;
using Content.Client.UserInterface;
using Content.Client.Utility;
using Content.Shared.Alert;
using OpenToolkit.Mathematics;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.GameObjects.Components.Mobs
{
public class AlertControl : BaseButton
{
public AlertPrototype Alert { get; }
/// <summary>
/// Total duration of the cooldown in seconds. Null if no duration / cooldown.
/// </summary>
public int? TotalDuration { get; set; }
private short? _severity;
private readonly TextureRect _icon;
private CooldownGraphic _cooldownGraphic;
private readonly IResourceCache _resourceCache;
/// <summary>
/// Creates an alert control reflecting the indicated alert + state
/// </summary>
/// <param name="alert">alert to display</param>
/// <param name="severity">severity of alert, null if alert doesn't have severity levels</param>
/// <param name="resourceCache">resourceCache to use to load alert icon textures</param>
public AlertControl(AlertPrototype alert, short? severity, IResourceCache resourceCache)
{
_resourceCache = resourceCache;
Alert = alert;
_severity = severity;
var texture = _resourceCache.GetTexture(alert.GetIconPath(_severity));
_icon = new TextureRect
{
TextureScale = (2, 2),
Texture = texture
};
Children.Add(_icon);
_cooldownGraphic = new CooldownGraphic();
Children.Add(_cooldownGraphic);
}
/// <summary>
/// Change the alert severity, changing the displayed icon
/// </summary>
public void SetSeverity(short? severity)
{
if (_severity != severity)
{
_severity = severity;
_icon.Texture = _resourceCache.GetTexture(Alert.GetIconPath(_severity));
}
}
/// <summary>
/// Updates the displayed cooldown amount, doing nothing if alertCooldown is null
/// </summary>
/// <param name="alertCooldown">cooldown start and end</param>
/// <param name="curTime">current game time</param>
public void UpdateCooldown((TimeSpan Start, TimeSpan End)? alertCooldown, in TimeSpan curTime)
{
if (!alertCooldown.HasValue)
{
_cooldownGraphic.Progress = 0;
_cooldownGraphic.Visible = false;
TotalDuration = null;
}
else
{
var start = alertCooldown.Value.Start;
var end = alertCooldown.Value.End;
var length = (end - start).TotalSeconds;
var progress = (curTime - start).TotalSeconds / length;
var ratio = (progress <= 1 ? (1 - progress) : (curTime - end).TotalSeconds * -5);
TotalDuration = (int?) Math.Round(length);
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
_cooldownGraphic.Visible = ratio > -1f;
}
}
}
}

View File

@@ -0,0 +1,345 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Client.UserInterface;
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.Graphics;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.Interfaces.UserInterface;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using Serilog;
namespace Content.Client.GameObjects.Components.Mobs
{
/// <inheritdoc/>
[RegisterComponent]
[ComponentReference(typeof(SharedAlertsComponent))]
public sealed class ClientAlertsComponent : SharedAlertsComponent
{
private static readonly float TooltipTextMaxWidth = 265;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private AlertsUI _ui;
private PanelContainer _tooltip;
private RichTextLabel _stateName;
private RichTextLabel _stateDescription;
private RichTextLabel _stateCooldown;
private AlertOrderPrototype _alertOrder;
private bool _tooltipReady;
[ViewVariables]
private Dictionary<AlertKey, AlertControl> _alertControls
= new Dictionary<AlertKey, AlertControl>();
/// <summary>
/// Allows calculating if we need to act due to this component being controlled by the current mob
/// TODO: should be revisited after space-wizards/RobustToolbox#1255
/// </summary>
[ViewVariables]
private bool CurrentlyControlled => _playerManager.LocalPlayer != null && _playerManager.LocalPlayer.ControlledEntity == Owner;
protected override void Shutdown()
{
base.Shutdown();
PlayerDetached();
}
public override void HandleMessage(ComponentMessage message, IComponent component)
{
base.HandleMessage(message, component);
switch (message)
{
case PlayerAttachedMsg _:
PlayerAttached();
break;
case PlayerDetachedMsg _:
PlayerDetached();
break;
}
}
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{
base.HandleComponentState(curState, nextState);
if (!(curState is AlertsComponentState state))
{
return;
}
// update the dict of states based on the array we got in the message
SetAlerts(state.Alerts);
UpdateAlertsControls();
}
private void PlayerAttached()
{
if (!CurrentlyControlled || _ui != null)
{
return;
}
_alertOrder = IoCManager.Resolve<IPrototypeManager>().EnumeratePrototypes<AlertOrderPrototype>().FirstOrDefault();
if (_alertOrder == null)
{
Logger.ErrorS("alert", "no alertOrder prototype found, alerts will be in random order");
}
_ui = new AlertsUI(IoCManager.Resolve<IClyde>());
var uiManager = IoCManager.Resolve<IUserInterfaceManager>();
uiManager.StateRoot.AddChild(_ui);
_tooltip = new PanelContainer
{
Visible = false,
StyleClasses = { StyleNano.StyleClassTooltipPanel }
};
var tooltipVBox = new VBoxContainer
{
RectClipContent = true
};
_tooltip.AddChild(tooltipVBox);
_stateName = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleNano.StyleClassTooltipAlertTitle }
};
tooltipVBox.AddChild(_stateName);
_stateDescription = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleNano.StyleClassTooltipAlertDescription }
};
tooltipVBox.AddChild(_stateDescription);
_stateCooldown = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleNano.StyleClassTooltipAlertCooldown }
};
tooltipVBox.AddChild(_stateCooldown);
uiManager.PopupRoot.AddChild(_tooltip);
UpdateAlertsControls();
}
private void PlayerDetached()
{
_ui?.Dispose();
_ui = null;
_alertControls.Clear();
}
/// <summary>
/// Updates the displayed alerts based on current state of Alerts, performing
/// a diff to ensure we only change what's changed (this avoids active tooltips disappearing any
/// time state changes)
/// </summary>
private void UpdateAlertsControls()
{
if (!CurrentlyControlled || _ui == null)
{
return;
}
// remove any controls with keys no longer present
var toRemove = new List<AlertKey>();
foreach (var existingKey in _alertControls.Keys)
{
if (!IsShowingAlert(existingKey))
{
toRemove.Add(existingKey);
}
}
foreach (var alertKeyToRemove in toRemove)
{
// remove and dispose the control
_alertControls.Remove(alertKeyToRemove, out var control);
control?.Dispose();
}
// now we know that alertControls contains alerts that should still exist but
// may need to updated,
// also there may be some new alerts we need to show.
// further, we need to ensure they are ordered w.r.t their configured order
foreach (var alertStatus in EnumerateAlertStates())
{
if (!AlertManager.TryDecode(alertStatus.AlertEncoded, out var newAlert))
{
Logger.ErrorS("alert", "Unable to decode alert {0}", alertStatus.AlertEncoded);
continue;
}
if (_alertControls.TryGetValue(newAlert.AlertKey, out var existingAlertControl) &&
existingAlertControl.Alert.AlertType == newAlert.AlertType)
{
// id is the same, simply update the existing control severity
existingAlertControl.SetSeverity(alertStatus.Severity);
}
else
{
existingAlertControl?.Dispose();
// this is a new alert + alert key or just a different alert with the same
// key, create the control and add it in the appropriate order
var newAlertControl = CreateAlertControl(newAlert, alertStatus);
if (_alertOrder != null)
{
var added = false;
foreach (var alertControl in _ui.Grid.Children)
{
if (_alertOrder.Compare(newAlert, ((AlertControl) alertControl).Alert) < 0)
{
var idx = alertControl.GetPositionInParent();
_ui.Grid.Children.Add(newAlertControl);
newAlertControl.SetPositionInParent(idx);
added = true;
break;
}
}
if (!added)
{
_ui.Grid.Children.Add(newAlertControl);
}
}
else
{
_ui.Grid.Children.Add(newAlertControl);
}
_alertControls[newAlert.AlertKey] = newAlertControl;
}
}
}
private AlertControl CreateAlertControl(AlertPrototype alert, AlertState alertState)
{
var alertControl = new AlertControl(alert, alertState.Severity, _resourceCache);
// show custom tooltip for the status control
alertControl.OnShowTooltip += AlertOnOnShowTooltip;
alertControl.OnHideTooltip += AlertOnOnHideTooltip;
alertControl.OnPressed += AlertControlOnPressed;
return alertControl;
}
private void AlertControlOnPressed(BaseButton.ButtonEventArgs args)
{
AlertPressed(args, args.Button as AlertControl);
}
private void AlertOnOnHideTooltip(object sender, EventArgs e)
{
_tooltipReady = false;
_tooltip.Visible = false;
}
private void AlertOnOnShowTooltip(object sender, EventArgs e)
{
var alertControl = (AlertControl) sender;
_stateName.SetMessage(alertControl.Alert.Name);
_stateDescription.SetMessage(alertControl.Alert.Description);
// check for a cooldown
if (alertControl.TotalDuration != null && alertControl.TotalDuration > 0)
{
_stateCooldown.SetMessage(FormattedMessage.FromMarkup("[color=#776a6a]" +
alertControl.TotalDuration +
" sec cooldown[/color]"));
_stateCooldown.Visible = true;
}
else
{
_stateCooldown.Visible = false;
}
// TODO: Text display of cooldown
Tooltips.PositionTooltip(_tooltip);
// if we set it visible here the size of the previous tooltip will flicker for a frame,
// so instead we wait until FrameUpdate to make it visible
_tooltipReady = true;
}
private void AlertPressed(BaseButton.ButtonEventArgs args, AlertControl alert)
{
if (args.Event.Function != EngineKeyFunctions.UIClick)
{
return;
}
if (AlertManager.TryEncode(alert.Alert, out var encoded))
{
SendNetworkMessage(new ClickAlertMessage(encoded));
}
else
{
Logger.ErrorS("alert", "unable to encode alert {0}", alert.Alert.AlertType);
}
}
public void FrameUpdate(float frameTime)
{
if (_tooltipReady)
{
_tooltipReady = false;
_tooltip.Visible = true;
}
foreach (var (alertKey, alertControl) in _alertControls)
{
// reconcile all alert controls with their current cooldowns
if (TryGetAlertState(alertKey, out var alertState))
{
alertControl.UpdateCooldown(alertState.Cooldown, _gameTiming.CurTime);
}
else
{
Logger.WarningS("alert", "coding error - no alert state for alert {0} " +
"even though we had an AlertControl for it, this" +
" should never happen", alertControl.Alert.AlertType);
}
}
}
protected override void AfterClearAlert()
{
UpdateAlertsControls();
}
public override void OnRemove()
{
base.OnRemove();
foreach (var alertControl in _alertControls.Values)
{
alertControl.OnShowTooltip -= AlertOnOnShowTooltip;
alertControl.OnHideTooltip -= AlertOnOnHideTooltip;
alertControl.OnPressed -= AlertControlOnPressed;
}
}
}
}

View File

@@ -1,200 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Client.UserInterface;
using Content.Client.Utility;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Client.GameObjects;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.Interfaces.UserInterface;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components.Mobs
{
/// <inheritdoc/>
[RegisterComponent]
[ComponentReference(typeof(SharedStatusEffectsComponent))]
public sealed class ClientStatusEffectsComponent : SharedStatusEffectsComponent
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private StatusEffectsUI _ui;
[ViewVariables]
private Dictionary<StatusEffect, StatusEffectStatus> _status = new Dictionary<StatusEffect, StatusEffectStatus>();
[ViewVariables]
private Dictionary<StatusEffect, CooldownGraphic> _cooldown = new Dictionary<StatusEffect, CooldownGraphic>();
public override IReadOnlyDictionary<StatusEffect, StatusEffectStatus> Statuses => _status;
/// <summary>
/// Allows calculating if we need to act due to this component being controlled by the current mob
/// </summary>
[ViewVariables]
private bool CurrentlyControlled => _playerManager.LocalPlayer != null && _playerManager.LocalPlayer.ControlledEntity == Owner;
protected override void Shutdown()
{
base.Shutdown();
PlayerDetached();
}
public override void HandleMessage(ComponentMessage message, IComponent component)
{
base.HandleMessage(message, component);
switch (message)
{
case PlayerAttachedMsg _:
PlayerAttached();
break;
case PlayerDetachedMsg _:
PlayerDetached();
break;
}
}
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{
base.HandleComponentState(curState, nextState);
if (!(curState is StatusEffectComponentState state) || _status == state.StatusEffects)
{
return;
}
_status = state.StatusEffects;
UpdateStatusEffects();
}
private void PlayerAttached()
{
if (!CurrentlyControlled || _ui != null)
{
return;
}
_ui = new StatusEffectsUI();
_userInterfaceManager.StateRoot.AddChild(_ui);
UpdateStatusEffects();
}
private void PlayerDetached()
{
_ui?.Dispose();
_ui = null;
_cooldown.Clear();
}
public override void ChangeStatusEffectIcon(StatusEffect effect, string icon)
{
if (_status.TryGetValue(effect, out var value) &&
value.Icon == icon)
{
return;
}
_status[effect] = new StatusEffectStatus
{
Icon = icon,
Cooldown = value.Cooldown
};
Dirty();
}
public void UpdateStatusEffects()
{
if (!CurrentlyControlled || _ui == null)
{
return;
}
_cooldown.Clear();
_ui.VBox.DisposeAllChildren();
foreach (var (key, effect) in _status.OrderBy(x => (int) x.Key))
{
var texture = _resourceCache.GetTexture(effect.Icon);
var status = new StatusControl(key, texture)
{
ToolTip = key.ToString()
};
if (effect.Cooldown.HasValue)
{
var cooldown = new CooldownGraphic();
status.Children.Add(cooldown);
_cooldown[key] = cooldown;
}
status.OnPressed += args => StatusPressed(args, status);
_ui.VBox.AddChild(status);
}
}
private void StatusPressed(BaseButton.ButtonEventArgs args, StatusControl status)
{
if (args.Event.Function != EngineKeyFunctions.UIClick)
{
return;
}
SendNetworkMessage(new ClickStatusMessage(status.Effect));
}
public override void RemoveStatusEffect(StatusEffect effect)
{
if (!_status.Remove(effect))
{
return;
}
UpdateStatusEffects();
Dirty();
}
public void FrameUpdate(float frameTime)
{
foreach (var (effect, cooldownGraphic) in _cooldown)
{
var status = _status[effect];
if (!status.Cooldown.HasValue)
{
cooldownGraphic.Progress = 0;
cooldownGraphic.Visible = false;
continue;
}
var start = status.Cooldown.Value.Item1;
var end = status.Cooldown.Value.Item2;
var length = (end - start).TotalSeconds;
var progress = (_gameTiming.CurTime - start).TotalSeconds / length;
var ratio = (progress <= 1 ? (1 - progress) : (_gameTiming.CurTime - end).TotalSeconds * -5);
cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
cooldownGraphic.Visible = ratio > -1f;
}
}
public override void ChangeStatusEffect(StatusEffect effect, string icon, (TimeSpan, TimeSpan)? cooldown)
{
_status[effect] = new StatusEffectStatus()
{
Icon = icon,
Cooldown = cooldown
};
Dirty();
}
}
}

View File

@@ -1,25 +0,0 @@
#nullable enable
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.GameObjects.Components.Mobs
{
public class StatusControl : BaseButton
{
public readonly StatusEffect Effect;
public StatusControl(StatusEffect effect, Texture? texture)
{
Effect = effect;
var item = new TextureRect
{
TextureScale = (2, 2),
Texture = texture
};
Children.Add(item);
}
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Content.Client.UserInterface.Stylesheets;
using Content.Shared.AI; using Content.Shared.AI;
using Robust.Client.Interfaces.Graphics.ClientEye; using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Client.Interfaces.UserInterface; using Robust.Client.Interfaces.UserInterface;
@@ -178,7 +179,7 @@ namespace Content.Client.GameObjects.EntitySystems.AI
var panel = new PanelContainer var panel = new PanelContainer
{ {
StyleClasses = {"tooltipBox"}, StyleClasses = { StyleNano.StyleClassTooltipPanel },
Children = {vBox}, Children = {vBox},
MouseFilter = Control.MouseFilterMode.Ignore, MouseFilter = Control.MouseFilterMode.Ignore,
ModulateSelfOverride = Color.White.WithAlpha(0.75f), ModulateSelfOverride = Color.White.WithAlpha(0.75f),

View File

@@ -5,7 +5,7 @@ using Robust.Shared.IoC;
namespace Content.Client.GameObjects.EntitySystems namespace Content.Client.GameObjects.EntitySystems
{ {
public class StatusEffectsSystem : EntitySystem public class AlertsSystem : EntitySystem
{ {
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -16,9 +16,9 @@ namespace Content.Client.GameObjects.EntitySystems
if (!_gameTiming.IsFirstTimePredicted) if (!_gameTiming.IsFirstTimePredicted)
return; return;
foreach (var clientStatusEffectsComponent in EntityManager.ComponentManager.EntityQuery<ClientStatusEffectsComponent>()) foreach (var clientAlertsComponent in EntityManager.ComponentManager.EntityQuery<ClientAlertsComponent>())
{ {
clientStatusEffectsComponent.FrameUpdate(frameTime); clientAlertsComponent.FrameUpdate(frameTime);
} }
} }
} }

View File

@@ -0,0 +1,73 @@
using System;
using Content.Client.UserInterface.Stylesheets;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Interfaces.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Maths;
namespace Content.Client.UserInterface
{
/// <summary>
/// The status effects display on the right side of the screen.
/// </summary>
public sealed class AlertsUI : Control
{
public GridContainer Grid { get; }
private readonly IClyde _clyde;
public AlertsUI(IClyde clyde)
{
_clyde = clyde;
var panelContainer = new PanelContainer
{
StyleClasses = {StyleNano.StyleClassTransparentBorderedWindowPanel},
SizeFlagsVertical = SizeFlags.FillExpand,
};
AddChild(panelContainer);
Grid = new GridContainer
{
MaxHeight = CalcMaxHeight(clyde.ScreenSize),
ExpandBackwards = true
};
panelContainer.AddChild(Grid);
clyde.OnWindowResized += ClydeOnOnWindowResized;
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin);
LayoutContainer.SetAnchorAndMarginPreset(this, LayoutContainer.LayoutPreset.TopRight, margin: 10);
LayoutContainer.SetMarginTop(this, 250);
}
protected override void UIScaleChanged()
{
Grid.MaxHeight = CalcMaxHeight(_clyde.ScreenSize);
base.UIScaleChanged();
}
private void ClydeOnOnWindowResized(WindowResizedEventArgs obj)
{
// TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
// this is here because there isn't currently a good way to allow the grid to adjust its height based
// on constraints, otherwise we would use anchors to lay it out
Grid.MaxHeight = CalcMaxHeight(obj.NewSize);;
}
private float CalcMaxHeight(Vector2i screenSize)
{
return Math.Max(((screenSize.Y) / UIScale) - 420, 1);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_clyde.OnWindowResized -= ClydeOnOnWindowResized;
}
}
}
}

View File

@@ -1,23 +0,0 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface
{
/// <summary>
/// The status effects display on the right side of the screen.
/// </summary>
public sealed class StatusEffectsUI : Control
{
public VBoxContainer VBox { get; }
public StatusEffectsUI()
{
VBox = new VBoxContainer();
AddChild(VBox);
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin);
LayoutContainer.SetAnchorAndMarginPreset(this, LayoutContainer.LayoutPreset.TopRight, margin: 10);
LayoutContainer.SetMarginTop(this, 250);
}
}
}

View File

@@ -13,6 +13,13 @@ namespace Content.Client.UserInterface.Stylesheets
{ {
public sealed class StyleNano : StyleBase public sealed class StyleNano : StyleBase
{ {
public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel";
public const string StyleClassTransparentBorderedWindowPanel = "TransparentBorderedWindowPanel";
public const string StyleClassTooltipPanel = "tooltipBox";
public const string StyleClassTooltipAlertTitle = "tooltipAlertTitle";
public const string StyleClassTooltipAlertDescription = "tooltipAlertDesc";
public const string StyleClassTooltipAlertCooldown = "tooltipAlertCooldown";
public const string StyleClassSliderRed = "Red"; public const string StyleClassSliderRed = "Red";
public const string StyleClassSliderGreen = "Green"; public const string StyleClassSliderGreen = "Green";
public const string StyleClassSliderBlue = "Blue"; public const string StyleClassSliderBlue = "Blue";
@@ -55,6 +62,7 @@ namespace Content.Client.UserInterface.Stylesheets
var notoSansDisplayBold14 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 14); var notoSansDisplayBold14 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 14);
var notoSans16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 16); var notoSans16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 16);
var notoSansBold16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 16); var notoSansBold16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 16);
var notoSansBold18 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 18);
var notoSansBold20 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 20); var notoSansBold20 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 20);
var textureCloseButton = resCache.GetTexture("/Textures/Interface/Nano/cross.svg.png"); var textureCloseButton = resCache.GetTexture("/Textures/Interface/Nano/cross.svg.png");
var windowHeaderTex = resCache.GetTexture("/Textures/Interface/Nano/window_header.png"); var windowHeaderTex = resCache.GetTexture("/Textures/Interface/Nano/window_header.png");
@@ -73,6 +81,20 @@ namespace Content.Client.UserInterface.Stylesheets
windowBackground.SetPatchMargin(StyleBox.Margin.Horizontal | StyleBox.Margin.Bottom, 2); windowBackground.SetPatchMargin(StyleBox.Margin.Horizontal | StyleBox.Margin.Bottom, 2);
windowBackground.SetExpandMargin(StyleBox.Margin.Horizontal | StyleBox.Margin.Bottom, 2); windowBackground.SetExpandMargin(StyleBox.Margin.Horizontal | StyleBox.Margin.Bottom, 2);
var borderedWindowBackgroundTex = resCache.GetTexture("/Textures/Interface/Nano/window_background_bordered.png");
var borderedWindowBackground = new StyleBoxTexture
{
Texture = borderedWindowBackgroundTex,
};
borderedWindowBackground.SetPatchMargin(StyleBox.Margin.All, 2);
var borderedTransparentWindowBackgroundTex = resCache.GetTexture("/Textures/Interface/Nano/transparent_window_background_bordered.png");
var borderedTransparentWindowBackground = new StyleBoxTexture
{
Texture = borderedTransparentWindowBackgroundTex,
};
borderedTransparentWindowBackground.SetPatchMargin(StyleBox.Margin.All, 2);
var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png"); var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png");
var lineEditTex = resCache.GetTexture("/Textures/Interface/Nano/lineedit.png"); var lineEditTex = resCache.GetTexture("/Textures/Interface/Nano/lineedit.png");
@@ -147,7 +169,7 @@ namespace Content.Client.UserInterface.Stylesheets
Texture = tooltipTexture, Texture = tooltipTexture,
}; };
tooltipBox.SetPatchMargin(StyleBox.Margin.All, 2); tooltipBox.SetPatchMargin(StyleBox.Margin.All, 2);
tooltipBox.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5); tooltipBox.SetContentMarginOverride(StyleBox.Margin.Horizontal, 7);
// Placeholder // Placeholder
var placeholderTexture = resCache.GetTexture("/Textures/Interface/Nano/placeholder.png"); var placeholderTexture = resCache.GetTexture("/Textures/Interface/Nano/placeholder.png");
@@ -245,6 +267,19 @@ namespace Content.Client.UserInterface.Stylesheets
{ {
new StyleProperty(PanelContainer.StylePropertyPanel, windowBackground), new StyleProperty(PanelContainer.StylePropertyPanel, windowBackground),
}), }),
// bordered window background
new StyleRule(
new SelectorElement(null, new[] {StyleClassBorderedWindowPanel}, null, null),
new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, borderedWindowBackground),
}),
new StyleRule(
new SelectorElement(null, new[] {StyleClassTransparentBorderedWindowPanel}, null, null),
new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, borderedTransparentWindowBackground),
}),
// Window header. // Window header.
new StyleRule( new StyleRule(
new SelectorElement(typeof(PanelContainer), new[] {SS14Window.StyleClassWindowHeader}, null, null), new SelectorElement(typeof(PanelContainer), new[] {SS14Window.StyleClassWindowHeader}, null, null),
@@ -464,7 +499,7 @@ namespace Content.Client.UserInterface.Stylesheets
new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox) new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox)
}), }),
new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {"tooltipBox"}, null, null), new[] new StyleRule(new SelectorElement(typeof(PanelContainer), new [] { StyleClassTooltipPanel }, null, null), new[]
{ {
new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox) new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox)
}), }),
@@ -482,6 +517,20 @@ namespace Content.Client.UserInterface.Stylesheets
new StyleProperty("font", notoSansItalic12), new StyleProperty("font", notoSansItalic12),
}), }),
// alert tooltip
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipAlertTitle}, null, null), new[]
{
new StyleProperty("font", notoSansBold18)
}),
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipAlertDescription}, null, null), new[]
{
new StyleProperty("font", notoSans16)
}),
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipAlertCooldown}, null, null), new[]
{
new StyleProperty("font", notoSans16)
}),
// Entity tooltip // Entity tooltip
new StyleRule( new StyleRule(
new SelectorElement(typeof(PanelContainer), new[] {ExamineSystem.StyleClassEntityTooltip}, null, new SelectorElement(typeof(PanelContainer), new[] {ExamineSystem.StyleClassEntityTooltip}, null,

View File

@@ -0,0 +1,110 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Client.GameObjects.Components.Mobs;
using Content.Client.UserInterface;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert;
using NUnit.Framework;
using Robust.Client.Interfaces.UserInterface;
using Robust.Client.Player;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
{
[TestFixture]
[TestOf(typeof(ClientAlertsComponent))]
[TestOf(typeof(ServerAlertsComponent))]
public class AlertsComponentTests : ContentIntegrationTest
{
[Test]
public async Task AlertsTest()
{
var (client, server) = await StartConnectedServerClientPair();
await server.WaitIdleAsync();
await client.WaitIdleAsync();
var serverPlayerManager = server.ResolveDependency<Robust.Server.Interfaces.Player.IPlayerManager>();
await server.WaitAssertion(() =>
{
var player = serverPlayerManager.GetAllPlayers().Single();
var playerEnt = player.AttachedEntity;
Assert.NotNull(playerEnt);
var alertsComponent = playerEnt.GetComponent<ServerAlertsComponent>();
Assert.NotNull(alertsComponent);
// show 2 alerts
alertsComponent.ShowAlert(AlertType.Debug1);
alertsComponent.ShowAlert(AlertType.Debug2);
});
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
var clientPlayerMgr = client.ResolveDependency<IPlayerManager>();
var clientUIMgr = client.ResolveDependency<IUserInterfaceManager>();
await client.WaitAssertion(() =>
{
var local = clientPlayerMgr.LocalPlayer;
Assert.NotNull(local);
var controlled = local.ControlledEntity;
Assert.NotNull(controlled);
var alertsComponent = controlled.GetComponent<ClientAlertsComponent>();
Assert.NotNull(alertsComponent);
// find the alertsui
var alertsUI =
clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is AlertsUI) as AlertsUI;
Assert.NotNull(alertsUI);
// we should be seeing 3 alerts - our health, and the 2 debug alerts, in a specific order.
Assert.That(alertsUI.Grid.ChildCount, Is.EqualTo(3));
var alertControls = alertsUI.Grid.Children.Select(c => c as AlertControl);
var alertIDs = alertControls.Select(ac => ac.Alert.AlertType).ToArray();
var expectedIDs = new [] {AlertType.HumanHealth, AlertType.Debug1, AlertType.Debug2};
Assert.That(alertIDs, Is.EqualTo(expectedIDs));
});
await server.WaitAssertion(() =>
{
var player = serverPlayerManager.GetAllPlayers().Single();
var playerEnt = player.AttachedEntity;
Assert.NotNull(playerEnt);
var alertsComponent = playerEnt.GetComponent<ServerAlertsComponent>();
Assert.NotNull(alertsComponent);
alertsComponent.ClearAlert(AlertType.Debug1);
});
await server.WaitRunTicks(5);
await client.WaitRunTicks(5);
await client.WaitAssertion(() =>
{
var local = clientPlayerMgr.LocalPlayer;
Assert.NotNull(local);
var controlled = local.ControlledEntity;
Assert.NotNull(controlled);
var alertsComponent = controlled.GetComponent<ClientAlertsComponent>();
Assert.NotNull(alertsComponent);
// find the alertsui
var alertsUI =
clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is AlertsUI) as AlertsUI;
Assert.NotNull(alertsUI);
// we should be seeing only 2 alerts now because one was cleared
Assert.That(alertsUI.Grid.ChildCount, Is.EqualTo(2));
var alertControls = alertsUI.Grid.Children.Select(c => c as AlertControl);
var alertIDs = alertControls.Select(ac => ac.Alert.AlertType).ToArray();
var expectedIDs = new [] {AlertType.HumanHealth, AlertType.Debug2};
Assert.That(alertIDs, Is.EqualTo(expectedIDs));
});
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Gravity; using Content.Server.GameObjects.Components.Gravity;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Gravity; using Content.Shared.GameObjects.Components.Gravity;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
@@ -32,7 +33,7 @@ namespace Content.IntegrationTests.Tests.Gravity
var tileDefinitionManager = server.ResolveDependency<ITileDefinitionManager>(); var tileDefinitionManager = server.ResolveDependency<ITileDefinitionManager>();
IEntity human = null; IEntity human = null;
SharedStatusEffectsComponent statusEffects = null; SharedAlertsComponent alerts = null;
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
@@ -57,7 +58,7 @@ namespace Content.IntegrationTests.Tests.Gravity
human = entityManager.SpawnEntity("HumanMob_Content", coordinates); human = entityManager.SpawnEntity("HumanMob_Content", coordinates);
Assert.True(human.TryGetComponent(out statusEffects)); Assert.True(human.TryGetComponent(out alerts));
}); });
// Let WeightlessSystem and GravitySystem tick // Let WeightlessSystem and GravitySystem tick
@@ -68,7 +69,7 @@ namespace Content.IntegrationTests.Tests.Gravity
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
// No gravity without a gravity generator // No gravity without a gravity generator
Assert.True(statusEffects.Statuses.ContainsKey(StatusEffect.Weightless)); Assert.True(alerts.IsShowingAlert(AlertType.Weightless));
gravityGenerator = human.EnsureComponent<GravityGeneratorComponent>(); gravityGenerator = human.EnsureComponent<GravityGeneratorComponent>();
}); });
@@ -78,7 +79,7 @@ namespace Content.IntegrationTests.Tests.Gravity
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
Assert.False(statusEffects.Statuses.ContainsKey(StatusEffect.Weightless)); Assert.False(alerts.IsShowingAlert(AlertType.Weightless));
// Disable the gravity generator // Disable the gravity generator
var args = new BreakageEventArgs {Owner = human}; var args = new BreakageEventArgs {Owner = human};
@@ -89,7 +90,7 @@ namespace Content.IntegrationTests.Tests.Gravity
await server.WaitAssertion(() => await server.WaitAssertion(() =>
{ {
Assert.False(statusEffects.Statuses.ContainsKey(StatusEffect.Weightless)); Assert.False(alerts.IsShowingAlert(AlertType.Weightless));
}); });
} }
} }

View File

@@ -0,0 +1,66 @@
using System;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
namespace Content.Server.Commands
{
/// <summary>
/// Utilities for writing commands
/// </summary>
public static class CommandUtils
{
/// <summary>
/// Gets the player session for the player with the indicated id,
/// sending a failure to the performer if unable to.
/// </summary>
public static bool TryGetSessionByUsernameOrId(IConsoleShell shell,
string usernameOrId, IPlayerSession performer, out IPlayerSession session)
{
var plyMgr = IoCManager.Resolve<IPlayerManager>();
if (plyMgr.TryGetSessionByUsername(usernameOrId, out session)) return true;
if (Guid.TryParse(usernameOrId, out var targetGuid))
{
if (plyMgr.TryGetSessionById(new NetUserId(targetGuid), out session)) return true;
shell.SendText(performer, "Unable to find user with that name/id.");
return false;
}
shell.SendText(performer, "Unable to find user with that name/id.");
return false;
}
/// <summary>
/// Gets the attached entity for the player session with the indicated id,
/// sending a failure to the performer if unable to.
/// </summary>
public static bool TryGetAttachedEntityByUsernameOrId(IConsoleShell shell,
string usernameOrId, IPlayerSession performer, out IEntity attachedEntity)
{
attachedEntity = null;
if (!TryGetSessionByUsernameOrId(shell, usernameOrId, performer, out var session)) return false;
if (session.AttachedEntity == null)
{
shell.SendText(performer, "User has no attached entity.");
return false;
}
attachedEntity = session.AttachedEntity;
return true;
}
/// <summary>
/// Checks if attached entity is null, returning false and sending a message
/// to performer if not.
/// </summary>
public static bool ValidateAttachedEntity(IConsoleShell shell, IPlayerSession performer, IEntity attachedEntity)
{
if (attachedEntity != null) return true;
shell.SendText(performer, "User has no attached entity.");
return false;
}
}
}

View File

@@ -9,6 +9,7 @@ using Content.Server.Interfaces.GameTicking;
using Content.Server.Interfaces.PDA; using Content.Server.Interfaces.PDA;
using Content.Server.Sandbox; using Content.Server.Sandbox;
using Content.Shared.Kitchen; using Content.Shared.Kitchen;
using Content.Shared.Alert;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.ContentPack; using Robust.Shared.ContentPack;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
@@ -74,6 +75,7 @@ namespace Content.Server
_gameTicker.Initialize(); _gameTicker.Initialize();
IoCManager.Resolve<RecipeManager>().Initialize(); IoCManager.Resolve<RecipeManager>().Initialize();
IoCManager.Resolve<AlertManager>().Initialize();
IoCManager.Resolve<BlackboardManager>().Initialize(); IoCManager.Resolve<BlackboardManager>().Initialize();
IoCManager.Resolve<ConsiderationsManager>().Initialize(); IoCManager.Resolve<ConsiderationsManager>().Initialize();
IoCManager.Resolve<IPDAUplinkManager>().Initialize(); IoCManager.Resolve<IPDAUplinkManager>().Initialize();

View File

@@ -6,6 +6,7 @@ using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.EntitySystems.DoAfter; using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Server.Interfaces.GameObjects.Components.Items; using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.ActionBlocking; using Content.Shared.GameObjects.Components.ActionBlocking;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
@@ -115,7 +116,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
CanStillInteract = _hands.Hands.Count() > CuffedHandCount; CanStillInteract = _hands.Hands.Count() > CuffedHandCount;
OnCuffedStateChanged.Invoke(); OnCuffedStateChanged.Invoke();
UpdateStatusEffect(); UpdateAlert();
UpdateHeldItems(); UpdateHeldItems();
Dirty(); Dirty();
} }
@@ -181,17 +182,17 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
/// <summary> /// <summary>
/// Updates the status effect indicator on the HUD. /// Updates the status effect indicator on the HUD.
/// </summary> /// </summary>
private void UpdateStatusEffect() private void UpdateAlert()
{ {
if (Owner.TryGetComponent(out ServerStatusEffectsComponent status)) if (Owner.TryGetComponent(out ServerAlertsComponent status))
{ {
if (CanStillInteract) if (CanStillInteract)
{ {
status.RemoveStatusEffect(StatusEffect.Cuffed); status.ClearAlert(AlertType.Handcuffed);
} }
else else
{ {
status.ChangeStatusEffectIcon(StatusEffect.Cuffed, "/Textures/Interface/StatusEffects/Handcuffed/Handcuffed.png"); status.ShowAlert(AlertType.Handcuffed);
} }
} }
} }
@@ -282,7 +283,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
CanStillInteract = _hands.Hands.Count() > CuffedHandCount; CanStillInteract = _hands.Hands.Count() > CuffedHandCount;
OnCuffedStateChanged.Invoke(); OnCuffedStateChanged.Invoke();
UpdateStatusEffect(); UpdateAlert();
Dirty(); Dirty();
if (CuffedHandCount == 0) if (CuffedHandCount == 0)

View File

@@ -2,6 +2,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Server.Interfaces.GameObjects; using Content.Server.Interfaces.GameObjects;
using Content.Shared.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
@@ -22,7 +23,7 @@ namespace Content.Server.GameObjects.Components.Atmos
public void Update(float airPressure) public void Update(float airPressure)
{ {
if (!Owner.TryGetComponent(out IDamageableComponent damageable)) return; if (!Owner.TryGetComponent(out IDamageableComponent damageable)) return;
Owner.TryGetComponent(out ServerStatusEffectsComponent status); Owner.TryGetComponent(out ServerAlertsComponent status);
var highPressureMultiplier = 1f; var highPressureMultiplier = 1f;
var lowPressureMultiplier = 1f; var lowPressureMultiplier = 1f;
@@ -50,11 +51,11 @@ namespace Content.Server.GameObjects.Components.Atmos
if (pressure <= Atmospherics.HazardLowPressure) if (pressure <= Atmospherics.HazardLowPressure)
{ {
status.ChangeStatusEffect(StatusEffect.Pressure, "/Textures/Interface/StatusEffects/Pressure/lowpressure2.png", null); status.ShowAlert(AlertType.LowPressure, 2);
break; break;
} }
status.ChangeStatusEffect(StatusEffect.Pressure, "/Textures/Interface/StatusEffects/Pressure/lowpressure1.png", null); status.ShowAlert(AlertType.LowPressure, 1);
break; break;
// High pressure. // High pressure.
@@ -72,16 +73,16 @@ namespace Content.Server.GameObjects.Components.Atmos
if (pressure >= Atmospherics.HazardHighPressure) if (pressure >= Atmospherics.HazardHighPressure)
{ {
status.ChangeStatusEffect(StatusEffect.Pressure, "/Textures/Interface/StatusEffects/Pressure/highpressure2.png", null); status.ShowAlert(AlertType.HighPressure, 2);
break; break;
} }
status.ChangeStatusEffect(StatusEffect.Pressure, "/Textures/Interface/StatusEffects/Pressure/highpressure1.png", null); status.ShowAlert(AlertType.HighPressure, 1);
break; break;
// Normal pressure. // Normal pressure.
default: default:
status?.RemoveStatusEffect(StatusEffect.Pressure); status?.ClearAlertCategory(AlertCategory.Pressure);
break; break;
} }

View File

@@ -4,6 +4,7 @@ using Content.Server.Atmos;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Temperature; using Content.Server.GameObjects.Components.Temperature;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.Damage; using Content.Shared.Damage;
@@ -93,15 +94,15 @@ namespace Content.Server.GameObjects.Components.Atmos
FireStacks = MathF.Min(0, FireStacks + 1); FireStacks = MathF.Min(0, FireStacks + 1);
} }
Owner.TryGetComponent(out ServerStatusEffectsComponent status); Owner.TryGetComponent(out ServerAlertsComponent status);
if (!OnFire) if (!OnFire)
{ {
status?.RemoveStatusEffect(StatusEffect.Fire); status?.ClearAlert(AlertType.Fire);
return; return;
} }
status?.ChangeStatusEffect(StatusEffect.Fire, "/Textures/Interface/StatusEffects/Fire/fire.png", null); status.ShowAlert(AlertType.Fire, onClickAlert: OnClickAlert);
if (FireStacks > 0) if (FireStacks > 0)
{ {
@@ -153,6 +154,14 @@ namespace Content.Server.GameObjects.Components.Atmos
} }
} }
private void OnClickAlert(ClickAlertEventArgs args)
{
if (args.Player.TryGetComponent(out FlammableComponent flammable))
{
flammable.Resist();
}
}
public void CollideWith(IEntity collidedWith) public void CollideWith(IEntity collidedWith)
{ {
if (!collidedWith.TryGetComponent(out FlammableComponent otherFlammable)) if (!collidedWith.TryGetComponent(out FlammableComponent otherFlammable))

View File

@@ -7,6 +7,7 @@ using Content.Server.GameObjects.Components.Mobs.State;
using Content.Server.GameObjects.Components.Pulling; using Content.Server.GameObjects.Components.Pulling;
using Content.Server.GameObjects.Components.Strap; using Content.Server.GameObjects.Components.Strap;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Buckle; using Content.Shared.GameObjects.Components.Buckle;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Strap; using Content.Shared.GameObjects.Components.Strap;
@@ -37,7 +38,7 @@ namespace Content.Server.GameObjects.Components.Buckle
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[ComponentDependency] public readonly AppearanceComponent? AppearanceComponent = null; [ComponentDependency] public readonly AppearanceComponent? AppearanceComponent = null;
[ComponentDependency] private readonly ServerStatusEffectsComponent? _serverStatusEffectsComponent = null; [ComponentDependency] private readonly ServerAlertsComponent? _serverAlertsComponent = null;
[ComponentDependency] private readonly StunnableComponent? _stunnableComponent = null; [ComponentDependency] private readonly StunnableComponent? _stunnableComponent = null;
[ComponentDependency] private readonly MobStateManagerComponent? _mobStateManagerComponent = null; [ComponentDependency] private readonly MobStateManagerComponent? _mobStateManagerComponent = null;
@@ -100,21 +101,31 @@ namespace Content.Server.GameObjects.Components.Buckle
/// </summary> /// </summary>
private void UpdateBuckleStatus() private void UpdateBuckleStatus()
{ {
if (_serverStatusEffectsComponent == null) if (_serverAlertsComponent == null)
{ {
return; return;
} }
if (Buckled) if (Buckled)
{ {
_serverStatusEffectsComponent.ChangeStatusEffectIcon(StatusEffect.Buckled, BuckledTo!.BuckledIcon); _serverAlertsComponent.ShowAlert(BuckledTo != null ? BuckledTo.BuckledAlertType : AlertType.Buckled,
onClickAlert: OnClickAlert);
} }
else else
{ {
_serverStatusEffectsComponent.RemoveStatusEffect(StatusEffect.Buckled); _serverAlertsComponent.ClearAlertCategory(AlertCategory.Buckled);
} }
} }
private void OnClickAlert(ClickAlertEventArgs args)
{
if (args.Player.TryGetComponent(out BuckleComponent? buckle))
{
buckle.TryUnbuckle(args.Player);
}
}
/// <summary> /// <summary>
/// Reattaches this entity to the strap, modifying its position and rotation. /// Reattaches this entity to the strap, modifying its position and rotation.
/// </summary> /// </summary>

View File

@@ -0,0 +1,166 @@
using System;
using Content.Server.Commands;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Players;
namespace Content.Server.GameObjects.Components.Mobs
{
[RegisterComponent]
[ComponentReference(typeof(SharedAlertsComponent))]
public sealed class ServerAlertsComponent : SharedAlertsComponent
{
protected override void Startup()
{
base.Startup();
if (EntitySystem.TryGet<WeightlessSystem>(out var weightlessSystem))
{
weightlessSystem.AddAlert(this);
}
else
{
Logger.WarningS("alert", "weightlesssystem not found");
}
}
public override void OnRemove()
{
if (EntitySystem.TryGet<WeightlessSystem>(out var weightlessSystem))
{
weightlessSystem.RemoveAlert(this);
}
else
{
Logger.WarningS("alert", "weightlesssystem not found");
}
base.OnRemove();
}
public override ComponentState GetComponentState()
{
return new AlertsComponentState(CreateAlertStatesArray());
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession session = null)
{
base.HandleNetworkMessage(message, netChannel, session);
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
switch (message)
{
case ClickAlertMessage msg:
{
var player = session.AttachedEntity;
if (player != Owner)
{
break;
}
// TODO: Implement clicking other status effects in the HUD
if (AlertManager.TryDecode(msg.EncodedAlert, out var alert))
{
PerformAlertClickCallback(alert, player);
}
else
{
Logger.WarningS("alert", "unrecognized encoded alert {0}", msg.EncodedAlert);
}
break;
}
}
}
}
public sealed class ShowAlert : IClientCommand
{
public string Command => "showalert";
public string Description => "Shows an alert for a player, defaulting to current player";
public string Help => "showalert <alertType> <severity, -1 if no severity> <name or userID, omit for current player>";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
var attachedEntity = player.AttachedEntity;
if (args.Length > 2)
{
var target = args[2];
if (!Commands.CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
}
if (!CommandUtils.ValidateAttachedEntity(shell, player, attachedEntity)) return;
if (!attachedEntity.TryGetComponent(out ServerAlertsComponent alertsComponent))
{
shell.SendText(player, "user has no alerts component");
return;
}
var alertType = args[0];
var severity = args[1];
var alertMgr = IoCManager.Resolve<AlertManager>();
if (!alertMgr.TryGet(Enum.Parse<AlertType>(alertType), out var alert))
{
shell.SendText(player, "unrecognized alertType " + alertType);
return;
}
if (!short.TryParse(severity, out var sevint))
{
shell.SendText(player, "invalid severity " + sevint);
return;
}
alertsComponent.ShowAlert(alert.AlertType, sevint == -1 ? (short?) null : sevint);
}
}
public sealed class ClearAlert : IClientCommand
{
public string Command => "clearalert";
public string Description => "Clears an alert for a player, defaulting to current player";
public string Help => "clearalert <alertType> <name or userID, omit for current player>";
public void Execute(IConsoleShell shell, IPlayerSession player, string[] args)
{
var attachedEntity = player.AttachedEntity;
if (args.Length > 1)
{
var target = args[1];
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
}
if (!CommandUtils.ValidateAttachedEntity(shell, player, attachedEntity)) return;
if (!attachedEntity.TryGetComponent(out ServerAlertsComponent alertsComponent))
{
shell.SendText(player, "user has no alerts component");
return;
}
var alertType = args[0];
var alertMgr = IoCManager.Resolve<AlertManager>();
if (!alertMgr.TryGet(Enum.Parse<AlertType>(alertType), out var alert))
{
shell.SendText(player, "unrecognized alertType " + alertType);
return;
}
alertsComponent.ClearAlert(alert.AlertType);
}
}
}

View File

@@ -1,152 +0,0 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.Components.Buckle;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Pulling;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Mobs
{
[RegisterComponent]
[ComponentReference(typeof(SharedStatusEffectsComponent))]
public sealed class ServerStatusEffectsComponent : SharedStatusEffectsComponent
{
[ViewVariables]
private readonly Dictionary<StatusEffect, StatusEffectStatus> _statusEffects = new Dictionary<StatusEffect, StatusEffectStatus>();
public override IReadOnlyDictionary<StatusEffect, StatusEffectStatus> Statuses => _statusEffects;
protected override void Startup()
{
base.Startup();
EntitySystem.Get<WeightlessSystem>().AddStatus(this);
}
public override void OnRemove()
{
EntitySystem.Get<WeightlessSystem>().RemoveStatus(this);
base.OnRemove();
}
public override ComponentState GetComponentState()
{
return new StatusEffectComponentState(_statusEffects);
}
public override void ChangeStatusEffectIcon(StatusEffect effect, string icon)
{
if (_statusEffects.TryGetValue(effect, out var value) && value.Icon == icon)
{
return;
}
_statusEffects[effect] = new StatusEffectStatus()
{Icon = icon, Cooldown = value.Cooldown};
Dirty();
}
public void ChangeStatusEffectCooldown(StatusEffect effect, ValueTuple<TimeSpan, TimeSpan> cooldown)
{
if (_statusEffects.TryGetValue(effect, out var value)
&& value.Cooldown == cooldown)
{
return;
}
_statusEffects[effect] = new StatusEffectStatus()
{
Icon = value.Icon, Cooldown = cooldown
};
Dirty();
}
public override void ChangeStatusEffect(StatusEffect effect, string icon, ValueTuple<TimeSpan, TimeSpan>? cooldown)
{
_statusEffects[effect] = new StatusEffectStatus()
{Icon = icon, Cooldown = cooldown};
Dirty();
}
public override void RemoveStatusEffect(StatusEffect effect)
{
if (!_statusEffects.Remove(effect))
{
return;
}
Dirty();
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession session = null)
{
base.HandleNetworkMessage(message, netChannel, session);
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
switch (message)
{
case ClickStatusMessage msg:
{
var player = session.AttachedEntity;
if (player != Owner)
{
break;
}
// TODO: Implement clicking other status effects in the HUD
switch (msg.Effect)
{
case StatusEffect.Buckled:
if (!player.TryGetComponent(out BuckleComponent buckle))
break;
buckle.TryUnbuckle(player);
break;
case StatusEffect.Piloting:
if (!player.TryGetComponent(out ShuttleControllerComponent controller))
break;
controller.RemoveController();
break;
case StatusEffect.Pulling:
EntitySystem
.Get<SharedPullingSystem>()
.GetPulled(player)?
.GetComponentOrNull<SharedPullableComponent>()?
.TryStopPull();
break;
case StatusEffect.Fire:
if (!player.TryGetComponent(out FlammableComponent flammable))
break;
flammable.Resist();
break;
default:
player.PopupMessage(msg.Effect.ToString());
break;
}
break;
}
}
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State; using Content.Shared.GameObjects.Components.Mobs.State;
@@ -17,10 +18,9 @@ namespace Content.Server.GameObjects.Components.Mobs.State
appearance.SetData(DamageStateVisuals.State, DamageState.Critical); appearance.SetData(DamageStateVisuals.State, DamageState.Critical);
} }
if (entity.TryGetComponent(out ServerStatusEffectsComponent status)) if (entity.TryGetComponent(out ServerAlertsComponent status))
{ {
status.ChangeStatusEffectIcon(StatusEffect.Health, status.ShowAlert(AlertType.HumanCrit); //Todo: combine humancrit-0 and humancrit-1 into a gif and display it
"/Textures/Interface/StatusEffects/Human/humancrit-0.png"); //Todo: combine humancrit-0 and humancrit-1 into a gif and display it
} }
if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlay)) if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlay))

View File

@@ -1,4 +1,5 @@
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State; using Content.Shared.GameObjects.Components.Mobs.State;
@@ -18,10 +19,9 @@ namespace Content.Server.GameObjects.Components.Mobs.State
appearance.SetData(DamageStateVisuals.State, DamageState.Dead); appearance.SetData(DamageStateVisuals.State, DamageState.Dead);
} }
if (entity.TryGetComponent(out ServerStatusEffectsComponent status)) if (entity.TryGetComponent(out ServerAlertsComponent status))
{ {
status.ChangeStatusEffectIcon(StatusEffect.Health, status.ShowAlert(AlertType.HumanDead);
"/Textures/Interface/StatusEffects/Human/humandead.png");
} }
if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlayComponent)) if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlayComponent))

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State; using Content.Shared.GameObjects.Components.Mobs.State;
@@ -51,9 +52,9 @@ namespace Content.Server.GameObjects.Components.Mobs.State
// TODO: Might want to add an OnRemove() to IMobState since those are where these components are being used // TODO: Might want to add an OnRemove() to IMobState since those are where these components are being used
base.OnRemove(); base.OnRemove();
if (Owner.TryGetComponent(out ServerStatusEffectsComponent status)) if (Owner.TryGetComponent(out ServerAlertsComponent status))
{ {
status.RemoveStatusEffect(StatusEffect.Health); status.ClearAlert(AlertType.HumanHealth);
} }
if (Owner.TryGetComponent(out ServerOverlayEffectsComponent overlay)) if (Owner.TryGetComponent(out ServerOverlayEffectsComponent overlay))

View File

@@ -1,5 +1,6 @@
using Content.Server.GameObjects.Components.Damage; using Content.Server.GameObjects.Components.Damage;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Mobs.State; using Content.Shared.GameObjects.Components.Mobs.State;
@@ -27,15 +28,14 @@ namespace Content.Server.GameObjects.Components.Mobs.State
public override void UpdateState(IEntity entity) public override void UpdateState(IEntity entity)
{ {
if (!entity.TryGetComponent(out ServerStatusEffectsComponent status)) if (!entity.TryGetComponent(out ServerAlertsComponent status))
{ {
return; return;
} }
if (!entity.TryGetComponent(out IDamageableComponent damageable)) if (!entity.TryGetComponent(out IDamageableComponent damageable))
{ {
status.ChangeStatusEffectIcon(StatusEffect.Health, status.ShowAlert(AlertType.HumanHealth, 0);
"/Textures/Interface/StatusEffects/Human/human0.png");
return; return;
} }
@@ -49,10 +49,9 @@ namespace Content.Server.GameObjects.Components.Mobs.State
return; return;
} }
var modifier = (int) (ruinable.TotalDamage / (threshold / 7f)); var modifier = (short) (ruinable.TotalDamage / (threshold / 7f));
status.ChangeStatusEffectIcon(StatusEffect.Health, status.ShowAlert(AlertType.HumanHealth, modifier);
"/Textures/Interface/StatusEffects/Human/human" + modifier + ".png");
break; break;
} }
@@ -63,10 +62,9 @@ namespace Content.Server.GameObjects.Components.Mobs.State
return; return;
} }
var modifier = (int) (damageable.TotalDamage / (threshold / 7f)); var modifier = (short) (damageable.TotalDamage / (threshold / 7f));
status.ChangeStatusEffectIcon(StatusEffect.Health, status.ShowAlert(AlertType.HumanHealth, modifier);
"/Textures/Interface/StatusEffects/Human/human" + modifier + ".png");
break; break;
} }
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.Components.Movement;
@@ -89,7 +90,7 @@ namespace Content.Server.GameObjects.Components.Mobs
} }
if (!StunStart.HasValue || !StunEnd.HasValue || if (!StunStart.HasValue || !StunEnd.HasValue ||
!Owner.TryGetComponent(out ServerStatusEffectsComponent status)) !Owner.TryGetComponent(out ServerAlertsComponent status))
{ {
return; return;
} }
@@ -102,7 +103,7 @@ namespace Content.Server.GameObjects.Components.Mobs
if (progress >= length) if (progress >= length)
{ {
Owner.SpawnTimer(250, () => status.RemoveStatusEffect(StatusEffect.Stun), StatusRemoveCancellation.Token); Owner.SpawnTimer(250, () => status.ClearAlert(AlertType.Stun), StatusRemoveCancellation.Token);
LastStun = null; LastStun = null;
} }
} }

View File

@@ -1,6 +1,7 @@
#nullable enable #nullable enable
using Content.Server.GameObjects.Components.Buckle; using Content.Server.GameObjects.Components.Buckle;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Strap; using Content.Shared.GameObjects.Components.Strap;
@@ -31,9 +32,9 @@ namespace Content.Server.GameObjects.Components.Movement
private bool _movingRight; private bool _movingRight;
/// <summary> /// <summary>
/// The icon to be displayed when piloting from this chair. /// ID of the alert to show when piloting
/// </summary> /// </summary>
private string _pilotingIcon = default!; private AlertType _pilotingAlertType;
/// <summary> /// <summary>
/// The entity that's currently controlling this component. /// The entity that's currently controlling this component.
@@ -137,7 +138,7 @@ namespace Content.Server.GameObjects.Components.Movement
if (_controller != null || if (_controller != null ||
!entity.TryGetComponent(out MindComponent? mind) || !entity.TryGetComponent(out MindComponent? mind) ||
mind.Mind == null || mind.Mind == null ||
!Owner.TryGetComponent(out ServerStatusEffectsComponent? status)) !Owner.TryGetComponent(out ServerAlertsComponent? status))
{ {
return; return;
} }
@@ -145,7 +146,15 @@ namespace Content.Server.GameObjects.Components.Movement
mind.Mind.Visit(Owner); mind.Mind.Visit(Owner);
_controller = entity; _controller = entity;
status.ChangeStatusEffectIcon(StatusEffect.Piloting, _pilotingIcon); status.ShowAlert(_pilotingAlertType, onClickAlert: OnClickAlert);
}
private void OnClickAlert(ClickAlertEventArgs args)
{
if (args.Player.TryGetComponent(out ShuttleControllerComponent? controller))
{
controller.RemoveController();
}
} }
/// <summary> /// <summary>
@@ -177,9 +186,9 @@ namespace Content.Server.GameObjects.Components.Movement
/// <param name="entity">The entity to update</param> /// <param name="entity">The entity to update</param>
private void UpdateRemovedEntity(IEntity entity) private void UpdateRemovedEntity(IEntity entity)
{ {
if (Owner.TryGetComponent(out ServerStatusEffectsComponent? status)) if (Owner.TryGetComponent(out ServerAlertsComponent? status))
{ {
status.RemoveStatusEffect(StatusEffect.Piloting); status.ClearAlert(_pilotingAlertType);
} }
if (entity.TryGetComponent(out MindComponent? mind)) if (entity.TryGetComponent(out MindComponent? mind))
@@ -211,13 +220,13 @@ namespace Content.Server.GameObjects.Components.Movement
{ {
base.ExposeData(serializer); base.ExposeData(serializer);
serializer.DataField(ref _pilotingIcon, "pilotingIcon", "/Textures/Interface/StatusEffects/Buckle/buckled.png"); serializer.DataField(ref _pilotingAlertType, "pilotingAlertType", AlertType.PilotingShuttle);
} }
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
Owner.EnsureComponent<ServerStatusEffectsComponent>(); Owner.EnsureComponent<ServerAlertsComponent>();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
@@ -70,11 +71,11 @@ namespace Content.Server.GameObjects.Components.Nutrition
} }
public static readonly Dictionary<HungerThreshold, string> HungerThresholdImages = new Dictionary<HungerThreshold, string> public static readonly Dictionary<HungerThreshold, AlertType> HungerThresholdAlertTypes = new Dictionary<HungerThreshold, AlertType>
{ {
{ HungerThreshold.Overfed, "/Textures/Interface/StatusEffects/Hunger/Overfed.png" }, { HungerThreshold.Overfed, AlertType.Overfed },
{ HungerThreshold.Peckish, "/Textures/Interface/StatusEffects/Hunger/Peckish.png" }, { HungerThreshold.Peckish, AlertType.Peckish },
{ HungerThreshold.Starving, "/Textures/Interface/StatusEffects/Hunger/Starving.png" }, { HungerThreshold.Starving, AlertType.Starving },
}; };
public void HungerThresholdEffect(bool force = false) public void HungerThresholdEffect(bool force = false)
@@ -89,15 +90,15 @@ namespace Content.Server.GameObjects.Components.Nutrition
} }
// Update UI // Update UI
Owner.TryGetComponent(out ServerStatusEffectsComponent statusEffectsComponent); Owner.TryGetComponent(out ServerAlertsComponent alertsComponent);
if (HungerThresholdImages.TryGetValue(_currentHungerThreshold, out var statusTexture)) if (HungerThresholdAlertTypes.TryGetValue(_currentHungerThreshold, out var alertId))
{ {
statusEffectsComponent?.ChangeStatusEffectIcon(StatusEffect.Hunger, statusTexture); alertsComponent?.ShowAlert(alertId);
} }
else else
{ {
statusEffectsComponent?.RemoveStatusEffect(StatusEffect.Hunger); alertsComponent?.ClearAlertCategory(AlertCategory.Hunger);
} }
switch (_currentHungerThreshold) switch (_currentHungerThreshold)

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
@@ -62,11 +63,11 @@ namespace Content.Server.GameObjects.Components.Nutrition
{ThirstThreshold.Dead, 0.0f}, {ThirstThreshold.Dead, 0.0f},
}; };
public static readonly Dictionary<ThirstThreshold, string> ThirstThresholdImages = new Dictionary<ThirstThreshold, string> public static readonly Dictionary<ThirstThreshold, AlertType> ThirstThresholdAlertTypes = new Dictionary<ThirstThreshold, AlertType>
{ {
{ThirstThreshold.OverHydrated, "/Textures/Interface/StatusEffects/Thirst/OverHydrated.png"}, {ThirstThreshold.OverHydrated, AlertType.Overhydrated},
{ThirstThreshold.Thirsty, "/Textures/Interface/StatusEffects/Thirst/Thirsty.png"}, {ThirstThreshold.Thirsty, AlertType.Thirsty},
{ThirstThreshold.Parched, "/Textures/Interface/StatusEffects/Thirst/Parched.png"}, {ThirstThreshold.Parched, AlertType.Parched},
}; };
public override void ExposeData(ObjectSerializer serializer) public override void ExposeData(ObjectSerializer serializer)
@@ -87,15 +88,15 @@ namespace Content.Server.GameObjects.Components.Nutrition
} }
// Update UI // Update UI
Owner.TryGetComponent(out ServerStatusEffectsComponent statusEffectsComponent); Owner.TryGetComponent(out ServerAlertsComponent alertsComponent);
if (ThirstThresholdImages.TryGetValue(_currentThirstThreshold, out var statusTexture)) if (ThirstThresholdAlertTypes.TryGetValue(_currentThirstThreshold, out var alertId))
{ {
statusEffectsComponent?.ChangeStatusEffectIcon(StatusEffect.Thirst, statusTexture); alertsComponent?.ShowAlert(alertId);
} }
else else
{ {
statusEffectsComponent?.RemoveStatusEffect(StatusEffect.Thirst); alertsComponent?.ClearAlertCategory(AlertCategory.Thirst);
} }
switch (_currentThirstThreshold) switch (_currentThirstThreshold)

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Server.GameObjects.Components.Buckle; using Content.Server.GameObjects.Components.Buckle;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Strap; using Content.Shared.GameObjects.Components.Strap;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs; using Content.Shared.GameObjects.Verbs;
@@ -27,7 +28,7 @@ namespace Content.Server.GameObjects.Components.Strap
private StrapPosition _position; private StrapPosition _position;
private string _buckleSound = null!; private string _buckleSound = null!;
private string _unbuckleSound = null!; private string _unbuckleSound = null!;
private string _buckledIcon = null!; private AlertType _buckledAlertType;
/// <summary> /// <summary>
/// The angle in degrees to rotate the player by when they get strapped /// The angle in degrees to rotate the player by when they get strapped
@@ -65,10 +66,10 @@ namespace Content.Server.GameObjects.Components.Strap
public string UnbuckleSound => _unbuckleSound; public string UnbuckleSound => _unbuckleSound;
/// <summary> /// <summary>
/// The icon to be displayed as a status when buckled /// ID of the alert to show when buckled
/// </summary> /// </summary>
[ViewVariables] [ViewVariables]
public string BuckledIcon => _buckledIcon; public AlertType BuckledAlertType => _buckledAlertType;
/// <summary> /// <summary>
/// The sum of the sizes of all the buckled entities in this strap /// The sum of the sizes of all the buckled entities in this strap
@@ -137,7 +138,7 @@ namespace Content.Server.GameObjects.Components.Strap
serializer.DataField(ref _position, "position", StrapPosition.None); serializer.DataField(ref _position, "position", StrapPosition.None);
serializer.DataField(ref _buckleSound, "buckleSound", "/Audio/Effects/buckle.ogg"); serializer.DataField(ref _buckleSound, "buckleSound", "/Audio/Effects/buckle.ogg");
serializer.DataField(ref _unbuckleSound, "unbuckleSound", "/Audio/Effects/unbuckle.ogg"); serializer.DataField(ref _unbuckleSound, "unbuckleSound", "/Audio/Effects/unbuckle.ogg");
serializer.DataField(ref _buckledIcon, "buckledIcon", "/Textures/Interface/StatusEffects/Buckle/buckled.png"); serializer.DataField(ref _buckledAlertType, "buckledAlertType", AlertType.Buckled);
serializer.DataField(ref _rotation, "rotation", 0); serializer.DataField(ref _rotation, "rotation", 0);
var defaultSize = 100; var defaultSize = 100;

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Damage;
@@ -74,43 +75,43 @@ namespace Content.Server.GameObjects.Components.Temperature
damageType = DamageType.Cold; damageType = DamageType.Cold;
} }
if (Owner.TryGetComponent(out ServerStatusEffectsComponent status)) if (Owner.TryGetComponent(out ServerAlertsComponent status))
{ {
switch(CurrentTemperature) switch(CurrentTemperature)
{ {
// Cold strong. // Cold strong.
case var t when t <= 260: case var t when t <= 260:
status.ChangeStatusEffect(StatusEffect.Temperature, "/Textures/Interface/StatusEffects/Temperature/cold3.png", null); status.ShowAlert(AlertType.Cold, 3);
break; break;
// Cold mild. // Cold mild.
case var t when t <= 280 && t > 260: case var t when t <= 280 && t > 260:
status.ChangeStatusEffect(StatusEffect.Temperature, "/Textures/Interface/StatusEffects/Temperature/cold2.png", null); status.ShowAlert(AlertType.Cold, 2);
break; break;
// Cold weak. // Cold weak.
case var t when t <= 292 && t > 280: case var t when t <= 292 && t > 280:
status.ChangeStatusEffect(StatusEffect.Temperature, "/Textures/Interface/StatusEffects/Temperature/cold1.png", null); status.ShowAlert(AlertType.Cold, 1);
break; break;
// Safe. // Safe.
case var t when t <= 327 && t > 292: case var t when t <= 327 && t > 292:
status.RemoveStatusEffect(StatusEffect.Temperature); status.ClearAlertCategory(AlertCategory.Temperature);
break; break;
// Heat weak. // Heat weak.
case var t when t <= 335 && t > 327: case var t when t <= 335 && t > 327:
status.ChangeStatusEffect(StatusEffect.Temperature, "/Textures/Interface/StatusEffects/Temperature/hot1.png", null); status.ShowAlert(AlertType.Hot, 1);
break; break;
// Heat mild. // Heat mild.
case var t when t <= 345 && t > 335: case var t when t <= 345 && t > 335:
status.ChangeStatusEffect(StatusEffect.Temperature, "/Textures/Interface/StatusEffects/Temperature/hot2.png", null); status.ShowAlert(AlertType.Hot, 2);
break; break;
// Heat strong. // Heat strong.
case var t when t > 345: case var t when t > 345:
status.ChangeStatusEffect(StatusEffect.Temperature, "/Textures/Interface/StatusEffects/Temperature/hot3.png", null); status.ShowAlert(AlertType.Hot, 3);
break; break;
} }
} }

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystemMessages.Gravity; using Content.Shared.GameObjects.EntitySystemMessages.Gravity;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
@@ -19,7 +20,7 @@ namespace Content.Server.GameObjects.EntitySystems
{ {
[Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IMapManager _mapManager = default!;
private readonly Dictionary<GridId, List<ServerStatusEffectsComponent>> _statuses = new Dictionary<GridId, List<ServerStatusEffectsComponent>>(); private readonly Dictionary<GridId, List<ServerAlertsComponent>> _alerts = new Dictionary<GridId, List<ServerAlertsComponent>>();
public override void Initialize() public override void Initialize()
{ {
@@ -31,15 +32,15 @@ namespace Content.Server.GameObjects.EntitySystems
public void Reset() public void Reset()
{ {
_statuses.Clear(); _alerts.Clear();
} }
public void AddStatus(ServerStatusEffectsComponent status) public void AddAlert(ServerAlertsComponent status)
{ {
var gridId = status.Owner.Transform.GridID; var gridId = status.Owner.Transform.GridID;
var statuses = _statuses.GetOrNew(gridId); var alerts = _alerts.GetOrNew(gridId);
statuses.Add(status); alerts.Add(status);
if (_mapManager.TryGetGrid(status.Owner.Transform.GridID, out var grid)) if (_mapManager.TryGetGrid(status.Owner.Transform.GridID, out var grid))
{ {
@@ -54,10 +55,10 @@ namespace Content.Server.GameObjects.EntitySystems
} }
} }
public void RemoveStatus(ServerStatusEffectsComponent status) public void RemoveAlert(ServerAlertsComponent status)
{ {
var grid = status.Owner.Transform.GridID; var grid = status.Owner.Transform.GridID;
if (!_statuses.TryGetValue(grid, out var statuses)) if (!_alerts.TryGetValue(grid, out var statuses))
{ {
return; return;
} }
@@ -67,7 +68,7 @@ namespace Content.Server.GameObjects.EntitySystems
private void GravityChanged(GravityChangedMessage ev) private void GravityChanged(GravityChangedMessage ev)
{ {
if (!_statuses.TryGetValue(ev.Grid.Index, out var statuses)) if (!_alerts.TryGetValue(ev.Grid.Index, out var statuses))
{ {
return; return;
} }
@@ -88,19 +89,19 @@ namespace Content.Server.GameObjects.EntitySystems
} }
} }
private void AddWeightless(ServerStatusEffectsComponent status) private void AddWeightless(ServerAlertsComponent status)
{ {
status.ChangeStatusEffect(StatusEffect.Weightless, "/Textures/Interface/StatusEffects/Weightless/weightless.png", null); status.ShowAlert(AlertType.Weightless);
} }
private void RemoveWeightless(ServerStatusEffectsComponent status) private void RemoveWeightless(ServerAlertsComponent status)
{ {
status.RemoveStatusEffect(StatusEffect.Weightless); status.ClearAlert(AlertType.Weightless);
} }
private void EntParentChanged(EntParentChangedMessage ev) private void EntParentChanged(EntParentChangedMessage ev)
{ {
if (!ev.Entity.TryGetComponent(out ServerStatusEffectsComponent status)) if (!ev.Entity.TryGetComponent(out ServerAlertsComponent status))
{ {
return; return;
} }
@@ -110,14 +111,14 @@ namespace Content.Server.GameObjects.EntitySystems
{ {
var oldGrid = mapGrid.GridIndex; var oldGrid = mapGrid.GridIndex;
if (_statuses.TryGetValue(oldGrid, out var oldStatuses)) if (_alerts.TryGetValue(oldGrid, out var oldStatuses))
{ {
oldStatuses.Remove(status); oldStatuses.Remove(status);
} }
} }
var newGrid = ev.Entity.Transform.GridID; var newGrid = ev.Entity.Transform.GridID;
var newStatuses = _statuses.GetOrNew(newGrid); var newStatuses = _alerts.GetOrNew(newGrid);
newStatuses.Add(status); newStatuses.Add(status);
} }

View File

@@ -18,6 +18,7 @@ using Content.Server.Sandbox;
using Content.Server.Utility; using Content.Server.Utility;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Content.Shared.Kitchen; using Content.Shared.Kitchen;
using Content.Shared.Alert;
using Robust.Shared.IoC; using Robust.Shared.IoC;
namespace Content.Server namespace Content.Server
@@ -37,6 +38,7 @@ namespace Content.Server
IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>(); IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
IoCManager.Register<IServerDbManager, ServerDbManager>(); IoCManager.Register<IServerDbManager, ServerDbManager>();
IoCManager.Register<RecipeManager, RecipeManager>(); IoCManager.Register<RecipeManager, RecipeManager>();
IoCManager.Register<AlertManager, AlertManager>();
IoCManager.Register<IPDAUplinkManager,PDAUplinkManager>(); IoCManager.Register<IPDAUplinkManager,PDAUplinkManager>();
IoCManager.Register<INodeGroupFactory, NodeGroupFactory>(); IoCManager.Register<INodeGroupFactory, NodeGroupFactory>();
IoCManager.Register<INodeGroupManager, NodeGroupManager>(); IoCManager.Register<INodeGroupManager, NodeGroupManager>();

View 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;
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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
}
}

View File

@@ -0,0 +1,279 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Alert;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Shared.GameObjects.Components.Mobs
{
/// <summary>
/// Handles the icons on the right side of the screen.
/// Should only be used for player-controlled entities.
/// </summary>
public abstract class SharedAlertsComponent : Component
{
private static readonly AlertState[] NO_ALERTS = new AlertState[0];
[Dependency]
protected readonly AlertManager AlertManager = default!;
public override string Name => "AlertsUI";
public override uint? NetID => ContentNetIDs.ALERTS;
[ViewVariables]
private Dictionary<AlertKey, ClickableAlertState> _alerts = new Dictionary<AlertKey, ClickableAlertState>();
/// <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<AlertState> EnumerateAlertStates()
{
return _alerts.Values.Select(alertData => alertData.AlertState);
}
/// <summary>
/// Invokes the alert's specified callback if there is one.
/// Not intended to be used on clientside.
/// </summary>
protected void PerformAlertClickCallback(AlertPrototype alert, IEntity owner)
{
if (_alerts.TryGetValue(alert.AlertKey, out var alertStateCallback))
{
alertStateCallback.OnClickAlert?.Invoke(new ClickAlertEventArgs(owner, alert));
}
else
{
Logger.DebugS("alert", "player {0} attempted to invoke" +
" alert click for {1} but that alert is not currently" +
" showing", owner.Name, alert.AlertType);
}
}
/// <summary>
/// Creates a new array containing all of the current alert states.
/// </summary>
/// <returns></returns>
protected AlertState[] CreateAlertStatesArray()
{
if (_alerts.Count == 0) return NO_ALERTS;
var states = new AlertState[_alerts.Count];
// because I don't trust LINQ
var idx = 0;
foreach (var alertData in _alerts.Values)
{
states[idx++] = alertData.AlertState;
}
return states;
}
protected bool TryGetAlertState(AlertKey key, out AlertState alertState)
{
if (_alerts.TryGetValue(key, out var alertData))
{
alertState = alertData.AlertState;
return true;
}
alertState = default;
return false;
}
/// <summary>
/// Replace the current active alerts with the specified alerts. Any
/// OnClickAlert callbacks on the active alerts will be erased.
/// </summary>
protected void SetAlerts(AlertState[] alerts)
{
var newAlerts = new Dictionary<AlertKey, ClickableAlertState>();
foreach (var alertState in alerts)
{
if (AlertManager.TryDecode(alertState.AlertEncoded, out var alert))
{
newAlerts[alert.AlertKey] = new ClickableAlertState
{
AlertState = alertState
};
}
else
{
Logger.ErrorS("alert", "unrecognized encoded alert {0}", alertState.AlertEncoded);
}
}
_alerts = newAlerts;
}
/// <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="onClickAlert">callback to invoke when ClickAlertMessage is received by the server
/// after being clicked by client. Has no effect when specified on the clientside.</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, OnClickAlert onClickAlert = null,
ValueTuple<TimeSpan, TimeSpan>? cooldown = null)
{
if (AlertManager.TryGetWithEncoded(alertType, out var alert, out var encoded))
{
if (_alerts.TryGetValue(alert.AlertKey, out var alertStateCallback) &&
alertStateCallback.AlertState.AlertEncoded == encoded &&
alertStateCallback.AlertState.Severity == severity && alertStateCallback.AlertState.Cooldown == cooldown)
{
alertStateCallback.OnClickAlert = onClickAlert;
return;
}
_alerts[alert.AlertKey] = new ClickableAlertState
{
AlertState = new AlertState
{Cooldown = cooldown, AlertEncoded = encoded, Severity = severity},
OnClickAlert = onClickAlert
};
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 clearing an alert prior to dirtying the control
/// </summary>
protected virtual void AfterClearAlert() { }
}
[Serializable, NetSerializable]
public class AlertsComponentState : ComponentState
{
public AlertState[] Alerts;
public AlertsComponentState(AlertState[] alerts) : base(ContentNetIDs.ALERTS)
{
Alerts = alerts;
}
}
/// <summary>
/// A message that calls the click interaction on a alert
/// </summary>
[Serializable, NetSerializable]
public class ClickAlertMessage : ComponentMessage
{
public readonly byte EncodedAlert;
public ClickAlertMessage(byte encodedAlert)
{
Directed = true;
EncodedAlert = encodedAlert;
}
}
[Serializable, NetSerializable]
public struct AlertState
{
public byte AlertEncoded;
public short? Severity;
public ValueTuple<TimeSpan, TimeSpan>? Cooldown;
}
public struct ClickableAlertState
{
public AlertState AlertState;
public OnClickAlert OnClickAlert;
}
public delegate void OnClickAlert(ClickAlertEventArgs args);
public class ClickAlertEventArgs : EventArgs
{
/// <summary>
/// Player clicking the alert
/// </summary>
public readonly IEntity Player;
/// <summary>
/// Alert that was clicked
/// </summary>
public readonly AlertPrototype Alert;
public ClickAlertEventArgs(IEntity player, AlertPrototype alert)
{
Player = player;
Alert = alert;
}
}
}

View File

@@ -1,76 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Mobs
{
/// <summary>
/// Handles the icons on the right side of the screen.
/// Should only be used for player-controlled entities
/// </summary>
public abstract class SharedStatusEffectsComponent : Component
{
public override string Name => "StatusEffectsUI";
public override uint? NetID => ContentNetIDs.STATUSEFFECTS;
public abstract IReadOnlyDictionary<StatusEffect, StatusEffectStatus> Statuses { get; }
public abstract void ChangeStatusEffectIcon(StatusEffect effect, string icon);
public abstract void ChangeStatusEffect(StatusEffect effect, string icon, ValueTuple<TimeSpan, TimeSpan>? cooldown);
public abstract void RemoveStatusEffect(StatusEffect effect);
}
[Serializable, NetSerializable]
public class StatusEffectComponentState : ComponentState
{
public Dictionary<StatusEffect, StatusEffectStatus> StatusEffects;
public StatusEffectComponentState(Dictionary<StatusEffect, StatusEffectStatus> statusEffects) : base(ContentNetIDs.STATUSEFFECTS)
{
StatusEffects = statusEffects;
}
}
/// <summary>
/// A message that calls the click interaction on a status effect
/// </summary>
[Serializable, NetSerializable]
public class ClickStatusMessage : ComponentMessage
{
public readonly StatusEffect Effect;
public ClickStatusMessage(StatusEffect effect)
{
Directed = true;
Effect = effect;
}
}
[Serializable, NetSerializable]
public struct StatusEffectStatus
{
public string Icon;
public ValueTuple<TimeSpan, TimeSpan>? Cooldown;
}
// Each status effect is assumed to be unique
public enum StatusEffect
{
Health,
Hunger,
Thirst,
Pressure,
Fire,
Temperature,
Stun,
Cuffed,
Buckled,
Piloting,
Pulling,
Pulled,
Weightless
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Threading; using System.Threading;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.Components.Movement;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
@@ -41,7 +42,7 @@ namespace Content.Shared.GameObjects.Components.Mobs
protected float KnockdownTimer; protected float KnockdownTimer;
protected float SlowdownTimer; protected float SlowdownTimer;
private string _stunTexture; private string _stunAlertId;
protected CancellationTokenSource StatusRemoveCancellation = new CancellationTokenSource(); protected CancellationTokenSource StatusRemoveCancellation = new CancellationTokenSource();
@@ -117,7 +118,7 @@ namespace Content.Shared.GameObjects.Components.Mobs
StunnedTimer = seconds; StunnedTimer = seconds;
LastStun = _gameTiming.CurTime; LastStun = _gameTiming.CurTime;
SetStatusEffect(); SetAlert();
OnStun(); OnStun();
Dirty(); Dirty();
@@ -144,7 +145,7 @@ namespace Content.Shared.GameObjects.Components.Mobs
KnockdownTimer = seconds; KnockdownTimer = seconds;
LastStun = _gameTiming.CurTime; LastStun = _gameTiming.CurTime;
SetStatusEffect(); SetAlert();
OnKnockdown(); OnKnockdown();
Dirty(); Dirty();
@@ -186,18 +187,18 @@ namespace Content.Shared.GameObjects.Components.Mobs
if (Owner.TryGetComponent(out MovementSpeedModifierComponent movement)) if (Owner.TryGetComponent(out MovementSpeedModifierComponent movement))
movement.RefreshMovementSpeedModifiers(); movement.RefreshMovementSpeedModifiers();
SetStatusEffect(); SetAlert();
Dirty(); Dirty();
} }
private void SetStatusEffect() private void SetAlert()
{ {
if (!Owner.TryGetComponent(out SharedStatusEffectsComponent status)) if (!Owner.TryGetComponent(out SharedAlertsComponent status))
{ {
return; return;
} }
status.ChangeStatusEffect(StatusEffect.Stun, _stunTexture, status.ShowAlert(AlertType.Stun, cooldown:
(StunStart == null || StunEnd == null) ? default : (StunStart.Value, StunEnd.Value)); (StunStart == null || StunEnd == null) ? default : (StunStart.Value, StunEnd.Value));
StatusRemoveCancellation.Cancel(); StatusRemoveCancellation.Cancel();
StatusRemoveCancellation = new CancellationTokenSource(); StatusRemoveCancellation = new CancellationTokenSource();
@@ -212,8 +213,8 @@ namespace Content.Shared.GameObjects.Components.Mobs
serializer.DataField(ref _slowdownCap, "slowdownCap", 20f); serializer.DataField(ref _slowdownCap, "slowdownCap", 20f);
serializer.DataField(ref _helpInterval, "helpInterval", 1f); serializer.DataField(ref _helpInterval, "helpInterval", 1f);
serializer.DataField(ref _helpKnockdownRemove, "helpKnockdownRemove", 1f); serializer.DataField(ref _helpKnockdownRemove, "helpKnockdownRemove", 1f);
serializer.DataField(ref _stunTexture, "stunTexture", serializer.DataField(ref _stunAlertId, "stunAlertId",
"/Textures/Objects/Weapons/Melee/stunbaton.rsi/stunbaton_off.png"); "stun");
} }
protected virtual void OnInteractHand() { } protected virtual void OnInteractHand() { }
@@ -230,7 +231,7 @@ namespace Content.Shared.GameObjects.Components.Mobs
KnockdownTimer -= _helpKnockdownRemove; KnockdownTimer -= _helpKnockdownRemove;
SetStatusEffect(); SetAlert();
Dirty(); Dirty();
return true; return true;

View File

@@ -1,12 +1,15 @@
#nullable enable #nullable enable
using System; using System;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Physics; using Content.Shared.Physics;
using Content.Shared.Physics.Pull; using Content.Shared.Physics.Pull;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.ComponentDependencies; using Robust.Shared.GameObjects.ComponentDependencies;
using Robust.Shared.GameObjects.Components; using Robust.Shared.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Physics; using Robust.Shared.Physics;
@@ -204,29 +207,36 @@ namespace Content.Shared.GameObjects.Components.Pulling
private void AddPullingStatuses(IEntity puller) private void AddPullingStatuses(IEntity puller)
{ {
if (Owner.TryGetComponent(out SharedStatusEffectsComponent? pulledStatus)) if (Owner.TryGetComponent(out SharedAlertsComponent? pulledStatus))
{ {
pulledStatus.ChangeStatusEffectIcon(StatusEffect.Pulled, pulledStatus.ShowAlert(AlertType.Pulled);
"/Textures/Interface/StatusEffects/Pull/pulled.png");
} }
if (puller.TryGetComponent(out SharedStatusEffectsComponent? ownerStatus)) if (puller.TryGetComponent(out SharedAlertsComponent? ownerStatus))
{ {
ownerStatus.ChangeStatusEffectIcon(StatusEffect.Pulling, ownerStatus.ShowAlert(AlertType.Pulling, onClickAlert: OnClickAlert);
"/Textures/Interface/StatusEffects/Pull/pulling.png");
} }
} }
private void OnClickAlert(ClickAlertEventArgs args)
{
EntitySystem
.Get<SharedPullingSystem>()
.GetPulled(args.Player)?
.GetComponentOrNull<SharedPullableComponent>()?
.TryStopPull();
}
private void RemovePullingStatuses(IEntity puller) private void RemovePullingStatuses(IEntity puller)
{ {
if (Owner.TryGetComponent(out SharedStatusEffectsComponent? pulledStatus)) if (Owner.TryGetComponent(out SharedAlertsComponent? pulledStatus))
{ {
pulledStatus.RemoveStatusEffect(StatusEffect.Pulled); pulledStatus.ClearAlert(AlertType.Pulled);
} }
if (puller.TryGetComponent(out SharedStatusEffectsComponent? ownerStatus)) if (puller.TryGetComponent(out SharedAlertsComponent? ownerStatus))
{ {
ownerStatus.RemoveStatusEffect(StatusEffect.Pulling); ownerStatus.ClearAlert(AlertType.Pulling);
} }
} }

View File

@@ -29,7 +29,7 @@
public const uint RESEARCH_CONSOLE = 1023; public const uint RESEARCH_CONSOLE = 1023;
public const uint WIRES = 1024; public const uint WIRES = 1024;
public const uint COMBATMODE = 1025; public const uint COMBATMODE = 1025;
public const uint STATUSEFFECTS = 1026; public const uint ALERTS = 1026;
public const uint OVERLAYEFFECTS = 1027; public const uint OVERLAYEFFECTS = 1027;
public const uint STOMACH = 1028; public const uint STOMACH = 1028;
public const uint ITEMCOOLDOWN = 1029; public const uint ITEMCOOLDOWN = 1029;

View File

@@ -0,0 +1,71 @@
using System.IO;
using System.Linq;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.Utility;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Tests.Server.GameObjects.Components.Mobs
{
[TestFixture]
[TestOf(typeof(ServerAlertsComponent))]
public class ServerAlertsComponentTests : ContentUnitTest
{
const string PROTOTYPES = @"
- type: alert
alertType: LowPressure
category: Pressure
icon: /Textures/Interface/Alerts/Pressure/lowpressure.png
- type: alert
alertType: HighPressure
category: Pressure
icon: /Textures/Interface/Alerts/Pressure/highpressure.png
";
[Test]
public void ShowAlerts()
{
// this is kind of unnecessary because there's integration test coverage of Alert components
// but wanted to keep it anyway to see what's possible w.r.t. testing components
// in a unit test
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.RegisterType(typeof(AlertPrototype));
var factory = IoCManager.Resolve<IComponentFactory>();
factory.Register<ServerAlertsComponent>();
prototypeManager.LoadFromStream(new StringReader(PROTOTYPES));
prototypeManager.Resync();
var alertManager = IoCManager.Resolve<AlertManager>();
alertManager.Initialize();
var alertsComponent = new ServerAlertsComponent();
alertsComponent = IoCManager.InjectDependencies(alertsComponent);
Assert.That(alertManager.TryGetWithEncoded(AlertType.LowPressure, out var lowpressure, out var lpencoded));
Assert.That(alertManager.TryGetWithEncoded(AlertType.HighPressure, out var highpressure, out var hpencoded));
alertsComponent.ShowAlert(AlertType.LowPressure);
var alertState = alertsComponent.GetComponentState() as AlertsComponentState;
Assert.NotNull(alertState);
Assert.That(alertState.Alerts.Length, Is.EqualTo(1));
Assert.That(alertState.Alerts[0], Is.EqualTo(new AlertState{AlertEncoded = lpencoded}));
alertsComponent.ShowAlert(AlertType.HighPressure);
alertState = alertsComponent.GetComponentState() as AlertsComponentState;
Assert.That(alertState.Alerts.Length, Is.EqualTo(1));
Assert.That(alertState.Alerts[0], Is.EqualTo(new AlertState{AlertEncoded = hpencoded}));
alertsComponent.ClearAlertCategory(AlertCategory.Pressure);
alertState = alertsComponent.GetComponentState() as AlertsComponentState;
Assert.That(alertState.Alerts.Length, Is.EqualTo(0));
}
}
}

View File

@@ -0,0 +1,63 @@
using System.IO;
using Content.Shared.Alert;
using NUnit.Framework;
using Robust.Shared.Interfaces.Log;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.UnitTesting;
using YamlDotNet.RepresentationModel;
namespace Content.Tests.Shared.Alert
{
[TestFixture, TestOf(typeof(AlertManager))]
public class AlertManagerTests : RobustUnitTest
{
const string PROTOTYPES = @"
- type: alert
alertType: LowPressure
icon: /Textures/Interface/Alerts/Pressure/lowpressure.png
- type: alert
alertType: HighPressure
icon: /Textures/Interface/Alerts/Pressure/highpressure.png
";
[Test]
public void TestAlertManager()
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.RegisterType(typeof(AlertPrototype));
prototypeManager.LoadFromStream(new StringReader(PROTOTYPES));
IoCManager.RegisterInstance<AlertManager>(new AlertManager());
var alertManager = IoCManager.Resolve<AlertManager>();
alertManager.Initialize();
Assert.That(alertManager.TryGet(AlertType.LowPressure, out var lowPressure));
Assert.That(lowPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/lowpressure.png"));
Assert.That(alertManager.TryGet(AlertType.HighPressure, out var highPressure));
Assert.That(highPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/highpressure.png"));
Assert.That(alertManager.TryGetWithEncoded(AlertType.LowPressure, out lowPressure, out var encodedLowPressure));
Assert.That(lowPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/lowpressure.png"));
Assert.That(alertManager.TryGetWithEncoded(AlertType.HighPressure, out highPressure, out var encodedHighPressure));
Assert.That(highPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/highpressure.png"));
Assert.That(alertManager.TryEncode(lowPressure, out var encodedLowPressure2));
Assert.That(encodedLowPressure2, Is.EqualTo(encodedLowPressure));
Assert.That(alertManager.TryEncode(highPressure, out var encodedHighPressure2));
Assert.That(encodedHighPressure2, Is.EqualTo(encodedHighPressure));
Assert.That(encodedLowPressure, Is.Not.EqualTo(encodedHighPressure));
Assert.That(alertManager.TryDecode(encodedLowPressure, out var decodedLowPressure));
Assert.That(decodedLowPressure, Is.EqualTo(lowPressure));
Assert.That(alertManager.TryDecode(encodedHighPressure, out var decodedHighPressure));
Assert.That(decodedHighPressure, Is.EqualTo(highPressure));
Assert.False(alertManager.TryEncode(AlertType.Debug1, out _));
Assert.False(alertManager.TryGetWithEncoded(AlertType.Debug1, out _, out _));
}
}
}

View File

@@ -0,0 +1,96 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Content.Shared.Alert;
using NUnit.Framework;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.UnitTesting;
namespace Content.Tests.Shared.Alert
{
[TestFixture, TestOf(typeof(AlertOrderPrototype))]
public class AlertOrderPrototypeTests : RobustUnitTest
{
const string PROTOTYPES = @"
- type: alertOrder
order:
- alertType: Handcuffed
- category: Pressure
- category: Hunger
- alertType: Hot
- alertType: Stun
- alertType: LowPressure
- category: Temperature
- type: alert
category: Pressure
alertType: LowPressure
- type: alert
category: Hunger
alertType: Overfed
- type: alert
category: Pressure
alertType: HighPressure
- type: alert
category: Hunger
alertType: Peckish
- type: alert
alertType: Stun
- type: alert
alertType: Handcuffed
- type: alert
category: Temperature
alertType: Hot
- type: alert
category: Temperature
alertType: Cold
- type: alert
alertType: Weightless
- type: alert
alertType: PilotingShuttle
";
[Test]
public void TestAlertOrderPrototype()
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
prototypeManager.RegisterType(typeof(AlertPrototype));
prototypeManager.RegisterType(typeof(AlertOrderPrototype));
prototypeManager.LoadFromStream(new StringReader(PROTOTYPES));
var alertOrder = prototypeManager.EnumeratePrototypes<AlertOrderPrototype>().FirstOrDefault();
var alerts = prototypeManager.EnumeratePrototypes<AlertPrototype>();
// ensure they sort according to our expected criteria
var expectedOrder = new List<AlertType>();
expectedOrder.Add(AlertType.Handcuffed);
expectedOrder.Add(AlertType.HighPressure);
// stuff with only category + same category ordered by enum value
expectedOrder.Add(AlertType.Overfed);
expectedOrder.Add(AlertType.Peckish);
expectedOrder.Add(AlertType.Hot);
expectedOrder.Add(AlertType.Stun);
expectedOrder.Add(AlertType.LowPressure);
expectedOrder.Add(AlertType.Cold);
// stuff at end of list ordered by enum value
expectedOrder.Add(AlertType.Weightless);
expectedOrder.Add(AlertType.PilotingShuttle);
var actual = alerts.ToList();
actual.Sort(alertOrder);
Assert.That(actual.Select(a => a.AlertType).ToList(), Is.EqualTo(expectedOrder));
}
}
}

View File

@@ -0,0 +1,62 @@
using System.IO;
using Content.Shared.Alert;
using NUnit.Framework;
using Robust.Shared.Interfaces.Log;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using Robust.UnitTesting;
using YamlDotNet.RepresentationModel;
namespace Content.Tests.Shared.Alert
{
[TestFixture, TestOf(typeof(AlertPrototype))]
public class AlertPrototypeTests : RobustUnitTest
{
private const string PROTOTYPE = @"- type: alert
alertType: HumanHealth
category: Health
icon: /Textures/Interface/Alerts/Human/human.rsi/human.png
name: Health
description: ""[color=green]Green[/color] good. [color=red]Red[/color] bad.""
minSeverity: 0
maxSeverity: 6";
[Test]
public void TestAlertKey()
{
Assert.That(new AlertKey(AlertType.HumanHealth, null), Is.Not.EqualTo(AlertKey.ForCategory(AlertCategory.Health)));
Assert.That((new AlertKey(null, AlertCategory.Health)), Is.EqualTo(AlertKey.ForCategory(AlertCategory.Health)));
Assert.That((new AlertKey(AlertType.Buckled, AlertCategory.Health)), Is.EqualTo(AlertKey.ForCategory(AlertCategory.Health)));
}
[TestCase(0, "/Textures/Interface/Alerts/Human/human.rsi/human0.png")]
[TestCase(null, "/Textures/Interface/Alerts/Human/human.rsi/human0.png")]
[TestCase(1, "/Textures/Interface/Alerts/Human/human.rsi/human1.png")]
[TestCase(6, "/Textures/Interface/Alerts/Human/human.rsi/human6.png")]
[TestCase(7, "/Textures/Interface/Alerts/Human/human.rsi/human6.png")]
public void GetsIconPath(short? severity, string expected)
{
var alert = GetTestPrototype();
Assert.That(alert.GetIconPath(severity), Is.EqualTo(expected));
}
private AlertPrototype GetTestPrototype()
{
using (TextReader stream = new StringReader(PROTOTYPE))
{
var yamlStream = new YamlStream();
yamlStream.Load(stream);
var document = yamlStream.Documents[0];
var rootNode = (YamlSequenceNode) document.RootNode;
var proto = (YamlMappingNode) rootNode[0];
var newReagent = new AlertPrototype();
newReagent.LoadFrom(proto);
return newReagent;
}
}
}
}

View File

@@ -232,6 +232,8 @@
- attachtogrid - attachtogrid
- attachtograndparent - attachtograndparent
- inrangeunoccluded - inrangeunoccluded
- showalert
- clearalert
- hungry - hungry
CanViewVar: true CanViewVar: true
CanAdminPlace: true CanAdminPlace: true

View File

@@ -0,0 +1,205 @@
- type: alertOrder
# Defines ordering in alert tab, higher up = higher in tab.
# List below can contain alert type or category, if both are present the id will take precedence.
# If item is not in list it will go at the bottom (ties broken by alert type enum value)
order:
- category: Health
- alertType: Fire
- alertType: Handcuffed
- category: Buckled
- alertType: Pulling
- category: Piloting
- alertType: Stun
- category: Pressure
- category: Temperature
- category: Hunger
- category: Thirst
- type: alert
alertType: LowPressure
category: Pressure
icon: /Textures/Interface/Alerts/Pressure/lowpressure.png
maxSeverity: 2
name: "[color=red]Low Pressure[/color]"
description: "The air around you is [color=red]hazardously thin[/color]. A [color=green]space suit[/color] would protect you."
- type: alert
alertType: HighPressure
category: Pressure
icon: /Textures/Interface/Alerts/Pressure/highpressure.png
maxSeverity: 2
name: "[color=red]High Pressure[/color]"
description: "The air around you is [color=red]hazardously thick[/color]. A [color=green]fire suit[/color] would protect you."
- type: alert
alertType: Fire
icon: /Textures/Interface/Alerts/Fire/fire.png
name: "[color=red]On Fire[/color]"
description: "You're [color=red]on fire[/color]. Click the alert to stop, drop and roll to put the fire out or move to a vacuum area."
- type: alert
alertType: Cold
category: Temperature
icon: /Textures/Interface/Alerts/Temperature/cold.png
maxSeverity: 3
name: "[color=cyan]Too Cold[/color]"
description: "You're [color=cyan]freezing cold![/color] Get somewhere warmer and take off any insulating clothing like a space suit."
- type: alert
alertType: Hot
category: Temperature
icon: /Textures/Interface/Alerts/Temperature/hot.png
maxSeverity: 3
name: "[color=red]Too Hot[/color]"
description: "It's [color=red]too hot![/color] Flee to space or at least away from the flames. Standing on weeds will heal you."
- type: alert
alertType: Weightless
icon: /Textures/Interface/Alerts/Weightless/weightless.png
name: Weightless
description: >-
Gravity has ceased affecting you, and you're floating around aimlessly. You'll need something large and heavy, like a
wall or lattice, to push yourself off if you want to move. A jetpack would enable free range of motion. A pair of
magboots would let you walk around normally on the floor. Barring those, you can throw things, use a fire extinguisher,
or shoot a gun to move around via Newton's 3rd Law of Motion.
- type: alert
alertType: Stun
icon: /Textures/Objects/Weapons/Melee/stunbaton.rsi/stunbaton_off.png
name: "[color=yellow]Stunned[/color]"
description: "You're [color=yellow]stunned[/color]! Wait for it to wear off."
- type: alert
alertType: Handcuffed
icon: /Textures/Interface/Alerts/Handcuffed/Handcuffed.png
name: "[color=yellow]Handcuffed[/color]"
description: "You're [color=yellow]handcuffed[/color] and can't act. If anyone drags you, you won't be able to move.."
- type: alert
alertType: Buckled
category: Buckled
icon: /Textures/Interface/Alerts/Buckle/buckled.png
name: "[color=yellow]Buckled[/color]"
description: "You've been [color=yellow]buckled[/color] to something. Click the alert to unbuckle unless you're [color=yellow]handcuffed.[/color]"
- type: alert
alertType: HumanCrit
category: Health
icon: /Textures/Interface/Alerts/Human/humancrit-0.png
name: "[color=red]Critical Condition[/color]"
description: "You're severely injured and unconscious."
- type: alert
alertType: HumanDead
category: Health
icon: /Textures/Interface/Alerts/Human/humandead.png
name: Dead
description: You're dead.
- type: alert
alertType: HumanHealth
category: Health
icon: /Textures/Interface/Alerts/Human/human.png
name: Health
description: "[color=green]Green[/color] good. [color=red]Red[/color] bad."
minSeverity: 0
maxSeverity: 6
- type: alert
alertType: PilotingShuttle
category: Piloting
icon: /Textures/Interface/Alerts/Buckle/buckled.png
name: Piloting Shuttle
description: You are piloting a shuttle. Click the alert to stop.
- type: alert
alertType: Overfed
category: Hunger
icon: /Textures/Interface/Alerts/Hunger/Overfed.png
name: "[color=yellow]Overfed[/color]"
description: You ate too much food, lardass. Run around the station and lose some weight.
- type: alert
alertType: Peckish
category: Hunger
icon: /Textures/Interface/Alerts/Hunger/Peckish.png
name: "[color=yellow]Peckish[/color]"
description: Some food would be good right about now.
- type: alert
alertType: Starving
category: Hunger
icon: /Textures/Interface/Alerts/Hunger/Starving.png
name: "[color=red]Starving[/color]"
description: You're severely malnourished. The hunger pains make moving around a chore.
- type: alert
alertType: Overhydrated
category: Thirst
icon: /Textures/Interface/Alerts/Thirst/OverHydrated.png
name: "[color=yellow]Overhydrated[/color]"
description: You drank too much.
- type: alert
alertType: Thirsty
category: Thirst
icon: /Textures/Interface/Alerts/Thirst/Thirsty.png
name: "[color=yellow]Thirsty[/color]"
description: Something to drink would be good right about now.
- type: alert
alertType: Parched
category: Thirst
icon: /Textures/Interface/Alerts/Thirst/Parched.png
name: "[color=red]Parched[/color]"
description: You're severely thirsty. The thirst makes moving around a chore.
- type: alert
alertType: Pulled
icon: /Textures/Interface/Alerts/Pull/pulled.png
name: Pulled
description: You're being pulled. Move to break free.
- type: alert
alertType: Pulling
icon: /Textures/Interface/Alerts/Pull/pulling.png
name: Pulling
description: You're pulling something. Click the alert to stop.
- type: alert
alertType: Debug1
icon: /Textures/Interface/Alerts/Human/human1.png
name: Debug
description: Debug
- type: alert
alertType: Debug2
icon: /Textures/Interface/Alerts/Human/human2.png
name: Debug
description: Debug
- type: alert
alertType: Debug3
icon: /Textures/Interface/Alerts/Human/human3.png
name: Debug
description: Debug
- type: alert
alertType: Debug4
icon: /Textures/Interface/Alerts/Human/human4.png
name: Debug
description: Debug
- type: alert
alertType: Debug5
icon: /Textures/Interface/Alerts/Human/human5.png
name: Debug
description: Debug
- type: alert
alertType: Debug6
icon: /Textures/Interface/Alerts/Human/human6.png
name: Debug
description: Debug

View File

@@ -10,7 +10,7 @@
show_examine_info: true show_examine_info: true
- type: Input - type: Input
context: "human" context: "human"
- type: StatusEffectsUI - type: AlertsUI
- type: OverlayEffectsUI - type: OverlayEffectsUI
- type: Eye - type: Eye
zoom: 0.5, 0.5 zoom: 0.5, 0.5

View File

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 520 B

View File

Before

Width:  |  Height:  |  Size: 414 B

After

Width:  |  Height:  |  Size: 414 B

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 233 B

View File

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

View File

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

View File

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

View File

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

View File

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

View File

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 214 B

View File

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 240 B

View File

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

View File

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

View File

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 207 B

View File

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 271 B

View File

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 271 B

View File

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 271 B

View File

Before

Width:  |  Height:  |  Size: 591 B

After

Width:  |  Height:  |  Size: 591 B

View File

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 578 B

View File

Before

Width:  |  Height:  |  Size: 601 B

After

Width:  |  Height:  |  Size: 601 B

View File

Before

Width:  |  Height:  |  Size: 506 B

After

Width:  |  Height:  |  Size: 506 B

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 525 B

After

Width:  |  Height:  |  Size: 525 B

View File

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 396 B

View File

Before

Width:  |  Height:  |  Size: 388 B

After

Width:  |  Height:  |  Size: 388 B

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 365 B

After

Width:  |  Height:  |  Size: 365 B

View File

Before

Width:  |  Height:  |  Size: 333 B

After

Width:  |  Height:  |  Size: 333 B

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 786 B

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 B

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B