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>
This commit is contained in:
Flipp Syder
2022-05-31 01:44:57 -07:00
committed by GitHub
parent 69d8a2beee
commit d4697c000c
59 changed files with 2945 additions and 27 deletions

View File

@@ -24,7 +24,8 @@ namespace Content.Server.Entry
"ToggleableLightVisuals",
"CableVisualizer",
"PotencyVisuals",
"PaperVisuals"
"PaperVisuals",
"SurveillanceCameraVisuals"
};
}
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.SurveillanceCamera;
// Dummy component for active surveillance monitors.
[RegisterComponent]
public sealed class ActiveSurveillanceCameraMonitorComponent : Component
{
}

View File

@@ -0,0 +1,46 @@
using Content.Shared.DeviceNetwork;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.SurveillanceCamera;
[RegisterComponent]
[Friend(typeof(SurveillanceCameraSystem))]
public sealed class SurveillanceCameraComponent : Component
{
// List of active viewers. This is for bookkeeping purposes,
// so that when a camera shuts down, any entity viewing it
// will immediately have their subscription revoked.
public HashSet<EntityUid> ActiveViewers { get; } = new();
// Monitors != Viewers, as viewers are entities that are tied
// to a player session that's viewing from this camera
//
// Monitors are grouped sets of viewers, and may be
// completely different monitor types (e.g., monitor console,
// AI, etc.)
public HashSet<EntityUid> ActiveMonitors { get; } = new();
// If this camera is active or not. Deactivating a camera
// will not allow it to obtain any new viewers.
[ViewVariables]
public bool Active { get; set; } = true;
// This one isn't easy to deal with. Will require a UI
// to change/set this so mapping these in isn't
// the most terrible thing possible.
[ViewVariables(VVAccess.ReadWrite)]
[DataField("id")]
public string CameraId { get; set; } = "camera";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("nameSet")]
public bool NameSet { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
[DataField("networkSet")]
public bool NetworkSet { get; set; }
// This has to be device network frequency prototypes.
[DataField("setupAvailableNetworks", customTypeSerializer:typeof(PrototypeIdListSerializer<DeviceFrequencyPrototype>))]
public List<string> AvailableNetworks { get; } = new();
}

View File

@@ -0,0 +1,35 @@
namespace Content.Server.SurveillanceCamera;
[RegisterComponent]
[Friend(typeof(SurveillanceCameraMonitorSystem))]
public sealed class SurveillanceCameraMonitorComponent : Component
{
// Currently active camera viewed by this monitor.
public EntityUid? ActiveCamera { get; set; }
public string ActiveCameraAddress { get; set; } = string.Empty;
// Last time this monitor was sent a heartbeat.
public float LastHeartbeat { get; set; }
// Last time this monitor sent a heartbeat.
public float LastHeartbeatSent { get; set; }
// Next camera this monitor is trying to connect to.
// If the monitor has connected to the camera, this
// should be set to null.
public string? NextCameraAddress { get; set; }
[ViewVariables]
// Set of viewers currently looking at this monitor.
public HashSet<EntityUid> Viewers { get; } = new();
// Current active subnet.
public string ActiveSubnet { get; set; } = default!;
// Known cameras in this subnet by address with name values.
// This is cleared when the subnet is changed.
public Dictionary<string, string> KnownCameras { get; } = new();
[ViewVariables]
// The subnets known by this camera monitor.
public Dictionary<string, string> KnownSubnets { get; } = new();
}

View File

