* 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>
498 lines
18 KiB
C#
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);
|
|
}
|
|
}
|