CatStation/Content.Server/Communications/CommunicationsConsoleSystem.cs
Rouden cc2c14f8e4
Device Network refactor part 1: API improvements (#44346)
* Refactor DeviceNetwork

* Singleton

* NetworkPayloads refactor

* Reorganize DeviceNetworkSystem

* Surveillance cameras

* Fix code

* Fix build

* Fix test fail by rewriting entire singleton logic

* Properly predict examine

* Update DeviceNetworkSystem.API.cs

* Update EntityTest.cs

* Update DeviceNetworkSystem.cs

* Alright I'll do this expensive check fine

* Fix disposals debug assert

* Implement device network static events support

* Use frozen dictionaries for performance

* Update SharedDeviceNetworkSystem.Payloads.cs

* Parallel payload handling support

* Fix some spagetti-code

* Optimize NetworkPayload cancellation

* Parallel payloads are now cancellable

* Fix screens

* Fix robotics console

* Allow new methods for shared

* Mail fix

* Turret fix

* Fix network system method usage + signal and borg fix

* finally fix atmos devices OH MY GOOOD

* Remove stupid parallelism from fax

* Fix surveiollance cameras

* Working on a single thing for 2 days straight award

* Fix the test?

* Update units.yml

* Add IDevice interface because why not

* Add more device network suffixes

* Fix camera router failing test

* yaml linter peak

* Improve docs for DeviceNetworkHandlers

* fix build

* Update CartridgeLoaderSystem.Relay.cs

* Remove parallel system and EntitySystem post init

* Add more docs and also events

* Remove unnecessary caching

* Update DevicePayloadComponentSystem.cs

* Back to entity events

* Implement DeviceData

* Remove serializable from payloads

* Update DeviceData.cs

* Add a prototype for a singleton

* Fix tests

* Fix air alarms

* Update DeviceNetworkSystem.cs

* Fix cameras

* Make camera router event-less

* Fix atmos sync payloads, add more docs

* Remove singleton + instant packets

* oops remove public access

* Fix tests?

* Why the hell overload effect makes puddle sparks

* Apparently it was redundant

* Update gates.yml

* Finish documentation

* Review

* Mark relay properties as obsolete

* Struct payloads support (#blazinglyfastmemorysafe)

* Update DeviceNetworkTest.cs

* Make device structs stuff better

* Move address prefix to DeviceNetworkComponent

* Locale fixes

* Fix tests

* Doesn't know boolean logic award

* Update AtmosAlarmableSystem.cs

* Fix address predicted examine

* Partial fix to air alarmables

* Fix deployable turrets UI

* Fix firelocks, maybe

* Apparently Activatable UI was enough lol

* Fix surveillance cameras

* Rename `QueuePacket` into `SendPacket`

* Simplify DeviceData as much as possible

* Update DeviceData.cs

* Update FaxMachineComponent.cs

* Remove DeviceData because its stupid

* Update ScreenSystem.cs

* Fix build (camera map broken still)

* Fix map

* fix usings

* Review changes

* Build fix
2026-08-31 19:42:56 +00:00

331 lines
13 KiB
C#

using Content.Server.Administration.Logs;
using Content.Server.Chat.Systems;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Popups;
using Content.Server.RoundEnd;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.AlertLevel;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Communications;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Popups;
using Content.Shared.Screens;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
namespace Content.Server.Communications
{
public sealed partial class CommunicationsConsoleSystem : EntitySystem
{
[Dependency] private AccessReaderSystem _accessReaderSystem = default!;
[Dependency] private AlertLevelSystem _alertLevelSystem = default!;
[Dependency] private ChatSystem _chatSystem = default!;
[Dependency] private DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private EmergencyShuttleSystem _emergency = default!;
[Dependency] private PopupSystem _popupSystem = default!;
[Dependency] private RoundEndSystem _roundEndSystem = default!;
[Dependency] private StationSystem _stationSystem = default!;
[Dependency] private UserInterfaceSystem _uiSystem = default!;
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private IAdminLogManager _adminLogger = default!;
[Dependency] private IdentitySystem _identity = default!;
private const float UIUpdateInterval = 5.0f;
public override void Initialize()
{
// All events that refresh the BUI
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
SubscribeLocalEvent<RoundEndSystemChangedEvent>(_ => OnGenericBroadcastEvent());
SubscribeLocalEvent<AlertLevelDelayFinishedEvent>((ref AlertLevelDelayFinishedEvent ev) => OnGenericBroadcastEvent());
// Messages from the BUI
SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleSelectAlertLevelMessage>(OnSelectAlertLevelMessage);
SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleAnnounceMessage>(OnAnnounceMessage);
SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleBroadcastMessage>(OnBroadcastMessage);
SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleCallEmergencyShuttleMessage>(OnCallShuttleMessage);
SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleRecallEmergencyShuttleMessage>(OnRecallShuttleMessage);
// On console init, set cooldown
SubscribeLocalEvent<CommunicationsConsoleComponent, MapInitEvent>(OnCommunicationsConsoleMapInit);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<CommunicationsConsoleComponent>();
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.IsUiOpen(uid, CommunicationsConsoleUiKey.Key))
UpdateCommsConsoleInterface(uid, comp);
}
base.Update(frameTime);
}
public void OnCommunicationsConsoleMapInit(EntityUid uid, CommunicationsConsoleComponent comp, MapInitEvent args)
{
comp.AnnouncementCooldownRemaining = comp.InitialDelay;
UpdateCommsConsoleInterface(uid, comp);
}
/// <summary>
/// Update the UI of every comms console.
/// </summary>
private void OnGenericBroadcastEvent()
{
var query = EntityQueryEnumerator<CommunicationsConsoleComponent>();
while (query.MoveNext(out var uid, out var comp))
{
UpdateCommsConsoleInterface(uid, comp);
}
}
/// <summary>
/// Updates all comms consoles belonging to the station that the alert level was set on
/// </summary>
/// <param name="args">Alert level changed event arguments</param>
private void OnAlertLevelChanged(ref AlertLevelChangedEvent args)
{
var query = EntityQueryEnumerator<CommunicationsConsoleComponent>();
while (query.MoveNext(out var uid, out var comp))
{
var entStation = _stationSystem.GetOwningStation(uid);
if (args.Station == entStation)
UpdateCommsConsoleInterface(uid, comp);
}
}
/// <summary>
/// Updates the UI for all comms consoles.
/// </summary>
public void UpdateCommsConsoleInterface()
{
var query = EntityQueryEnumerator<CommunicationsConsoleComponent>();
while (query.MoveNext(out var uid, out var comp))
{
UpdateCommsConsoleInterface(uid, comp);
}
}
/// <summary>
/// Updates the UI for a particular comms console.
/// </summary>
public void UpdateCommsConsoleInterface(EntityUid uid, CommunicationsConsoleComponent comp)
{
// TODO: Use component states and predict the UI
_uiSystem.SetUiState(uid, CommunicationsConsoleUiKey.Key, new CommunicationsConsoleInterfaceState(
CanAnnounce(comp),
CanCallOrRecall(comp),
_roundEndSystem.ExpectedCountdownEnd
));
}
private static bool CanAnnounce(CommunicationsConsoleComponent comp)
{
return comp.AnnouncementCooldownRemaining <= 0f;
}
private bool CanUse(EntityUid user, EntityUid console)
{
if (TryComp<AccessReaderComponent>(console, out var accessReaderComponent))
{
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;
// Ensure that we can communicate with the shuttle (either call or recall)
if (!comp.CanShuttle)
return false;
// Calling shuttle checks
if (_roundEndSystem.ExpectedCountdownEnd is null)
return true;
// 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.Actor is not { Valid: true } mob)
return;
if (!CanUse(mob, uid))
{
_popupSystem.PopupCursor(Loc.GetString("comms-console-permission-denied"), message.Actor, PopupType.Medium);
return;
}
var stationUid = _stationSystem.GetOwningStation(uid);
if (stationUid != null)
{
_alertLevelSystem.SetLevel(stationUid.Value, message.Level);
}
}
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.Actor is { Valid: true } mob)
{
if (!CanAnnounce(comp))
{
return;
}
if (!CanUse(mob, uid))
{
_popupSystem.PopupEntity(Loc.GetString("comms-console-permission-denied"), uid, message.Actor);
return;
}
author = _identity.GetIdentityShortInfo(mob, uid) ?? author;
}
comp.AnnouncementCooldownRemaining = comp.Delay;
UpdateCommsConsoleInterface(uid, comp);
var ev = new CommunicationConsoleAnnouncementEvent(uid, comp, msg, message.Actor);
RaiseLocalEvent(ref ev);
// allow admemes with vv
Loc.TryGetString(comp.Title, out var title);
title ??= comp.Title;
if (comp.AnnounceSentBy)
msg += "\n" + Loc.GetString("comms-console-announcement-sent-by") + " " + author;
if (comp.Global)
{
_chatSystem.DispatchGlobalAnnouncement(msg, title, announcementSound: comp.Sound, colorOverride: comp.Color);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following global announcement: {msg}");
return;
}
_chatSystem.DispatchStationAnnouncement(uid, msg, title, colorOverride: comp.Color);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following station announcement: {msg}");
}
private void OnBroadcastMessage(EntityUid uid, CommunicationsConsoleComponent component, CommunicationsConsoleBroadcastMessage message)
{
if (!TryComp<DeviceNetworkComponent>(uid, out var net))
return;
var payload = new ScreenTextPayload
{
Text = message.Message,
};
_deviceNetworkSystem.SendPacket(uid, null, ref payload, net.TransmitFrequency);
_adminLogger.Add(LogType.DeviceNetwork, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following broadcast: {message.Message:msg}");
}
private void OnCallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleCallEmergencyShuttleMessage message)
{
if (!CanCallOrRecall(comp))
return;
var mob = message.Actor;
if (!CanUse(mob, uid))
{
_popupSystem.PopupEntity(Loc.GetString("comms-console-permission-denied"), uid, message.Actor);
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.Actor);
return;
}
_roundEndSystem.RequestRoundEnd(mob, uid);
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(mob):player} has called the shuttle.");
}
private void OnRecallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleRecallEmergencyShuttleMessage message)
{
if (!CanCallOrRecall(comp))
return;
var mob = message.Actor;
if (!CanUse(mob, uid))
{
_popupSystem.PopupEntity(Loc.GetString("comms-console-permission-denied"), uid, message.Actor);
return;
}
_roundEndSystem.CancelRoundEndCountdown(mob, uid);
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(message.Actor):player} has recalled the shuttle.");
}
}
/// <summary>
/// Raised on announcement
/// </summary>
[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;
}
/// <summary>
/// Raised on shuttle call attempt. Can be cancelled
/// </summary>
[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;
}
}