@@ -0,0 +1,31 @@
using Content.Shared.DeviceNetwork;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.SurveillanceCamera;
[RegisterComponent]
public sealed class SurveillanceCameraRouterComponent : Component
{
[ViewVariables] public bool Active { get; set; }
// The name of the subnet connected to this router.
[DataField("subnetName")]
public string SubnetName { get; set; } = string.Empty;
[ViewVariables]
// The monitors to route to. This raises an issue related to
// camera monitors disappearing before sending a D/C packet,
// this could probably be refreshed every time a new monitor
// is added or removed from active routing.
public HashSet<string> MonitorRoutes { get; } = new();
[ViewVariables]
// The frequency that talks to this router's subnet.
public uint SubnetFrequency;
[DataField("subnetFrequency", customTypeSerializer:typeof(PrototypeIdSerializer<DeviceFrequencyPrototype>))]
public string? SubnetFrequencyId { get; set; }
[DataField("setupAvailableNetworks", customTypeSerializer:typeof(PrototypeIdListSerializer<DeviceFrequencyPrototype>))]
public List<string> AvailableNetworks { get; } = new();
}

View File

@@ -0,0 +1,497 @@
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);
}
}

View File

@@ -0,0 +1,268 @@
using Content.Server.Administration.Managers;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Ghost.Components;
using Content.Server.Power.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.DeviceNetwork;
using Content.Shared.SurveillanceCamera;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Server.SurveillanceCamera;
public sealed class SurveillanceCameraRouterSystem : EntitySystem
{
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
public override void Initialize()
{
SubscribeLocalEvent<SurveillanceCameraRouterComponent, ComponentInit>(OnInitialize);
SubscribeLocalEvent<SurveillanceCameraRouterComponent, DeviceNetworkPacketEvent>(OnPacketReceive);
SubscribeLocalEvent<SurveillanceCameraRouterComponent, SurveillanceCameraSetupSetNetwork>(OnSetNetwork);
SubscribeLocalEvent<SurveillanceCameraRouterComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
SubscribeLocalEvent<SurveillanceCameraRouterComponent, PowerChangedEvent>(OnPowerChanged);
}
private void OnInitialize(EntityUid uid, SurveillanceCameraRouterComponent router, ComponentInit args)
{
if (router.SubnetFrequencyId == null ||
!_prototypeManager.TryIndex(router.SubnetFrequencyId, out DeviceFrequencyPrototype? subnetFrequency))
{
return;
}
router.SubnetFrequency = subnetFrequency.Frequency;
router.Active = true;
}
private void OnPacketReceive(EntityUid uid, SurveillanceCameraRouterComponent router, DeviceNetworkPacketEvent args)
{
if (!router.Active
|| string.IsNullOrEmpty(args.SenderAddress)
|| !args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
{
return;
}
switch (command)
{
case SurveillanceCameraSystem.CameraConnectMessage:
if (!args.Data.TryGetValue(SurveillanceCameraSystem.CameraAddressData, out string? address))
{
return;
}
ConnectCamera(uid, args.SenderAddress, address, router);
break;
case SurveillanceCameraSystem.CameraHeartbeatMessage:
if (!args.Data.TryGetValue(SurveillanceCameraSystem.CameraAddressData, out string? camera))
{
return;
}
SendHeartbeat(uid, args.SenderAddress, camera, router);
break;
case SurveillanceCameraSystem.CameraSubnetConnectMessage:
AddMonitorToRoute(uid, args.SenderAddress, router);
PingSubnet(uid, router);
break;
case SurveillanceCameraSystem.CameraSubnetDisconnectMessage:
RemoveMonitorFromRoute(uid, args.SenderAddress, router);
break;
case SurveillanceCameraSystem.CameraPingSubnetMessage:
PingSubnet(uid, router);
break;
case SurveillanceCameraSystem.CameraPingMessage:
SubnetPingResponse(uid, args.SenderAddress, router);
break;
case SurveillanceCameraSystem.CameraDataMessage:
SendCameraInfo(uid, args.Data, router);
break;
}
}
private void OnPowerChanged(EntityUid uid, SurveillanceCameraRouterComponent component, PowerChangedEvent args)
{
component.MonitorRoutes.Clear();
component.Active = args.Powered;
}
private void AddVerbs(EntityUid uid, SurveillanceCameraRouterComponent component, GetVerbsEvent<AlternativeVerb> verbs)
{
if (!_actionBlocker.CanInteract(verbs.User, uid))
{
return;
}
if (component.SubnetFrequencyId != null)
{
return;
}
AlternativeVerb verb = new();
verb.Text = Loc.GetString("surveillance-camera-setup");
verb.Act = () => OpenSetupInterface(uid, verbs.User, component);
verbs.Verbs.Add(verb);
}
private void OnSetNetwork(EntityUid uid, SurveillanceCameraRouterComponent component,
SurveillanceCameraSetupSetNetwork args)
{
if (args.UiKey is not SurveillanceCameraSetupUiKey key
|| key != SurveillanceCameraSetupUiKey.Router)
{
return;
}
if (args.Network < 0 || args.Network >= component.AvailableNetworks.Count)
{
return;
}
if (!_prototypeManager.TryIndex<DeviceFrequencyPrototype>(component.AvailableNetworks[args.Network],
out var frequency))
{
return;
}
component.SubnetFrequencyId = component.AvailableNetworks[args.Network];
component.SubnetFrequency = frequency.Frequency;
component.Active = true;
UpdateSetupInterface(uid, component);
}
private void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraRouterComponent? camera = null, ActorComponent? actor = null)
{
if (!Resolve(uid, ref camera)
|| !Resolve(player, ref actor))
{
return;
}
_userInterface.GetUiOrNull(uid, SurveillanceCameraSetupUiKey.Router)!.Open(actor.PlayerSession);
UpdateSetupInterface(uid, camera);
}
private void UpdateSetupInterface(EntityUid uid, SurveillanceCameraRouterComponent? router = null, DeviceNetworkComponent? deviceNet = null)
{
if (!Resolve(uid, ref router, ref deviceNet))
{
return;
}
if (router.AvailableNetworks.Count == 0 || router.SubnetFrequencyId != null)
{
_userInterface.TryCloseAll(uid, SurveillanceCameraSetupUiKey.Router);
return;
}
var state = new SurveillanceCameraSetupBoundUiState(router.SubnetName, deviceNet.ReceiveFrequency ?? 0,
router.AvailableNetworks, true, router.SubnetFrequencyId != null);
_userInterface.TrySetUiState(uid, SurveillanceCameraSetupUiKey.Router, state);
}
private void SendHeartbeat(EntityUid uid, string origin, string destination,
SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraHeartbeatMessage },
{ SurveillanceCameraSystem.CameraAddressData, origin }
};
_deviceNetworkSystem.QueuePacket(uid, destination, payload, router.SubnetFrequency);
}
private void SubnetPingResponse(EntityUid uid, string origin, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router) || router.SubnetFrequencyId == null)
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraSubnetData },
{ SurveillanceCameraSystem.CameraSubnetData, router.SubnetFrequencyId }
};
_deviceNetworkSystem.QueuePacket(uid, origin, payload);
}
private void ConnectCamera(EntityUid uid, string origin, string address, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraConnectMessage },
{ SurveillanceCameraSystem.CameraAddressData, origin }
};
_deviceNetworkSystem.QueuePacket(uid, address, payload, router.SubnetFrequency);
}
// Adds a monitor to the set of routes.
private void AddMonitorToRoute(EntityUid uid, string address, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
router.MonitorRoutes.Add(address);
}
private void RemoveMonitorFromRoute(EntityUid uid, string address, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
router.MonitorRoutes.Remove(address);
}
// Pings a subnet to get all camera information.
private void PingSubnet(EntityUid uid, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraPingMessage },
{ SurveillanceCameraSystem.CameraSubnetData, router.SubnetName }
};
_deviceNetworkSystem.QueuePacket(uid, null, payload, router.SubnetFrequency);
}
// Sends camera information to all monitors currently interested.
private void SendCameraInfo(EntityUid uid, NetworkPayload payload, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
foreach (var address in router.MonitorRoutes)
{
_deviceNetworkSystem.QueuePacket(uid, address, payload);
}
}
}

