CatStation/Content.Shared/AlertLevel/AlertLevelSystem.cs
slarticodefast 0b5bacf5c2
Refactor and predict Alert Levels (#41122)
* prediction

* fix

* reviews

* warnings

* cleanup

* grupdate

* bruh

---------

Co-authored-by: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
2026-07-27 05:11:33 +00:00

325 lines
12 KiB
C#

using System.Diagnostics.CodeAnalysis;
using Content.Shared.Chat;
using Content.Shared.Station;
using Content.Shared.CCVar;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.AlertLevel;
public sealed partial class AlertLevelSystem : EntitySystem
{
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private INetManager _net = default!;
[Dependency] private IPrototypeManager _prototype = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private SharedChatSystem _chat = default!;
[Dependency] private SharedStationSystem _station = default!;
private static string _fallbackAlertKey = "alert-level-unknown";
private static string _fallbackAlertName = $"{_fallbackAlertKey}";
private static string _fallbackAlertAnnouncement = $"{_fallbackAlertKey}";
private static string _fallbackAlertInstructions = $"{_fallbackAlertKey}";
public override void Update(float time)
{
var query = EntityQueryEnumerator<AlertLevelComponent>();
var curTime = _timing.CurTime;
while (query.MoveNext(out var station, out var alertComp))
{
if (alertComp.DelayedUntil <= curTime)
{
// Raise an event so that UIs can refresh.
var ev = new AlertLevelDelayFinishedEvent();
RaiseLocalEvent(ref ev);
alertComp.DelayedUntil = null;
Dirty(station, alertComp);
}
}
}
/// <summary>
/// Can the alert level currently be changed from the communications console?
/// This considers both the cooldown and if the current mode prevents changing it, for example when the nuke is armed.
/// </summary>
public bool CanChangeAlertLevel(Entity<AlertLevelComponent?> station)
{
if (!Resolve(station, ref station.Comp))
return false;
return station.Comp.DelayedUntil == null && !station.Comp.IsLevelLocked;
}
/// <summary>
/// Gets a list of alert levels that are available on this station and can be selected from the communications console.
/// </summary>
public List<ProtoId<AlertLevelPrototype>> GetSelectableAlertLevels(Entity<AlertLevelComponent?> station)
{
List<ProtoId<AlertLevelPrototype>> list = new();
if (!Resolve(station, ref station.Comp))
return list;
foreach (var levelId in station.Comp.AvailableAlertLevels)
{
if (!_prototype.Resolve(levelId, out var level))
continue;
if (level.Selectable)
list.Add(levelId);
}
return list;
}
/// <summary>
/// Gets the current alert level for this station entity.
/// </summary>
public bool TryGetLevel(Entity<AlertLevelComponent?> station, [NotNullWhen(true)] out ProtoId<AlertLevelPrototype>? level)
{
level = null;
if (!Resolve(station, ref station.Comp, false))
return false;
level = station.Comp.CurrentAlertLevel;
return true;
}
/// <summary>
/// Gets the default alert level for this station entity.
/// </summary>
public bool TryGetDefaultLevel(Entity<AlertLevelComponent?> station, [NotNullWhen(true)] out ProtoId<AlertLevelPrototype>? level)
{
level = null;
if (!Resolve(station, ref station.Comp, false))
return false;
level = station.Comp.DefaultAlertLevel;
return true;
}
/// <summary>
/// Gets the remaining cooldown for changing the alert level from the communications console.
/// </summary>
public TimeSpan GetAlertLevelDelay(Entity<AlertLevelComponent?> station)
{
if (!Resolve(station, ref station.Comp, false))
return TimeSpan.Zero;
if (station.Comp.DelayedUntil == null)
return TimeSpan.Zero;
return station.Comp.DelayedUntil.Value - _timing.CurTime;
}
/// <summary>
/// Set the alert level based on the station's entity uid.
/// </summary>
/// <param name="station">Station entity UID.</param>
/// <param name="level">Level to change the station's alert level to.</param>
/// <param name="playSound">Play the alert level's sound?</param>
/// <param name="announce">Announce the alert level change in chat?</param>
/// <param name="force">Force the alert change. This applies if the alert level is not selectable.</param>
/// <param name="locked">Override to decide if the crew will be able to change from this alert level via the console. Will use the value given in the prototype if null.</param>
public void SetLevel(
Entity<AlertLevelComponent?> station,
ProtoId<AlertLevelPrototype> level,
bool playSound = true,
bool announce = true,
bool force = false,
bool? locked = null)
{
if (!Resolve(station, ref station.Comp))
return;
if (station.Comp.CurrentAlertLevel == level)
return;
if (!_prototype.Resolve(level, out var prototype))
return;
if (!force)
{
if (!CanChangeAlertLevel(station) || !prototype.Selectable)
return;
station.Comp.DelayedUntil = _timing.CurTime + TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.GameAlertLevelChangeDelay));
}
station.Comp.CurrentAlertLevel = level;
station.Comp.IsLevelLocked = prototype.DisableSelection;
if (locked != null) // optional override
station.Comp.IsLevelLocked = locked.Value;
Dirty(station);
var stationName = MetaData(station.Owner).EntityName;
// The full announcement to be spat out into chat.
var announcementFull = Loc.GetString("alert-level-announcement",
("name", prototype.LocalizedName),
("announcement", AlertLevelAnnouncement(prototype)));
var ev = new AlertLevelChangedEvent(station, level);
RaiseLocalEvent(ref ev);
if (_net.IsClient)
return;
var playDefault = false;
if (playSound)
{
if (prototype.Sound != null)
{
var filter = _station.GetInOwningStation(station);
_audio.PlayGlobal(prototype.Sound, filter, true);
}
else
{
playDefault = true;
}
}
if (announce)
{
_chat.DispatchStationAnnouncement(
station,
announcementFull,
playDefaultSound: playDefault,
colorOverride: prototype.Color,
sender: stationName);
}
}
/// <summary>
/// Returns the name, announcement, and description of this station's alert level.
/// </summary>
public (string Name, string Announcement, string Instructions) AlertLevelData(Entity<AlertLevelComponent?> station)
{
if (!TryGetDefaultLevel(station, out var level))
{
return (Loc.GetString(_fallbackAlertName),
Loc.GetString(_fallbackAlertAnnouncement),
Loc.GetString(_fallbackAlertInstructions));
}
return AlertLevelData(level);
}
/// <summary>
/// Returns the name, announcement, and description of the given alert level.
/// </summary>
public (string Name, string Announcement, string Instructions) AlertLevelData(ProtoId<AlertLevelPrototype>? level)
{
if (!ProtoMan.Resolve(level, out var proto))
{
return (Loc.GetString(_fallbackAlertName),
Loc.GetString(_fallbackAlertAnnouncement),
Loc.GetString(_fallbackAlertInstructions));
}
return AlertLevelData(proto);
}
/// <summary>
/// Returns the name, announcement, and description of the given alert level.
/// </summary>
public (string Name, string Announcement, string Instructions) AlertLevelData(AlertLevelPrototype level)
{
return (level.LocalizedName, AlertLevelAnnouncement(level), AlertLevelInstructions(level));
}
/// <summary>
/// Returns the name of this station's current alert level.
/// </summary>
/// <returns>Returns the name of the current Alert Level.</returns>
public string AlertLevelName(Entity<AlertLevelComponent?> station)
{
return !TryGetDefaultLevel(station, out var level)
? Loc.GetString(_fallbackAlertName)
: AlertLevelName(level);
}
/// <summary>
/// Returns the name of the given alert level.
/// </summary>
/// <returns>Returns the name of the given Alert Level.</returns>
public string AlertLevelName(ProtoId<AlertLevelPrototype>? level)
{
return !ProtoMan.Resolve(level, out var proto)
? Loc.GetString(_fallbackAlertName)
: proto.LocalizedName;
}
/// <summary>
/// Gets the announcement message for the stations' current alert level.
/// </summary>
/// <returns>Returns the description of the current Alert Level.</returns>
public string AlertLevelAnnouncement(Entity<AlertLevelComponent?> station)
{
return !TryGetDefaultLevel(station, out var level)
? Loc.GetString(_fallbackAlertAnnouncement)
: AlertLevelAnnouncement(level);
}
/// <see cref="AlertLevelAnnouncement(AlertLevelPrototype)"/>
public string AlertLevelAnnouncement(ProtoId<AlertLevelPrototype>? level)
{
return !ProtoMan.Resolve(level, out var proto)
? Loc.GetString(_fallbackAlertAnnouncement)
: AlertLevelAnnouncement(proto);
}
/// <summary>
/// Gets the announcement message for the given alert level.
/// </summary>
/// <returns>Returns the announcement for the given Alert Level.</returns>
public string AlertLevelAnnouncement(AlertLevelPrototype level)
{
return Loc.GetString(level.Announcement ?? _fallbackAlertAnnouncement);
}
/// <summary>
/// Gets the instructions for the station's current alert level.
/// </summary>
/// <returns>Returns the instructions for the current alert level.</returns>
public string AlertLevelInstructions(Entity<AlertLevelComponent?> station)
{
return !TryGetDefaultLevel(station, out var level)
? Loc.GetString(_fallbackAlertInstructions)
: AlertLevelInstructions(level);
}
/// <see cref="AlertLevelInstructions(AlertLevelPrototype)"/>
public string AlertLevelInstructions(ProtoId<AlertLevelPrototype>? level)
{
return !ProtoMan.Resolve(level, out var proto)
? Loc.GetString(_fallbackAlertInstructions)
: AlertLevelInstructions(proto);
}
/// <summary>
/// Gets the instructions for the given alert level.
/// </summary>
/// <returns>Returns the instructions for the given Alert Level.</returns>
public string AlertLevelInstructions(AlertLevelPrototype level)
{
return Loc.GetString(level.Instructions ?? _fallbackAlertInstructions);
}
}
/// <summary>
/// Broadcast event that is raised when the alert level selection delay for a station is over and it can be switched again.
/// </summary>
[ByRefEvent]
public record struct AlertLevelDelayFinishedEvent;
/// <summary>
/// Broadcast event that is raised when the a station changes its alert level.
/// </summary>
[ByRefEvent]
public record struct AlertLevelChangedEvent(EntityUid Station, ProtoId<AlertLevelPrototype> AlertLevel);