CatStation/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.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

468 lines
17 KiB
C#

using System.Linq;
using Content.Server.DeviceNetwork.Systems;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.DeviceNetwork.Systems;
using Content.Shared.Power;
using Content.Shared.UserInterface;
using Content.Shared.SurveillanceCamera;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.SurveillanceCamera;
public sealed partial class SurveillanceCameraMonitorSystem : EntitySystem
{
[Dependency] private SurveillanceCameraSystem _surveillanceCameras = default!;
[Dependency] private UserInterfaceSystem _userInterface = default!;
[Dependency] private DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private DeviceNetworkRouterSystem _deviceNetworkRouter = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraDeactivateEvent>(OnSurveillanceCameraDeactivate);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, AfterActivatableUIOpenEvent>(OnToggleInterface);
Subs.BuiEvents<SurveillanceCameraMonitorComponent>(SurveillanceCameraMonitorUiKey.Key, subs =>
{
subs.Event<SurveillanceCameraRefreshCamerasMessage>(OnRefreshCamerasMessage);
subs.Event<SurveillanceCameraRefreshSubnetsMessage>(OnRefreshSubnetsMessage);
subs.Event<SurveillanceCameraDisconnectMessage>(OnDisconnectMessage);
subs.Event<SurveillanceCameraMonitorSubnetRequestMessage>(OnSubnetRequest);
subs.Event<SurveillanceCameraMonitorSwitchMessage>(OnSwitchMessage);
subs.Event<BoundUIClosedEvent>(OnBoundUiClose);
});
}
private const float MaxHeartbeatTime = 300f;
private const float HeartbeatDelay = 30f;
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveSurveillanceCameraMonitorComponent, SurveillanceCameraMonitorComponent>();
while (query.MoveNext(out var uid, out _, out var monitor))
{
monitor.LastHeartbeatSent += frameTime;
SendHeartbeat(uid, monitor);
monitor.LastHeartbeat += frameTime;
if (monitor.LastHeartbeat > MaxHeartbeatTime)
{
DisconnectCamera(uid, true, monitor);
RemComp<ActiveSurveillanceCameraMonitorComponent>(uid);
}
}
}
/// ROUTING:
///
/// Monitor freq: General frequency for cameras, routers, and monitors to speak on.
///
/// Subnet freqs: Frequency for each specific subnet. Routers ping cameras here,
/// cameras ping back on monitor frequency. When a monitor
/// selects a subnet, it saves that subnet's frequency
/// so it can connect to the camera. All outbound cameras
/// always speak on the monitor frequency and will not
/// do broadcast pings - whatever talks to it, talks to it.
///
/// How a camera is discovered:
///
/// Subnet ping:
/// Surveillance camera monitor - [ monitor freq ] -> Router
/// Router -> camera discovery
/// Router - [ subnet freq ] -> Camera
/// Camera -> router ping
/// Camera - [ monitor freq ] -> Router
/// Router -> monitor data forward
/// Router - [ monitor freq ] -> Monitor
#region Event Handling
private void OnComponentStartup(EntityUid uid, SurveillanceCameraMonitorComponent component, ComponentStartup args)
{
RefreshSubnets(uid, component);
}
private void OnSubnetRequest(EntityUid uid, SurveillanceCameraMonitorComponent component,
SurveillanceCameraMonitorSubnetRequestMessage args)
{
if (args.Actor is { Valid: true } actor && !Deleted(actor))
{
SetActiveSubnet(uid, args.Subnet, component);
}
}
[SubscribeLocalEvent]
private void OnCameraConnect(Entity<SurveillanceCameraMonitorComponent> ent, ref DeviceNetworkPacketEvent<SurveillanceCameraConnectPayload> args)
{
var payload = args.Data;
if (ent.Comp.NextCameraAddress == payload.SenderAddress)
{
if (payload.SenderAddress != null)
ent.Comp.ActiveCameraAddress = payload.SenderAddress;
TrySwitchCameraByUid(ent, payload.Sender, ent.Comp);
}
ent.Comp.NextCameraAddress = null;
}
[SubscribeLocalEvent]
private void OnCameraHeartbeat(Entity<SurveillanceCameraMonitorComponent> ent, ref DeviceNetworkPacketEvent<SurveillanceCameraHeartbeatPayload> args)
{
if (args.Data.SenderAddress == ent.Comp.ActiveCameraAddress)
{
ent.Comp.LastHeartbeat = 0;
}
}
[SubscribeLocalEvent]
private void OnCameraData(Entity<SurveillanceCameraMonitorComponent> ent, ref DeviceNetworkPacketEvent<SurveillanceCameraDataPayload> args)
{
var payload = args.Data;
var subnetData = payload.Subnet;
if (ent.Comp.ActiveSubnet != subnetData)
{
DisconnectFromSubnet(ent, subnetData);
}
if (payload.SenderAddress != null)
ent.Comp.KnownCameras.TryAdd(payload.SenderAddress, payload.Name);
UpdateUserInterface(ent, ent.Comp);
}
[SubscribeLocalEvent]
private void OnSubnetData(Entity<SurveillanceCameraMonitorComponent> ent, ref DeviceNetworkPacketEvent<SurveillanceCameraSubnetDataPayload> args)
{
ent.Comp.KnownSubnets.TryAdd(args.Data.TransmitFrequency, args.SenderAddress);
UpdateUserInterface(ent, ent.Comp);
}
private void OnDisconnectMessage(EntityUid uid, SurveillanceCameraMonitorComponent component,
SurveillanceCameraDisconnectMessage message)
{
DisconnectCamera(uid, true, component);
}
private void OnRefreshCamerasMessage(EntityUid uid, SurveillanceCameraMonitorComponent component,
SurveillanceCameraRefreshCamerasMessage message)
{
component.KnownCameras.Clear();
PingCameraNetwork(uid, component);
}
private void OnRefreshSubnetsMessage(EntityUid uid, SurveillanceCameraMonitorComponent component,
SurveillanceCameraRefreshSubnetsMessage message)
{
RefreshSubnets(uid, component);
}
private void OnSwitchMessage(EntityUid uid, SurveillanceCameraMonitorComponent component, SurveillanceCameraMonitorSwitchMessage message)
{
// there would be a null check here, but honestly
// whichever one is the "latest" switch message gets to
// do the switch
TrySwitchCameraByAddress(uid, message.Address, message.CameraSubnet, component);
}
private void OnPowerChanged(EntityUid uid, SurveillanceCameraMonitorComponent component, ref PowerChangedEvent args)
{
if (!args.Powered)
{
RemoveActiveCamera(uid, component);
component.NextCameraAddress = null;
component.ActiveSubnet = string.Empty;
}
}
private void OnShutdown(EntityUid uid, SurveillanceCameraMonitorComponent component, ComponentShutdown args)
{
RemoveActiveCamera(uid, component);
}
private void OnToggleInterface(EntityUid uid, SurveillanceCameraMonitorComponent component,
AfterActivatableUIOpenEvent args)
{
AfterOpenUserInterface(uid, args.User, component);
}
// This is to ensure that there's no delay in ensuring that a camera is deactivated.
private void OnSurveillanceCameraDeactivate(EntityUid uid, SurveillanceCameraMonitorComponent monitor, SurveillanceCameraDeactivateEvent args)
{
DisconnectCamera(uid, false, monitor);
}
private void OnBoundUiClose(EntityUid uid, SurveillanceCameraMonitorComponent component, BoundUIClosedEvent args)
{
RemoveViewer(uid, args.Actor, component);
}
#endregion
private void SendHeartbeat(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| monitor.LastHeartbeatSent < HeartbeatDelay
|| monitor.ActiveSubnet is not { } activeSubnet
|| !monitor.KnownSubnets.TryGetValue(activeSubnet, out var subnetAddress))
{
return;
}
var payload = new SurveillanceCameraHeartbeatPayload();
_deviceNetworkRouter.SendPacketRouted(uid, ref payload, subnetAddress, monitor.ActiveCameraAddress, ProtoMan.Index(activeSubnet).Frequency);
monitor.LastHeartbeatSent = 0;
}
private void DisconnectCamera(EntityUid uid, bool removeViewers, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
if (removeViewers)
{
RemoveActiveCamera(uid, monitor);
}
monitor.ActiveCamera = null;
monitor.ActiveCameraAddress = string.Empty;
RemComp<ActiveSurveillanceCameraMonitorComponent>(uid);
UpdateUserInterface(uid, monitor);
}
private void RefreshSubnets(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
monitor.KnownSubnets.Clear();
PingSubnets(uid, monitor);
}
private void PingCameraNetwork(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| monitor.ActiveSubnet == null
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet.Value, out var subnetData))
return;
var payload = new SurveillanceCameraPingPayload { Subnet = monitor.ActiveSubnet };
_deviceNetworkRouter.SendPacketRouted(uid, ref payload, null, null, ProtoMan.Index(monitor.ActiveSubnet.Value).Frequency);
}
private void SetActiveSubnet(EntityUid uid, ProtoId<DeviceFrequencyPrototype> subnet,
SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.ContainsKey(subnet))
{
return;
}
if (monitor.ActiveSubnet is { } previousSubnet)
DisconnectFromSubnet(uid, previousSubnet);
DisconnectCamera(uid, true, monitor);
monitor.ActiveSubnet = subnet;
monitor.KnownCameras.Clear();
UpdateUserInterface(uid, monitor);
ConnectToSubnet(uid, subnet);
}
private void PingSubnets(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
var payload = new SurveillanceCameraPingSubnetPayload();
_deviceNetworkSystem.SendPacket(uid, null, ref payload);
}
private void ConnectToSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| string.IsNullOrEmpty(subnet)
|| !monitor.KnownSubnets.TryGetValue(subnet, out var address))
{
return;
}
var payload = new SurveillanceCameraSubnetConnectPayload();
_deviceNetworkSystem.SendPacket(uid, address, ref payload);
PingSubnets(uid);
}
private void DisconnectFromSubnet(EntityUid uid, ProtoId<DeviceFrequencyPrototype> subnet, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.TryGetValue(subnet, out var address))
{
return;
}
var payload = new SurveillanceCameraSubnetDisconnectPayload();
_deviceNetworkSystem.SendPacket(uid, address, ref payload);
}
// Adds a viewer to the camera and the monitor.
private void AddViewer(EntityUid uid, EntityUid player, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
monitor.Viewers.Add(player);
if (monitor.ActiveCamera != null)
{
_surveillanceCameras.AddActiveViewer(monitor.ActiveCamera.Value, player, uid);
}
UpdateUserInterface(uid, monitor, player);
}
// Removes a viewer from the camera and the monitor.
private void RemoveViewer(EntityUid uid, EntityUid player, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
monitor.Viewers.Remove(player);
if (monitor.ActiveCamera != null)
{
_surveillanceCameras.RemoveActiveViewer(monitor.ActiveCamera.Value, player);
}
}
// Sets the camera. If the camera is not null, this will return.
//
// The camera should always attempt to switch over, rather than
// directly setting it, so that the active viewer list and view
// subscriptions can be updated.
private void SetCamera(EntityUid uid, EntityUid camera, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| monitor.ActiveCamera != null)
{
return;
}
_surveillanceCameras.AddActiveViewers(camera, monitor.Viewers, uid);
monitor.ActiveCamera = camera;
AddComp<ActiveSurveillanceCameraMonitorComponent>(uid);
UpdateUserInterface(uid, monitor);
}
// Switches the camera's viewers over to this new given camera.
private void SwitchCamera(EntityUid uid, EntityUid camera, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| monitor.ActiveCamera == null)
{
return;
}
_surveillanceCameras.SwitchActiveViewers(monitor.ActiveCamera.Value, camera, monitor.Viewers, uid);
monitor.ActiveCamera = camera;
UpdateUserInterface(uid, monitor);
}
private void TrySwitchCameraByAddress(EntityUid uid, string address, string? cameraSubnet = null, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
return;
if (cameraSubnet != null && cameraSubnet != monitor.ActiveSubnet)
SetActiveSubnet(uid, cameraSubnet, monitor);
if (monitor.ActiveSubnet is not { } activeSubnet
|| !monitor.KnownSubnets.TryGetValue(activeSubnet, out var subnetAddress))
return;
var payload = new SurveillanceCameraConnectRequestPayload();
monitor.NextCameraAddress = address;
_deviceNetworkRouter.SendPacketRouted(uid, ref payload, subnetAddress, address, ProtoMan.Index(activeSubnet).Frequency);
}
// Attempts to switch over the current viewed camera on this monitor
// to the new camera.
private void TrySwitchCameraByUid(EntityUid uid, EntityUid newCamera, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
if (monitor.ActiveCamera == null)
{
SetCamera(uid, newCamera, monitor);
}
else
{
SwitchCamera(uid, newCamera, monitor);
}
}
private void RemoveActiveCamera(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| monitor.ActiveCamera == null)
{
return;
}
_surveillanceCameras.RemoveActiveViewers(monitor.ActiveCamera.Value, monitor.Viewers, uid);
UpdateUserInterface(uid, monitor);
}
// This is public primarily because it might be useful to have the ability to
// have this component added to any entity, and have them open the BUI (somehow).
public void AfterOpenUserInterface(EntityUid uid, EntityUid player, SurveillanceCameraMonitorComponent? monitor = null, ActorComponent? actor = null)
{
if (!Resolve(uid, ref monitor)
|| !Resolve(player, ref actor))
{
return;
}
AddViewer(uid, player);
}
private void UpdateUserInterface(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null, EntityUid? player = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
var state = new SurveillanceCameraMonitorUiState(
GetNetEntity(monitor.ActiveCamera),
monitor.KnownSubnets.Keys.ToHashSet(),
monitor.ActiveCameraAddress,
monitor.ActiveSubnet,
monitor.KnownCameras);
_userInterface.SetUiState(uid, SurveillanceCameraMonitorUiKey.Key, state);
}
}