From 6e108bd4007006e558d239571a7b955fa0b660c5 Mon Sep 17 00:00:00 2001
From: Flipp Syder <76629141+vulppine@users.noreply.github.com>
Date: Sun, 16 Oct 2022 10:44:14 -0700
Subject: [PATCH] Surveillance camera listening/speaking (#11640)
---
.../Monitor/Systems/AtmosAlarmableSystem.cs | 4 +-
.../Monitor/Systems/AtmosMonitoringSystem.cs | 2 +-
.../SurveillanceCameraMicrophoneComponent.cs | 47 ++++++++++
.../SurveillanceCameraSpeakerComponent.cs | 18 ++++
.../SurveillanceCameraMicrophoneSystem.cs | 48 +++++++++++
.../SurveillanceCameraMonitorSystem.cs | 8 +-
.../SurveillanceCameraSpeakerSystem.cs | 85 +++++++++++++++++++
.../surveillance-camera-speaker.ftl | 1 +
.../surveillance-camera-ui.ftl | 1 +
.../Machines/Computers/computers.yml | 3 +
.../Machines/wireless_surveillance_camera.yml | 5 ++
.../Wallmounts/monitors_televisions.yml | 2 +
12 files changed, 219 insertions(+), 5 deletions(-)
create mode 100644 Content.Server/SurveillanceCamera/Components/SurveillanceCameraMicrophoneComponent.cs
create mode 100644 Content.Server/SurveillanceCamera/Components/SurveillanceCameraSpeakerComponent.cs
create mode 100644 Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs
create mode 100644 Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs
create mode 100644 Resources/Locale/en-US/surveillance-camera/surveillance-camera-speaker.ftl
diff --git a/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs b/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs
index 38d492cf75..b674f86442 100644
--- a/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs
+++ b/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs
@@ -243,7 +243,7 @@ public sealed class AtmosAlarmableSystem : EntitySystem
///
public void Reset(EntityUid uid, AtmosAlarmableComponent? alarmable = null, TagComponent? tags = null)
{
- if (!Resolve(uid, ref alarmable, ref tags) || alarmable.LastAlarmState == AtmosAlarmType.Normal)
+ if (!Resolve(uid, ref alarmable, ref tags, false) || alarmable.LastAlarmState == AtmosAlarmType.Normal)
{
return;
}
@@ -285,7 +285,7 @@ public sealed class AtmosAlarmableSystem : EntitySystem
{
alarm = null;
- if (!Resolve(uid, ref alarmable))
+ if (!Resolve(uid, ref alarmable, false))
{
return false;
}
diff --git a/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs b/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
index a37c930eee..079a9b6911 100644
--- a/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
+++ b/Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
@@ -337,7 +337,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
{
if (!monitor.NetEnabled) return;
- if (!Resolve(monitor.Owner, ref tags))
+ if (!Resolve(monitor.Owner, ref tags, false))
{
return;
}
diff --git a/Content.Server/SurveillanceCamera/Components/SurveillanceCameraMicrophoneComponent.cs b/Content.Server/SurveillanceCamera/Components/SurveillanceCameraMicrophoneComponent.cs
new file mode 100644
index 0000000000..ef4d43387c
--- /dev/null
+++ b/Content.Server/SurveillanceCamera/Components/SurveillanceCameraMicrophoneComponent.cs
@@ -0,0 +1,47 @@
+using Content.Server.Radio.Components;
+using Content.Shared.Interaction;
+using Content.Shared.Radio;
+using Content.Shared.Whitelist;
+
+namespace Content.Server.SurveillanceCamera;
+
+///
+/// Component that allows surveillance cameras to listen to the local
+/// environment. All surveillance camera monitors have speakers for this.
+///
+[RegisterComponent]
+[ComponentReference(typeof(IListen))]
+public sealed class SurveillanceCameraMicrophoneComponent : Component, IListen
+{
+ public bool Enabled { get; set; } = true;
+
+ ///
+ /// Components that the microphone checks for to avoid transmitting
+ /// messages from these entities over the surveillance camera.
+ /// Used to avoid things like feedback loops, or radio spam.
+ ///
+ [DataField("blacklist")]
+ public EntityWhitelist BlacklistedComponents { get; } = new();
+
+ // TODO: Once IListen is removed, **REMOVE THIS**
+
+ private SurveillanceCameraMicrophoneSystem? _microphoneSystem;
+ protected override void Initialize()
+ {
+ base.Initialize();
+
+ _microphoneSystem = EntitySystem.Get();
+ }
+
+ public int ListenRange { get; } = 10;
+ public bool CanListen(string message, EntityUid source, RadioChannelPrototype? channelPrototype)
+ {
+ return _microphoneSystem != null
+ && _microphoneSystem.CanListen(Owner, source, this);
+ }
+
+ public void Listen(string message, EntityUid speaker, RadioChannelPrototype? channel)
+ {
+ _microphoneSystem?.RelayEntityMessage(Owner, speaker, message);
+ }
+}
diff --git a/Content.Server/SurveillanceCamera/Components/SurveillanceCameraSpeakerComponent.cs b/Content.Server/SurveillanceCamera/Components/SurveillanceCameraSpeakerComponent.cs
new file mode 100644
index 0000000000..63874c72f7
--- /dev/null
+++ b/Content.Server/SurveillanceCamera/Components/SurveillanceCameraSpeakerComponent.cs
@@ -0,0 +1,18 @@
+namespace Content.Server.SurveillanceCamera;
+
+///
+/// This allows surveillance cameras to speak, if the camera in question
+/// has a microphone that listens to speech.
+///
+[RegisterComponent]
+public sealed class SurveillanceCameraSpeakerComponent : Component
+{
+ // mostly copied from Speech
+ [DataField("speechEnabled")] public bool SpeechEnabled = true;
+
+ [ViewVariables] public float SpeechSoundCooldown = 0.5f;
+
+ [ViewVariables] public readonly Queue LastSpokenNames = new();
+
+ public TimeSpan LastSoundPlayed = TimeSpan.Zero;
+}
diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs
new file mode 100644
index 0000000000..9ee4a32dc7
--- /dev/null
+++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMicrophoneSystem.cs
@@ -0,0 +1,48 @@
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+
+namespace Content.Server.SurveillanceCamera;
+
+public sealed class SurveillanceCameraMicrophoneSystem : EntitySystem
+{
+ [Dependency] private SharedInteractionSystem _interactionSystem = default!;
+
+ public bool CanListen(EntityUid source, EntityUid speaker, SurveillanceCameraMicrophoneComponent? microphone = null)
+ {
+ if (!Resolve(source, ref microphone))
+ {
+ return false;
+ }
+
+ return microphone.Enabled
+ && !microphone.BlacklistedComponents.IsValid(speaker)
+ && _interactionSystem.InRangeUnobstructed(source, speaker, range: microphone.ListenRange);
+ }
+ public void RelayEntityMessage(EntityUid source, EntityUid speaker, string message, SurveillanceCameraComponent? camera = null)
+ {
+ if (!Resolve(source, ref camera))
+ {
+ return;
+ }
+
+ var ev = new SurveillanceCameraSpeechSendEvent(speaker, message);
+
+ foreach (var monitor in camera.ActiveMonitors)
+ {
+ RaiseLocalEvent(monitor, ev);
+ }
+ }
+}
+
+public sealed class SurveillanceCameraSpeechSendEvent : EntityEventArgs
+{
+ public EntityUid Speaker { get; }
+ public string Message { get; }
+
+ public SurveillanceCameraSpeechSendEvent(EntityUid speaker, string message)
+ {
+ Speaker = speaker;
+ Message = message;
+ }
+}
+
diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs
index 87fcce5df3..09f767ffd9 100644
--- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs
+++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs
@@ -1,13 +1,18 @@
using System.Linq;
+using Content.Server.Chat.Systems;
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.Speech;
using Content.Shared.SurveillanceCamera;
using Robust.Server.GameObjects;
using Robust.Server.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
namespace Content.Server.SurveillanceCamera;
@@ -370,8 +375,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
if (monitor.ActiveCamera != null)
{
- EntityUid? monitorUid = monitor.Viewers.Count == 0 ? uid : null;
- _surveillanceCameras.RemoveActiveViewer(monitor.ActiveCamera.Value, player, monitorUid);
+ _surveillanceCameras.RemoveActiveViewer(monitor.ActiveCamera.Value, player);
}
}
diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs
new file mode 100644
index 0000000000..2e6ede8a79
--- /dev/null
+++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs
@@ -0,0 +1,85 @@
+using Content.Server.Chat.Systems;
+using Content.Shared.Speech;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.SurveillanceCamera;
+
+///
+/// This handles speech for surveillance camera monitors.
+///
+public sealed class SurveillanceCameraSpeakerSystem : EntitySystem
+{
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly ChatSystem _chatSystem = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnSpeechSent);
+ SubscribeLocalEvent(OnTransformSpeech);
+ }
+
+ private void OnSpeechSent(EntityUid uid, SurveillanceCameraSpeakerComponent component,
+ SurveillanceCameraSpeechSendEvent args)
+ {
+ if (!component.SpeechEnabled)
+ {
+ return;
+ }
+
+ var time = _gameTiming.CurTime;
+ var cd = TimeSpan.FromSeconds(component.SpeechSoundCooldown);
+
+ // this part's mostly copied from speech
+ if (time - component.LastSoundPlayed < cd
+ && TryComp(args.Speaker, out var speech)
+ && speech.SpeechSounds != null
+ && _prototypeManager.TryIndex(speech.SpeechSounds, out SpeechSoundsPrototype? speechProto))
+ {
+ var sound = args.Message[^1] switch
+ {
+ '?' => speechProto.AskSound,
+ '!' => speechProto.ExclaimSound,
+ _ => speechProto.SaySound
+ };
+
+ var uppercase = 0;
+ for (var i = 0; i < args.Message.Length; i++)
+ {
+ if (char.IsUpper(args.Message[i]))
+ {
+ uppercase++;
+ }
+ }
+
+ if (uppercase > args.Message.Length / 2)
+ {
+ sound = speechProto.ExclaimSound;
+ }
+
+ var scale = (float) _random.NextGaussian(1, speechProto.Variation);
+ var param = speech.AudioParams.WithPitchScale(scale);
+ _audioSystem.PlayPvs(sound, uid, param);
+
+ component.LastSoundPlayed = time;
+ }
+
+ var nameEv = new TransformSpeakerNameEvent(args.Speaker, Name(args.Speaker));
+ RaiseLocalEvent(args.Speaker, nameEv);
+ component.LastSpokenNames.Enqueue(nameEv.Name);
+
+ _chatSystem.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Speak, false);
+ }
+
+ private void OnTransformSpeech(EntityUid uid, SurveillanceCameraSpeakerComponent component,
+ TransformSpeakerNameEvent args)
+ {
+ args.Name = Loc.GetString("surveillance-camera-microphone-message", ("speaker", Name(uid)),
+ ("originalName", component.LastSpokenNames.Dequeue()));
+ }
+}
diff --git a/Resources/Locale/en-US/surveillance-camera/surveillance-camera-speaker.ftl b/Resources/Locale/en-US/surveillance-camera/surveillance-camera-speaker.ftl
new file mode 100644
index 0000000000..86e34894cc
--- /dev/null
+++ b/Resources/Locale/en-US/surveillance-camera/surveillance-camera-speaker.ftl
@@ -0,0 +1 @@
+surveillance-camera-microphone-message = {$speaker} ({$originalName})
diff --git a/Resources/Locale/en-US/surveillance-camera/surveillance-camera-ui.ftl b/Resources/Locale/en-US/surveillance-camera/surveillance-camera-ui.ftl
index 25b6c6a58f..9cb32b6eaf 100644
--- a/Resources/Locale/en-US/surveillance-camera/surveillance-camera-ui.ftl
+++ b/Resources/Locale/en-US/surveillance-camera/surveillance-camera-ui.ftl
@@ -10,3 +10,4 @@ surveillance-camera-monitor-ui-no-subnets = No Subnets
surveillance-camera-setup = Setup
surveillance-camera-setup-ui-set = Set
+
diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
index 310248846d..8e0733b3b8 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
@@ -674,7 +674,10 @@
- type: WirelessNetworkConnection
range: 200
- type: DeviceNetworkRequiresPower
+ - type: Speech
+ - type: SurveillanceCameraSpeaker
- type: SurveillanceCameraMonitor
+ speechEnabled: true
- type: ActivatableUI
key: enum.SurveillanceCameraMonitorUiKey.Key
- type: ActivatableUIRequiresPower
diff --git a/Resources/Prototypes/Entities/Structures/Machines/wireless_surveillance_camera.yml b/Resources/Prototypes/Entities/Structures/Machines/wireless_surveillance_camera.yml
index e87eb9febb..b4fffb8171 100644
--- a/Resources/Prototypes/Entities/Structures/Machines/wireless_surveillance_camera.yml
+++ b/Resources/Prototypes/Entities/Structures/Machines/wireless_surveillance_camera.yml
@@ -22,6 +22,11 @@
density: 80
mask:
- MachineMask
+ - type: SurveillanceCameraMicrophone
+ blacklist:
+ components:
+ - SurveillanceCamera
+ - SurveillanceCameraMonitor
- type: UserInterface
interfaces:
- key: enum.SurveillanceCameraSetupUiKey.Camera
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/monitors_televisions.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/monitors_televisions.yml
index 7f14559fdf..1584c3fcea 100644
--- a/Resources/Prototypes/Entities/Structures/Wallmounts/monitors_televisions.yml
+++ b/Resources/Prototypes/Entities/Structures/Wallmounts/monitors_televisions.yml
@@ -159,6 +159,8 @@
- type: WirelessNetworkConnection
range: 200
- type: DeviceNetworkRequiresPower
+ - type: Speech
+ - type: SurveillanceCameraSpeaker
- type: SurveillanceCameraMonitor
- type: ActivatableUI
key: enum.SurveillanceCameraMonitorUiKey.Key