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