View File

@@ -0,0 +1,417 @@
using Content.Server.Administration.Managers;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Ghost.Components;
using Content.Server.Power.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.DeviceNetwork;
using Content.Shared.SurveillanceCamera;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Server.SurveillanceCamera;
public sealed class SurveillanceCameraSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly ViewSubscriberSystem _viewSubscriberSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
// Pings a surveillance camera subnet. All cameras will always respond
// with a data message if they are on the same subnet.
public const string CameraPingSubnetMessage = "surveillance_camera_ping_subnet";
// Pings a surveillance camera. Useful to ensure that the camera is still on
// before connecting fully.
public const string CameraPingMessage = "surveillance_camera_ping";
// Camera heartbeat. Monitors ping this to ensure that a camera is still able to
// be contacted. If this doesn't get sent after some time, the monitor will
// automatically disconnect.
public const string CameraHeartbeatMessage = "surveillance_camera_heartbeat";
// Surveillance camera data. This generally should contain nothing
// except for the subnet that this camera is on -
// this is because of the fact that the PacketEvent already
// contains the sender UID, and that this will always be targeted
// towards the sender that pinged the camera.
public const string CameraDataMessage = "surveillance_camera_data";
public const string CameraConnectMessage = "surveillance_camera_connect";
public const string CameraSubnetConnectMessage = "surveillance_camera_subnet_connect";
public const string CameraSubnetDisconnectMessage = "surveillance_camera_subnet_disconnect";
public const string CameraAddressData = "surveillance_camera_data_origin";
public const string CameraNameData = "surveillance_camera_data_name";
public const string CameraSubnetData = "surveillance_camera_data_subnet";
public const int CameraNameLimit = 32;
public override void Initialize()
{
SubscribeLocalEvent<SurveillanceCameraComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<SurveillanceCameraComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<SurveillanceCameraComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<SurveillanceCameraComponent, SurveillanceCameraSetupSetName>(OnSetName);
SubscribeLocalEvent<SurveillanceCameraComponent, SurveillanceCameraSetupSetNetwork>(OnSetNetwork);
SubscribeLocalEvent<SurveillanceCameraComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
}
private void OnPacketReceived(EntityUid uid, SurveillanceCameraComponent component, DeviceNetworkPacketEvent args)
{
if (!component.Active)
{
return;
}
if (!TryComp(uid, out DeviceNetworkComponent? deviceNet))
{
return;
}
if (args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
{
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, string.Empty },
{ CameraAddressData, deviceNet.Address },
{ CameraNameData, component.CameraId },
{ CameraSubnetData, string.Empty }
};
var dest = string.Empty;
switch (command)
{
case CameraConnectMessage:
if (!args.Data.TryGetValue(CameraAddressData, out dest)
|| string.IsNullOrEmpty(args.Address))
{
return;
}
payload[DeviceNetworkConstants.Command] = CameraConnectMessage;
break;
case CameraHeartbeatMessage:
if (!args.Data.TryGetValue(CameraAddressData, out dest)
|| string.IsNullOrEmpty(args.Address))
{
return;
}
payload[DeviceNetworkConstants.Command] = CameraHeartbeatMessage;
break;
case CameraPingMessage:
if (!args.Data.TryGetValue(CameraSubnetData, out string? subnet))
{
return;
}
dest = args.SenderAddress;
payload[CameraSubnetData] = subnet;
payload[DeviceNetworkConstants.Command] = CameraDataMessage;
break;
}
_deviceNetworkSystem.QueuePacket(
uid,
dest,
payload);
}
}
private void AddVerbs(EntityUid uid, SurveillanceCameraComponent component, GetVerbsEvent<AlternativeVerb> verbs)
{
if (!_actionBlocker.CanInteract(verbs.User, uid))
{
return;
}
if (component.NameSet && component.NetworkSet)
{
return;
}
AlternativeVerb verb = new();
verb.Text = Loc.GetString("surveillance-camera-setup");
verb.Act = () => OpenSetupInterface(uid, verbs.User, component);
verbs.Verbs.Add(verb);
}
private void OnPowerChanged(EntityUid camera, SurveillanceCameraComponent component, PowerChangedEvent args)
{
SetActive(camera, args.Powered, component);
}
private void OnShutdown(EntityUid camera, SurveillanceCameraComponent component, ComponentShutdown args)
{
Deactivate(camera, component);
}
private void OnSetName(EntityUid uid, SurveillanceCameraComponent component, SurveillanceCameraSetupSetName args)
{
if (args.UiKey is not SurveillanceCameraSetupUiKey key
|| key != SurveillanceCameraSetupUiKey.Camera
|| string.IsNullOrEmpty(args.Name)
|| args.Name.Length > CameraNameLimit)
{
return;
}
component.CameraId = args.Name;
component.NameSet = true;
UpdateSetupInterface(uid, component);
}
private void OnSetNetwork(EntityUid uid, SurveillanceCameraComponent component,
SurveillanceCameraSetupSetNetwork args)
{
if (args.UiKey is not SurveillanceCameraSetupUiKey key
|| key != SurveillanceCameraSetupUiKey.Camera)
{
return;
}
if (args.Network < 0 || args.Network >= component.AvailableNetworks.Count)
{
return;
}
if (!_prototypeManager.TryIndex<DeviceFrequencyPrototype>(component.AvailableNetworks[args.Network],
out var frequency))
{
return;
}
_deviceNetworkSystem.SetReceiveFrequency(uid, frequency.Frequency);
component.NetworkSet = true;
UpdateSetupInterface(uid, component);
}
private void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraComponent? camera = null, ActorComponent? actor = null)
{
if (!Resolve(uid, ref camera)
|| !Resolve(player, ref actor))
{
return;
}
_userInterface.GetUiOrNull(uid, SurveillanceCameraSetupUiKey.Camera)!.Open(actor.PlayerSession);
UpdateSetupInterface(uid, camera);
}
private void UpdateSetupInterface(EntityUid uid, SurveillanceCameraComponent? camera = null, DeviceNetworkComponent? deviceNet = null)
{
if (!Resolve(uid, ref camera, ref deviceNet))
{
return;
}
if (camera.NameSet && camera.NetworkSet)
{
_userInterface.TryCloseAll(uid, SurveillanceCameraSetupUiKey.Camera);
return;
}
if (camera.AvailableNetworks.Count == 0)
{
if (deviceNet.ReceiveFrequencyId != null)
{
camera.AvailableNetworks.Add(deviceNet.ReceiveFrequencyId);
}
else if (!camera.NetworkSet)
{
_userInterface.TryCloseAll(uid, SurveillanceCameraSetupUiKey.Camera);
return;
}
}
var state = new SurveillanceCameraSetupBoundUiState(camera.CameraId, deviceNet.ReceiveFrequency ?? 0,
camera.AvailableNetworks, camera.NameSet, camera.NetworkSet);
_userInterface.TrySetUiState(uid, SurveillanceCameraSetupUiKey.Camera, state);
}
// If the camera deactivates for any reason, it must have all viewers removed,
// and the relevant event broadcast to all systems.
private void Deactivate(EntityUid camera, SurveillanceCameraComponent? component = null)
{
if (!Resolve(camera, ref component))
{
return;
}
var ev = new SurveillanceCameraDeactivateEvent(camera);
RemoveActiveViewers(camera, new(component.ActiveViewers), null, component);
component.Active = false;
// Send a targetted event to all monitors.
foreach (var monitor in component.ActiveMonitors)
{
RaiseLocalEvent(monitor, ev);
}
component.ActiveMonitors.Clear();
// Send a local event that's broadcasted everywhere afterwards.
RaiseLocalEvent(ev);
UpdateVisuals(camera, component);
}
public void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null)
{
if (!Resolve(camera, ref component))
{
return;
}
if (setting)
{
component.Active = setting;
}
else
{
Deactivate(camera, component);
}
UpdateVisuals(camera, component);
}
public void AddActiveViewer(EntityUid camera, EntityUid player, EntityUid? monitor = null, SurveillanceCameraComponent? component = null, ActorComponent? actor = null)
{
if (!Resolve(camera, ref component)
|| !component.Active
|| !Resolve(player, ref actor))
{
return;
}
_viewSubscriberSystem.AddViewSubscriber(camera, actor.PlayerSession);
component.ActiveViewers.Add(player);
if (monitor != null)
{
component.ActiveMonitors.Add(monitor.Value);
}
UpdateVisuals(camera, component);
}
public void AddActiveViewers(EntityUid camera, HashSet<EntityUid> players, EntityUid? monitor = null, SurveillanceCameraComponent? component = null)
{
if (!Resolve(camera, ref component) || !component.Active)
{
return;
}
foreach (var player in players)
{
AddActiveViewer(camera, player, monitor, component);
}
}
// Switch the set of active viewers from one camera to another.
public void SwitchActiveViewers(EntityUid oldCamera, EntityUid newCamera, HashSet<EntityUid> players, EntityUid? monitor = null, SurveillanceCameraComponent? oldCameraComponent = null, SurveillanceCameraComponent? newCameraComponent = null)
{
if (!Resolve(oldCamera, ref oldCameraComponent)
|| !Resolve(newCamera, ref newCameraComponent)
|| !oldCameraComponent.Active
|| !newCameraComponent.Active)
{
return;
}
if (monitor != null)
{
oldCameraComponent.ActiveMonitors.Remove(monitor.Value);
newCameraComponent.ActiveMonitors.Add(monitor.Value);
}
foreach (var player in players)
{
RemoveActiveViewer(oldCamera, player, null, oldCameraComponent);
AddActiveViewer(newCamera, player, null, newCameraComponent);
}
}
public void RemoveActiveViewer(EntityUid camera, EntityUid player, EntityUid? monitor = null, SurveillanceCameraComponent? component = null, ActorComponent? actor = null)
{
if (!Resolve(camera, ref component)
|| !Resolve(player, ref actor))
{
return;
}
_viewSubscriberSystem.RemoveViewSubscriber(camera, actor.PlayerSession);
component.ActiveViewers.Remove(player);
if (monitor != null)
{
component.ActiveMonitors.Remove(monitor.Value);
}
UpdateVisuals(camera, component);
}
public void RemoveActiveViewers(EntityUid camera, HashSet<EntityUid> players, EntityUid? monitor = null, SurveillanceCameraComponent? component = null)
{
if (!Resolve(camera, ref component))
{
return;
}
foreach (var player in players)
{
RemoveActiveViewer(camera, player, monitor, component);
}
}
private void UpdateVisuals(EntityUid camera, SurveillanceCameraComponent? component = null, AppearanceComponent? appearance = null)
{
// Don't log missing, because otherwise tests fail.
if (!Resolve(camera, ref component, ref appearance, false))
{
return;
}
var key = SurveillanceCameraVisuals.Disabled;
if (component.Active)
{
key = SurveillanceCameraVisuals.Active;
}
if (component.ActiveViewers.Count > 0 || component.ActiveMonitors.Count > 0)
{
key = SurveillanceCameraVisuals.InUse;
}
appearance.SetData(SurveillanceCameraVisualsKey.Key, key);
}
}
public sealed class OnSurveillanceCameraViewerAddEvent : EntityEventArgs
{
}
public sealed class OnSurveillanceCameraViewerRemoveEvent : EntityEventArgs
{
}
// What happens when a camera deactivates.
public sealed class SurveillanceCameraDeactivateEvent : EntityEventArgs
{
public EntityUid Camera { get; }
public SurveillanceCameraDeactivateEvent(EntityUid camera)
{
Camera = camera;
}
}