using System.Globalization; using Content.Server.Access.Systems; using Content.Server.Administration.Logs; using Content.Server.AlertLevel; using Content.Server.Chat.Systems; using Content.Server.DeviceNetwork; using Content.Server.DeviceNetwork.Components; using Content.Server.DeviceNetwork.Systems; using Content.Server.Interaction; using Content.Server.Popups; using Content.Server.RoundEnd; using Content.Server.Screens; using Content.Server.Screens.Components; using Content.Server.Shuttles.Systems; using Content.Server.Station.Systems; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Communications; using Content.Shared.Database; using Content.Shared.DeviceNetwork; using Content.Shared.Emag.Components; using Content.Shared.Popups; using Robust.Server.GameObjects; using Robust.Shared.Configuration; namespace Content.Server.Communications { public sealed class CommunicationsConsoleSystem : EntitySystem { [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!; [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly AlertLevelSystem _alertLevelSystem = default!; [Dependency] private readonly ChatSystem _chatSystem = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private readonly EmergencyShuttleSystem _emergency = default!; [Dependency] private readonly IdCardSystem _idCardSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly RoundEndSystem _roundEndSystem = default!; [Dependency] private readonly StationSystem _stationSystem = default!; [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; private const float UIUpdateInterval = 5.0f; public override void Initialize() { // All events that refresh the BUI SubscribeLocalEvent(OnAlertLevelChanged); SubscribeLocalEvent((uid, comp, _) => UpdateCommsConsoleInterface(uid, comp)); SubscribeLocalEvent(_ => OnGenericBroadcastEvent()); SubscribeLocalEvent(_ => OnGenericBroadcastEvent()); // Messages from the BUI SubscribeLocalEvent(OnSelectAlertLevelMessage); SubscribeLocalEvent(OnAnnounceMessage); SubscribeLocalEvent(OnBroadcastMessage); SubscribeLocalEvent(OnCallShuttleMessage); SubscribeLocalEvent(OnRecallShuttleMessage); // On console init, set cooldown SubscribeLocalEvent(OnCommunicationsConsoleMapInit); } public override void Update(float frameTime) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { // TODO refresh the UI in a less horrible way if (comp.AnnouncementCooldownRemaining >= 0f) { comp.AnnouncementCooldownRemaining -= frameTime; } comp.UIUpdateAccumulator += frameTime; if (comp.UIUpdateAccumulator < UIUpdateInterval) continue; comp.UIUpdateAccumulator -= UIUpdateInterval; if (_uiSystem.TryGetUi(uid, CommunicationsConsoleUiKey.Key, out var ui) && ui.SubscribedSessions.Count > 0) UpdateCommsConsoleInterface(uid, comp, ui); } base.Update(frameTime); } public void OnCommunicationsConsoleMapInit(EntityUid uid, CommunicationsConsoleComponent comp, MapInitEvent args) { comp.AnnouncementCooldownRemaining = comp.InitialDelay; } /// /// Update the UI of every comms console. /// private void OnGenericBroadcastEvent() { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { UpdateCommsConsoleInterface(uid, comp); } } /// /// Updates all comms consoles belonging to the station that the alert level was set on /// /// Alert level changed event arguments private void OnAlertLevelChanged(AlertLevelChangedEvent args) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { var entStation = _stationSystem.GetOwningStation(uid); if (args.Station == entStation) UpdateCommsConsoleInterface(uid, comp); } } /// /// Updates the UI for all comms consoles. /// public void UpdateCommsConsoleInterface() { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { UpdateCommsConsoleInterface(uid, comp); } } /// /// Updates the UI for a particular comms console. /// public void UpdateCommsConsoleInterface(EntityUid uid, CommunicationsConsoleComponent comp, PlayerBoundUserInterface? ui = null) { if (ui == null && !_uiSystem.TryGetUi(uid, CommunicationsConsoleUiKey.Key, out ui)) return; var stationUid = _stationSystem.GetOwningStation(uid); List? levels = null; string currentLevel = default!; float currentDelay = 0; if (stationUid != null) { if (TryComp(stationUid.Value, out AlertLevelComponent? alertComp) && alertComp.AlertLevels != null) { if (alertComp.IsSelectable) { levels = new(); foreach (var (id, detail) in alertComp.AlertLevels.Levels) { if (detail.Selectable) { levels.Add(id); } } } currentLevel = alertComp.CurrentLevel; currentDelay = _alertLevelSystem.GetAlertLevelDelay(stationUid.Value, alertComp); } } _uiSystem.SetUiState(ui, new CommunicationsConsoleInterfaceState( CanAnnounce(comp), CanBroadcast(comp), CanCallOrRecall(comp), levels, currentLevel, currentDelay, _roundEndSystem.ExpectedCountdownEnd )); } private static bool CanAnnounce(CommunicationsConsoleComponent comp) { return comp.AnnouncementCooldownRemaining <= 0f; } private static bool CanBroadcast(CommunicationsConsoleComponent comp) { return comp.AnnouncementCooldownRemaining <= 0f; } private bool CanUse(EntityUid user, EntityUid console) { // This shouldn't technically be possible because of BUI but don't trust client. if (!_interaction.InRangeUnobstructed(console, user)) return false; if (TryComp(console, out var accessReaderComponent) && !HasComp(console)) { return _accessReaderSystem.IsAllowed(user, console, accessReaderComponent); } return true; } private bool CanCallOrRecall(CommunicationsConsoleComponent comp) { // Defer to what the round end system thinks we should be able to do. if (_emergency.EmergencyShuttleArrived || !_roundEndSystem.CanCallOrRecall()) return false; // Calling shuttle checks if (_roundEndSystem.ExpectedCountdownEnd is null) return comp.CanShuttle; // Recalling shuttle checks var recallThreshold = _cfg.GetCVar(CCVars.EmergencyRecallTurningPoint); // shouldn't really be happening if we got here if (_roundEndSystem.ShuttleTimeLeft is not { } left || _roundEndSystem.ExpectedShuttleLength is not { } expected) return false; return !(left.TotalSeconds / expected.TotalSeconds < recallThreshold); } private void OnSelectAlertLevelMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleSelectAlertLevelMessage message) { if (message.Session.AttachedEntity is not { Valid: true } mob) return; if (!CanUse(mob, uid)) { _popupSystem.PopupCursor(Loc.GetString("comms-console-permission-denied"), message.Session, PopupType.Medium); return; } var stationUid = _stationSystem.GetOwningStation(uid); if (stationUid != null) { _alertLevelSystem.SetLevel(stationUid.Value, message.Level, true, true); } } private void OnAnnounceMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleAnnounceMessage message) { var maxLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength); var msg = SharedChatSystem.SanitizeAnnouncement(message.Message, maxLength); var author = Loc.GetString("comms-console-announcement-unknown-sender"); if (message.Session.AttachedEntity is { Valid: true } mob) { if (!CanAnnounce(comp)) { return; } if (!CanUse(mob, uid)) { _popupSystem.PopupEntity(Loc.GetString("comms-console-permission-denied"), uid, message.Session); return; } if (_idCardSystem.TryFindIdCard(mob, out var id)) { author = $"{id.Comp.FullName} ({CultureInfo.CurrentCulture.TextInfo.ToTitleCase(id.Comp.JobTitle ?? string.Empty)})".Trim(); } } comp.AnnouncementCooldownRemaining = comp.Delay; UpdateCommsConsoleInterface(uid, comp); var ev = new CommunicationConsoleAnnouncementEvent(uid, comp, msg, message.Session.AttachedEntity); RaiseLocalEvent(ref ev); // allow admemes with vv Loc.TryGetString(comp.Title, out var title); title ??= comp.Title; msg += "\n" + Loc.GetString("comms-console-announcement-sent-by") + " " + author; if (comp.Global) { _chatSystem.DispatchGlobalAnnouncement(msg, title, announcementSound: comp.Sound, colorOverride: comp.Color); if (message.Session.AttachedEntity != null) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Session.AttachedEntity.Value):player} has sent the following global announcement: {msg}"); return; } _chatSystem.DispatchStationAnnouncement(uid, msg, title, colorOverride: comp.Color); if (message.Session.AttachedEntity != null) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Session.AttachedEntity.Value):player} has sent the following station announcement: {msg}"); } private void OnBroadcastMessage(EntityUid uid, CommunicationsConsoleComponent component, CommunicationsConsoleBroadcastMessage message) { if (!TryComp(uid, out var net)) return; var payload = new NetworkPayload { [ScreenMasks.Text] = message.Message }; _deviceNetworkSystem.QueuePacket(uid, null, payload, net.TransmitFrequency); if (message.Session.AttachedEntity != null) _adminLogger.Add(LogType.DeviceNetwork, LogImpact.Low, $"{ToPrettyString(message.Session.AttachedEntity.Value):player} has sent the following broadcast: {message.Message:msg}"); } private void OnCallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleCallEmergencyShuttleMessage message) { if (!CanCallOrRecall(comp)) return; if (message.Session.AttachedEntity is not { Valid: true } mob) return; if (!CanUse(mob, uid)) { _popupSystem.PopupEntity(Loc.GetString("comms-console-permission-denied"), uid, message.Session); return; } var ev = new CommunicationConsoleCallShuttleAttemptEvent(uid, comp, mob); RaiseLocalEvent(ref ev); if (ev.Cancelled) { _popupSystem.PopupEntity(ev.Reason ?? Loc.GetString("comms-console-shuttle-unavailable"), uid, message.Session); return; } _roundEndSystem.RequestRoundEnd(uid); _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(mob):player} has called the shuttle."); } private void OnRecallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleRecallEmergencyShuttleMessage message) { if (!CanCallOrRecall(comp)) return; if (message.Session.AttachedEntity is not { Valid: true } mob) return; if (!CanUse(mob, uid)) { _popupSystem.PopupEntity(Loc.GetString("comms-console-permission-denied"), uid, message.Session); return; } _roundEndSystem.CancelRoundEndCountdown(uid); _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(mob):player} has recalled the shuttle."); } } /// /// Raised on announcement /// [ByRefEvent] public record struct CommunicationConsoleAnnouncementEvent(EntityUid Uid, CommunicationsConsoleComponent Component, string Text, EntityUid? Sender) { public EntityUid Uid = Uid; public CommunicationsConsoleComponent Component = Component; public EntityUid? Sender = Sender; public string Text = Text; } /// /// Raised on shuttle call attempt. Can be cancelled /// [ByRefEvent] public record struct CommunicationConsoleCallShuttleAttemptEvent(EntityUid Uid, CommunicationsConsoleComponent Component, EntityUid? Sender) { public bool Cancelled = false; public EntityUid Uid = Uid; public CommunicationsConsoleComponent Component = Component; public EntityUid? Sender = Sender; public string? Reason; } }