Files
tbd-station-14/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs
Flipp Syder d4697c000c Surveillance cameras (#8246)
* cameras but i didn't feel like git stashing them

* Adds more functionality server-side for surveillance cameras

* rider moment

* Adds skeleton for SurveillanceCameraMonitorBoundUi on client

* whoops

* makes surveillance camera monitor UI more defined

* removes tree from SurveillanceCameraMonitorWindow

* surveillance camera active flag, other routing things

* actually sets how SurveillanceCameraMonitorSystem sends camera info to clients

* adds entity yaml, changes field

* adds the camera/monitor entities, makes the UI open

* SurveillanceCameraRouters (not implemented fully)

* subnets for cameras, server-side

* it works!

* fixes rotation in cameras

* whoops

restores surveillance cameras to ignored components
makes it so that switching cameras now lerps the other camera

* makes the UI work

* makes it so that cameras actually refresh now

* cleanup

* adds camera.rsi

* cleans up prototypes a bit

* adds camera subnet frequencies, cameras in subnets

* adds surveillance camera router subnets

* might fix testing errors

* adds the circuit board to the surveillance camera monitor

* fixes up the camera monitor (the detective will get his tv soon)

* adds heartbeat, ensures subnet data is passed into cameras to send

* fixes up a few things

* whoops

* changes to UI internals

* fixes subnet selection issue

* localized strings for UI

* changes 'test' id to 'camera' for cameras

* whoops

* missing s

* camera static!

* adds a delay to camera switching

* adjusts a few things in camera timing

* adds setup for cameras/routers, localization for frequency names

* adds setup ui for routers, makes subnet names in monitor window follow frequency name in prototype

* localization, some cleanup

* ui adjustments

* adds surveillance camera visuals

* fixes a bug when closing the UI for monitors

* adds disconnect message to UI

* adds construction graph to cameras

* adds the camera to the construction menu

* fixes network selection for setup, tweak to assembly

* adds surveillance camera router construction, fixes up surveillance camera wire cutting

* adds disconnect button to monitor UI

* switches around the status text

* tweaks monitor UI

* makes the address actually show

* might make tests pass

* UI adjustments, hard name limit

* ok, that didn't work

* adds wireless cameras

* makes the television work/look nicer

* adds tripod cameras in addition to mobile cameras

* makes wireless cameras constructable

* fixes up those prototypes

* reorganization in C#, small cleanup

* ensures that power changes deactivate some devices

* adds a component to the television, comments out a function

* actually, never mind, i forgot that wireless cameras existed/are creatable for a second

* tweaks to router construction, removes SubnetTest from prototypes

* removes it from frequencies too

* Apply suggestions from code review

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* type serializers into components

* setup window opens centered, enum is now byte

* replaces active monitor list with ActiveSurveillanceCameraMonitorComponent

* adds paused/deleted entity checks, changes how verbs are given

* removes EntitySystem.Get<T>() references

* fixes bug related to selecting network from setup, alphabet-orders network listing in setup

* rider moment

* adds minwidth to surveillance camera setup window

* addresses reviews

* should fix the issue with camera visuals not updating properly

* addresses some reviews

* addresses further review

* addresses reviews related to RSIs

* never needed a key there anyways

* changes a few things with routers to ensure that they're active

* whoops

* ensurecomp over addcomp

* whoops

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2022-05-31 18:44:57 +10:00

498 lines
18 KiB
C#

using System.Linq;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Server.Wires;
using Content.Shared.Interaction;
using Content.Shared.SurveillanceCamera;
using Robust.Server.GameObjects;
using Robust.Server.Player;
namespace Content.Server.SurveillanceCamera;
public sealed class SurveillanceCameraMonitorSystem : EntitySystem
{
[Dependency] private readonly SurveillanceCameraSystem _surveillanceCameras = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraDeactivateEvent>(OnSurveillanceCameraDeactivate);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, BoundUIClosedEvent>(OnBoundUiClose);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraMonitorSwitchMessage>(OnSwitchMessage);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraMonitorSubnetRequestMessage>(OnSubnetRequest);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, AfterActivatableUIOpenEvent>(OnToggleInterface);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraRefreshCamerasMessage>(OnRefreshCamerasMessage);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraRefreshSubnetsMessage>(OnRefreshSubnetsMessage);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraDisconnectMessage>(OnDisconnectMessage);
}
private const float _maxHeartbeatTime = 300f;
private const float _heartbeatDelay = 30f;
public override void Update(float frameTime)
{
foreach (var (_, monitor) in EntityQuery<ActiveSurveillanceCameraMonitorComponent, SurveillanceCameraMonitorComponent>())
{
if (Paused(monitor.Owner))
{
continue;
}
monitor.LastHeartbeatSent += frameTime;
SendHeartbeat(monitor.Owner, monitor);
monitor.LastHeartbeat += frameTime;
if (monitor.LastHeartbeat > _maxHeartbeatTime)
{
DisconnectCamera(monitor.Owner, true, monitor);
EntityManager.RemoveComponent<ActiveSurveillanceCameraMonitorComponent>(monitor.Owner);
}
}
}
/// 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.Session.AttachedEntity != null)
{
SetActiveSubnet(uid, args.Subnet, component);
}
}
private void OnPacketReceived(EntityUid uid, SurveillanceCameraMonitorComponent component,
DeviceNetworkPacketEvent args)
{
if (string.IsNullOrEmpty(args.SenderAddress))
{
return;
}
if (args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
{
switch (command)
{
case SurveillanceCameraSystem.CameraConnectMessage:
if (component.NextCameraAddress == args.SenderAddress)
{
component.ActiveCameraAddress = args.SenderAddress;
TrySwitchCameraByUid(uid, args.Sender, component);
}
component.NextCameraAddress = null;
break;
case SurveillanceCameraSystem.CameraHeartbeatMessage:
if (args.SenderAddress == component.ActiveCameraAddress)
{
component.LastHeartbeat = 0;
}
break;
case SurveillanceCameraSystem.CameraDataMessage:
if (!args.Data.TryGetValue(SurveillanceCameraSystem.CameraNameData, out string? name)
|| !args.Data.TryGetValue(SurveillanceCameraSystem.CameraSubnetData, out string? subnetData)
|| !args.Data.TryGetValue(SurveillanceCameraSystem.CameraAddressData, out string? address))
{
return;
}
if (component.ActiveSubnet != subnetData)
{
DisconnectFromSubnet(uid, subnetData);
}
if (!component.KnownCameras.ContainsKey(address))
{
component.KnownCameras.Add(address, name);
}
UpdateUserInterface(uid, component);
break;
case SurveillanceCameraSystem.CameraSubnetData:
if (args.Data.TryGetValue(SurveillanceCameraSystem.CameraSubnetData, out string? subnet)
&& !component.KnownSubnets.ContainsKey(subnet))
{
component.KnownSubnets.Add(subnet, args.SenderAddress);
}
UpdateUserInterface(uid, component);
break;
}
}
}
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();
RequestActiveSubnetInfo(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, component);
}
private void OnPowerChanged(EntityUid uid, SurveillanceCameraMonitorComponent component, PowerChangedEvent args)
{
if (!args.Powered)
{
RemoveActiveCamera(uid, component);
component.NextCameraAddress = null;
component.ActiveSubnet = string.Empty;
}
}
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)
{
if (args.Session.AttachedEntity == null)
{
return;
}
RemoveViewer(uid, args.Session.AttachedEntity.Value, component);
}
#endregion
private void SendHeartbeat(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| monitor.LastHeartbeatSent < _heartbeatDelay
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress))
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraHeartbeatMessage },
{ SurveillanceCameraSystem.CameraAddressData, monitor.ActiveCameraAddress }
};
_deviceNetworkSystem.QueuePacket(uid, subnetAddress, payload);
}
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;
EntityManager.RemoveComponent<ActiveSurveillanceCameraMonitorComponent>(uid);
UpdateUserInterface(uid, monitor);
}
private void RefreshSubnets(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
monitor.KnownSubnets.Clear();
PingCameraNetwork(uid, monitor);
}
private void PingCameraNetwork(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraPingMessage }
};
_deviceNetworkSystem.QueuePacket(uid, null, payload);
}
private void SetActiveSubnet(EntityUid uid, string subnet,
SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.ContainsKey(subnet))
{
return;
}
DisconnectFromSubnet(uid, monitor.ActiveSubnet);
DisconnectCamera(uid, true, monitor);
monitor.ActiveSubnet = subnet;
monitor.KnownCameras.Clear();
UpdateUserInterface(uid, monitor);
ConnectToSubnet(uid, subnet);
}
private void RequestActiveSubnetInfo(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var address))
{
return;
}
var payload = new NetworkPayload()
{
{DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraPingSubnetMessage},
};
_deviceNetworkSystem.QueuePacket(uid, address, payload);
}
private void ConnectToSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.TryGetValue(subnet, out var address))
{
return;
}
var payload = new NetworkPayload()
{
{DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraSubnetConnectMessage},
};
_deviceNetworkSystem.QueuePacket(uid, address, payload);
RequestActiveSubnetInfo(uid);
}
private void DisconnectFromSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.TryGetValue(subnet, out var address))
{
return;
}
var payload = new NetworkPayload()
{
{DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraSubnetDisconnectMessage},
};
_deviceNetworkSystem.QueuePacket(uid, address, 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)
{
EntityUid? monitorUid = monitor.Viewers.Count == 0 ? uid : null;
_surveillanceCameras.RemoveActiveViewer(monitor.ActiveCamera.Value, player, monitorUid);
}
}
// 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,
SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress))
{
return;
}
var payload = new NetworkPayload()
{
{DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraConnectMessage},
{SurveillanceCameraSystem.CameraAddressData, address}
};
monitor.NextCameraAddress = address;
_deviceNetworkSystem.QueuePacket(uid, subnetAddress, payload);
}
// 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;
}
IPlayerSession? session = null;
if (player != null
&& TryComp(player, out ActorComponent? actor))
{
session = actor.PlayerSession;
}
var state = new SurveillanceCameraMonitorUiState(monitor.ActiveCamera, monitor.KnownSubnets.Keys.ToHashSet(), monitor.ActiveCameraAddress, monitor.ActiveSubnet, monitor.KnownCameras);
_userInterface.TrySetUiState(uid, SurveillanceCameraMonitorUiKey.Key, state);
}
}