IListener and IRadio purge (#11980)

This commit is contained in:
Leon Friedrich
2022-11-15 17:09:27 +13:00
committed by GitHub
parent bc525425da
commit 0b5a58001c
48 changed files with 946 additions and 643 deletions

View File

@@ -37,7 +37,8 @@ namespace Content.Server.Chat.Commands
if (string.IsNullOrEmpty(message))
return;
EntitySystem.Get<ChatSystem>().TrySendInGameICMessage(playerEntity, message, InGameICChatType.Emote, false, shell, player);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>()
.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Emote, false, false, shell, player);
}
}
}

View File

@@ -37,7 +37,8 @@ namespace Content.Server.Chat.Commands
if (string.IsNullOrEmpty(message))
return;
EntitySystem.Get<ChatSystem>().TrySendInGameICMessage(playerEntity, message, InGameICChatType.Speak, false, shell, player);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>()
.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Speak, false, false, shell, player);
}
}
}

View File

@@ -1,4 +1,4 @@
using Content.Server.Chat.Systems;
using Content.Server.Chat.Systems;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
@@ -37,7 +37,8 @@ namespace Content.Server.Chat.Commands
if (string.IsNullOrEmpty(message))
return;
EntitySystem.Get<ChatSystem>().TrySendInGameICMessage(playerEntity, message, InGameICChatType.Whisper, false, shell, player);
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>()
.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Whisper, false, false, shell, player);
}
}
}

View File

@@ -99,7 +99,7 @@ namespace Content.Server.Chat.Managers
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
ChatMessageToMany(ChatChannel.Admin, message, wrappedMessage, default, false, clients.ToList());
ChatMessageToMany(ChatChannel.Admin, message, wrappedMessage, default, false, clients);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin announcement from {message}: {message}");
}
@@ -217,7 +217,7 @@ namespace Content.Server.Chat.Managers
_netManager.ServerSendMessage(msg, client);
}
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, List<INetChannel> clients, Color? colorOverride = null)
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, IEnumerable<INetChannel> clients, Color? colorOverride = null)
{
var msg = new MsgChatMessage();
msg.Channel = channel;
@@ -229,7 +229,7 @@ namespace Content.Server.Chat.Managers
{
msg.MessageColorOverride = colorOverride.Value;
}
_netManager.ServerSendToMany(msg, clients);
_netManager.ServerSendToMany(msg, clients.ToList());
}
public void ChatMessageToManyFiltered(Filter filter, ChatChannel channel, string message, string wrappedMessage, EntityUid source,

View File

@@ -26,7 +26,7 @@ namespace Content.Server.Chat.Managers
void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat,
INetChannel client, Color? colorOverride = null);
void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat,
List<INetChannel> clients, Color? colorOverride = null);
IEnumerable<INetChannel> clients, Color? colorOverride = null);
void ChatMessageToManyFiltered(Filter filter, ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, Color? colorOverride);
void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, Color? colorOverride = null);

View File

@@ -1,6 +1,6 @@
using System.Linq;
using System.Text.RegularExpressions;
using Content.Server.Headset;
using Content.Server.Radio.Components;
using Content.Shared.Radio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@@ -76,12 +76,9 @@ public sealed partial class ChatSystem
// Re-capitalize message since we removed the prefix.
message = SanitizeMessageCapital(message);
if (_inventory.TryGetSlotEntity(source, "ears", out var entityUid) &&
TryComp(entityUid, out HeadsetComponent? headset))
{
headset.RadioRequested = true;
}
else
var hasHeadset = _inventory.TryGetSlotEntity(source, "ears", out var entityUid) && HasComp<HeadsetComponent>(entityUid);
if (!hasHeadset && !HasComp<IntrinsicRadioTransmitterComponent>(source))
{
_popup.PopupEntity(Loc.GetString("chat-manager-no-headset-on-message"), source, Filter.Entities(source));
}

View File

@@ -30,6 +30,8 @@ using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Content.Server.Speech.EntitySystems;
using Content.Shared.Radio;
namespace Content.Server.Chat.Systems;
@@ -48,15 +50,14 @@ public sealed partial class ChatSystem : SharedChatSystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly ListeningSystem _listener = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
private const int VoiceRange = 10; // how far voice goes in world units
private const int WhisperRange = 2; // how far whisper goes in world units
private const string DefaultAnnouncementSound = "/Audio/Announcements/announce.ogg";
public const int VoiceRange = 10; // how far voice goes in world units
public const int WhisperRange = 2; // how far whisper goes in world units
public const string DefaultAnnouncementSound = "/Audio/Announcements/announce.ogg";
private bool _loocEnabled = true;
private bool _deadLoocEnabled = false;
@@ -106,9 +107,19 @@ public sealed partial class ChatSystem : SharedChatSystem
_configurationManager.SetCVar(CCVars.OocEnabled, true);
}
// ReSharper disable once InconsistentNaming
public void TrySendInGameICMessage(EntityUid source, string message, InGameICChatType desiredType, bool hideChat,
IConsoleShell? shell = null, IPlayerSession? player = null)
/// <summary>
/// Sends an in-character chat message to relevant clients.
/// </summary>
/// <param name="source">The entity that is speaking</param>
/// <param name="message">The message being spoken or emoted</param>
/// <param name="desiredType">The chat type</param>
/// <param name="hideChat">Whether or not this message should appear in the chat window</param>
/// <param name="hideGlobalGhostChat">Whether or not this message should appear in the chat window for out-of-range ghosts (which otherwise ignore range restrictions)</param>
/// <param name="shell"></param>
/// <param name="player">The player doing the speaking</param>
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
public void TrySendInGameICMessage(EntityUid source, string message, InGameICChatType desiredType, bool hideChat, bool hideGlobalGhostChat = false,
IConsoleShell? shell = null, IPlayerSession? player = null, string? nameOverride = null)
{
if (HasComp<GhostComponent>(source))
{
@@ -126,6 +137,7 @@ public sealed partial class ChatSystem : SharedChatSystem
if (!CanSendInGame(message, shell, player))
return;
hideGlobalGhostChat |= hideChat;
bool shouldCapitalize = (desiredType != InGameICChatType.Emote);
bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation);
@@ -134,7 +146,7 @@ public sealed partial class ChatSystem : SharedChatSystem
// Was there an emote in the message? If so, send it.
if (player != null && emoteStr != message && emoteStr != null)
{
SendEntityEmote(source, emoteStr, hideChat);
SendEntityEmote(source, emoteStr, hideChat, hideGlobalGhostChat, nameOverride);
}
// This can happen if the entire string is sanitized out.
@@ -145,13 +157,13 @@ public sealed partial class ChatSystem : SharedChatSystem
switch (desiredType)
{
case InGameICChatType.Speak:
SendEntitySpeak(source, message, hideChat);
SendEntitySpeak(source, message, hideChat, hideGlobalGhostChat, nameOverride);
break;
case InGameICChatType.Whisper:
SendEntityWhisper(source, message, hideChat);
SendEntityWhisper(source, message, hideChat, hideGlobalGhostChat, null, nameOverride);
break;
case InGameICChatType.Emote:
SendEntityEmote(source, message, hideChat);
SendEntityEmote(source, message, hideChat, hideGlobalGhostChat, nameOverride);
break;
}
}
@@ -245,7 +257,7 @@ public sealed partial class ChatSystem : SharedChatSystem
#region Private API
private void SendEntitySpeak(EntityUid source, string originalMessage, bool hideChat = false)
private void SendEntitySpeak(EntityUid source, string originalMessage, bool hideChat, bool hideGlobalGhostChat, string? nameOverride)
{
if (!_actionBlocker.CanSpeak(source))
return;
@@ -254,30 +266,38 @@ public sealed partial class ChatSystem : SharedChatSystem
if (channel != null)
{
_listener.PingListeners(source, message, channel);
SendEntityWhisper(source, message, hideChat);
SendEntityWhisper(source, message, hideChat, hideGlobalGhostChat, channel, nameOverride);
return;
}
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
RaiseLocalEvent(source, nameEv);
var name = FormattedMessage.EscapeText(nameEv.Name);
message = TransformSpeech(source, message);
if (message.Length == 0)
return;
// get the entity's apparent name (if no override provided).
string name;
if (nameOverride != null)
{
name = nameOverride;
}
else
{
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
RaiseLocalEvent(source, nameEv);
name = nameEv.Name;
}
name = FormattedMessage.EscapeText(name);
var wrappedMessage = Loc.GetString("chat-manager-entity-say-wrap-message",
("entityName", name), ("message", FormattedMessage.EscapeText(message)));
SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, hideChat);
_listener.PingListeners(source, message, null);
SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, hideChat, hideGlobalGhostChat);
var ev = new EntitySpokeEvent(message);
RaiseLocalEvent(source, ev);
var ev = new EntitySpokeEvent(source, message, channel, null);
RaiseLocalEvent(source, ev, true);
// To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc.
if (!TryComp(source, out ActorComponent? mind))
if (!HasComp<ActorComponent>(source))
return;
if (originalMessage == message)
@@ -286,7 +306,7 @@ public sealed partial class ChatSystem : SharedChatSystem
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
}
private void SendEntityWhisper(EntityUid source, string originalMessage, bool hideChat = false)
private void SendEntityWhisper(EntityUid source, string originalMessage, bool hideChat, bool hideGlobalGhostChat, RadioChannelPrototype? channel, string? nameOverride)
{
if (!_actionBlocker.CanSpeak(source))
return;
@@ -297,48 +317,47 @@ public sealed partial class ChatSystem : SharedChatSystem
var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f);
var transformSource = Transform(source);
var sourceCoords = transformSource.Coordinates;
// get the entity's apparent name (if no override provided).
string name;
if (nameOverride != null)
{
name = nameOverride;
}
else
{
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
RaiseLocalEvent(source, nameEv);
name = nameEv.Name;
}
name = FormattedMessage.EscapeText(name);
var name = FormattedMessage.EscapeText(nameEv.Name);
var xforms = GetEntityQuery<TransformComponent>();
var ghosts = GetEntityQuery<GhostComponent>();
var sessions = new List<ICommonSession>();
ClientDistanceToList(source, VoiceRange, sessions);
// Whisper needs these special calculations, since it can obfuscate the message.
foreach (var session in sessions)
foreach (var (session, data) in GetRecipients(source, VoiceRange))
{
if (session.AttachedEntity is not { Valid: true } playerEntity)
continue;
var transformEntity = xforms.GetComponent(playerEntity);
if (hideGlobalGhostChat && data.Observer && data.Range < 0)
continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them.
if (sourceCoords.InRange(EntityManager, transformEntity.Coordinates, WhisperRange) ||
ghosts.HasComponent(playerEntity))
if (data.Range <= WhisperRange)
{
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
("entityName", name), ("message", FormattedMessage.EscapeText(message)));
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, hideChat, session.ConnectedClient);
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, data.HideChatOverride ?? hideChat, session.ConnectedClient);
}
else
{
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
("entityName", name), ("message", FormattedMessage.EscapeText(obfuscatedMessage)));
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedMessage, source, hideChat,
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedMessage, source, data.HideChatOverride ?? hideChat,
session.ConnectedClient);
}
}
var ev = new EntitySpokeEvent(message);
RaiseLocalEvent(source, ev, false);
var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage);
RaiseLocalEvent(source, ev, true);
if (originalMessage == message)
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}: {originalMessage}.");
@@ -346,18 +365,19 @@ public sealed partial class ChatSystem : SharedChatSystem
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
}
private void SendEntityEmote(EntityUid source, string action, bool hideChat)
private void SendEntityEmote(EntityUid source, string action, bool hideChat, bool hideGlobalGhostChat, string? nameOverride)
{
if (!_actionBlocker.CanEmote(source)) return;
var name = FormattedMessage.EscapeText(Identity.Name(source, EntityManager));
// get the entity's apparent name (if no override provided).
string name = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(source, EntityManager));
// Emotes use Identity.Name, since it doesn't actually involve your voice at all.
var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
("entityName", name),
("message", FormattedMessage.EscapeText(action)));
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, hideChat);
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, hideChat, hideGlobalGhostChat);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}");
}
@@ -375,7 +395,7 @@ public sealed partial class ChatSystem : SharedChatSystem
("entityName", name),
("message", FormattedMessage.EscapeText(message)));
SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat);
SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat, false);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}");
}
@@ -411,11 +431,13 @@ public sealed partial class ChatSystem : SharedChatSystem
/// <summary>
/// Sends a chat message to the given players in range of the source entity.
/// </summary>
private void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat)
private void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool hideGlobalGhostChat)
{
var sessions = new List<ICommonSession>();
ClientDistanceToList(source, VoiceRange, sessions);
_chatManager.ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, sessions.Select(s => s.ConnectedClient).ToList());
foreach (var (session, data) in GetRecipients(source, VoiceRange))
{
var entHideChat = data.HideChatOverride ?? (hideChat || hideGlobalGhostChat && data.Observer && data.Range < 0);
_chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.ConnectedClient);
}
}
/// <summary>
@@ -502,8 +524,14 @@ public sealed partial class ChatSystem : SharedChatSystem
return message;
}
private void ClientDistanceToList(EntityUid source, int voiceRange, List<ICommonSession> playerSessions)
/// <summary>
/// Returns list of players and ranges for all players withing some range. Also returns observers with a range of -1.
/// </summary>
private Dictionary<ICommonSession, ICChatRecipientData> GetRecipients(EntityUid source, float voiceRange)
{
// TODO proper speech occlusion
var recipients = new Dictionary<ICommonSession, ICChatRecipientData>();
var ghosts = GetEntityQuery<GhostComponent>();
var xforms = GetEntityQuery<TransformComponent>();
@@ -518,13 +546,28 @@ public sealed partial class ChatSystem : SharedChatSystem
var transformEntity = xforms.GetComponent(playerEntity);
if (transformEntity.MapID != sourceMapId ||
!ghosts.HasComponent(playerEntity) &&
!sourceCoords.InRange(EntityManager, transformEntity.Coordinates, voiceRange))
if (transformEntity.MapID != sourceMapId)
continue;
playerSessions.Add(player);
var observer = ghosts.HasComponent(playerEntity);
// even if they are an observer, in some situations we still need the range
if (sourceCoords.TryDistance(EntityManager, transformEntity.Coordinates, out var distance) && distance < voiceRange)
{
recipients.Add(player, new ICChatRecipientData(distance, observer));
continue;
}
if (observer)
recipients.Add(player, new ICChatRecipientData(-1, true));
}
RaiseLocalEvent(new ExpandICChatRecipientstEvent(source, voiceRange, recipients));
return recipients;
}
public readonly record struct ICChatRecipientData(float Range, bool Observer, bool? HideChatOverride = null)
{
}
private string ObfuscateMessageReadability(string message, float chance)
@@ -550,6 +593,14 @@ public sealed partial class ChatSystem : SharedChatSystem
#endregion
}
/// <summary>
/// This event is raised before chat messages are sent out to clients. This enables some systems to send the chat
/// messages to otherwise out-of view entities (e.g. for multiple viewports from cameras).
/// </summary>
public record class ExpandICChatRecipientstEvent(EntityUid Source, float VoiceRange, Dictionary<ICommonSession, ChatSystem.ICChatRecipientData> Recipients)
{
}
public sealed class TransformSpeakerNameEvent : EntityEventArgs
{
public EntityUid Sender;
@@ -582,11 +633,22 @@ public sealed class TransformSpeechEvent : EntityEventArgs
/// </summary>
public sealed class EntitySpokeEvent : EntityEventArgs
{
public string Message;
public readonly EntityUid Source;
public readonly string Message;
public readonly string? ObfuscatedMessage; // not null if this was a whisper
public EntitySpokeEvent(string message)
/// <summary>
/// If the entity was trying to speak into a radio, this was the channel they were trying to access. If a radio
/// message gets sent on this channel, this should be set to null to prevent duplicate messages.
/// </summary>
public RadioChannelPrototype? Channel;
public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, string? obfuscatedMessage)
{
Source = source;
Message = message;
Channel = channel;
ObfuscatedMessage = obfuscatedMessage;
}
}

View File

@@ -167,7 +167,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
var newPosition = destination * progress;
// This is some supreme shit code.
EntityManager.GetComponent<TransformComponent>(holder.Owner).Coordinates = origin.Offset(newPosition).WithEntityId(currentTube.Owner); ;
EntityManager.GetComponent<TransformComponent>(holder.Owner).Coordinates = origin.Offset(newPosition).WithEntityId(currentTube.Owner);
continue;
}

View File

@@ -1,22 +1,12 @@
using Content.Shared.Interaction;
using Content.Server.Radio.Components;
using Content.Shared.Radio;
using System.Text.RegularExpressions;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Radio.EntitySystems;
using System;
namespace Content.Server.Explosion.Components
{
/// <summary>
/// Sends a trigger when the keyphrase is heard
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IListen))]
public sealed class TriggerOnVoiceComponent : Component, IListen
public sealed class TriggerOnVoiceComponent : Component
{
private SharedInteractionSystem _sharedInteractionSystem = default!;
private TriggerSystem _triggerSystem = default!;
public bool IsListening => IsRecording || !string.IsNullOrWhiteSpace(KeyPhrase);
[ViewVariables(VVAccess.ReadWrite)]
[DataField("keyPhrase")]
@@ -26,51 +16,13 @@ namespace Content.Server.Explosion.Components
[DataField("listenRange")]
public int ListenRange { get; private set; } = 4;
[ViewVariables]
[DataField("isRecording")]
public bool IsRecording = false;
[ViewVariables]
[DataField("minLength")]
public int MinLength = 3;
[ViewVariables]
[DataField("maxLength")]
public int MaxLength = 50;
/// <summary>
/// Displays 'recorded' popup only for the one who activated
/// it in order to allow for stealthily recording others
/// </summary>
[ViewVariables]
public EntityUid Activator;
protected override void Initialize()
{
base.Initialize();
_sharedInteractionSystem = EntitySystem.Get<SharedInteractionSystem>();
_triggerSystem = EntitySystem.Get<TriggerSystem>();
}
bool IListen.CanListen(string message, EntityUid source, RadioChannelPrototype? channelPrototype)
{
//will hear standard speech and radio messages originating nearby but not independant whispers
return _sharedInteractionSystem.InRangeUnobstructed(Owner, source, range: ListenRange);
}
void IListen.Listen(string message, EntityUid speaker, RadioChannelPrototype? channel)
{
message = message.Trim();
if (IsRecording && message.Length >= MinLength && message.Length <= MaxLength)
{
KeyPhrase = message;
_triggerSystem.ToggleRecord(this, Activator, true);
}
else if (KeyPhrase != null && message.Contains(KeyPhrase, StringComparison.InvariantCultureIgnoreCase))
{
_triggerSystem.Trigger(Owner, speaker);
}
}
}
}

View File

@@ -1,14 +1,10 @@
using Content.Server.Explosion.Components;
using Content.Server.Nutrition.Components;
using Content.Server.Speech;
using Content.Server.Speech.Components;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Microsoft.CodeAnalysis.Options;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server.Explosion.EntitySystems
{
@@ -16,8 +12,37 @@ namespace Content.Server.Explosion.EntitySystems
{
private void InitializeVoice()
{
SubscribeLocalEvent<TriggerOnVoiceComponent, ComponentInit>(OnVoiceInit);
SubscribeLocalEvent<TriggerOnVoiceComponent, ExaminedEvent>(OnVoiceExamine);
SubscribeLocalEvent<TriggerOnVoiceComponent, GetVerbsEvent<AlternativeVerb>>(OnVoiceGetAltVerbs);
SubscribeLocalEvent<TriggerOnVoiceComponent, ListenEvent>(OnListen);
}
private void OnVoiceInit(EntityUid uid, TriggerOnVoiceComponent component, ComponentInit args)
{
if (component.IsListening)
EnsureComp<ActiveListenerComponent>(uid).Range = component.ListenRange;
else
RemCompDeferred<ActiveListenerComponent>(uid);
}
private void OnListen(EntityUid uid, TriggerOnVoiceComponent component, ListenEvent args)
{
var message = args.Message.Trim();
if (component.IsRecording)
{
if (message.Length >= component.MinLength || message.Length <= component.MaxLength)
FinishRecording(component, args.Source, args.Message);
return;
}
if (!string.IsNullOrWhiteSpace(component.KeyPhrase) && message.Contains(component.KeyPhrase, StringComparison.InvariantCultureIgnoreCase))
{
_adminLogger.Add(LogType.Trigger, LogImpact.High,
$"A voice-trigger on {ToPrettyString(uid):entity} was triggered by {ToPrettyString(args.Source):speaker} speaking the key-phrase {component.KeyPhrase}.");
Trigger(uid);
}
}
private void OnVoiceGetAltVerbs(EntityUid uid, TriggerOnVoiceComponent component, GetVerbsEvent<AlternativeVerb> args)
@@ -27,35 +52,74 @@ namespace Content.Server.Explosion.EntitySystems
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString("verb-trigger-voice-record"),
Act = () => ToggleRecord(component, args.User),
Text = Loc.GetString(component.IsRecording ? "verb-trigger-voice-record-stop" : "verb-trigger-voice-record"),
Act = () =>
{
if (component.IsRecording)
StopRecording(component);
else
StartRecording(component, args.User);
},
Priority = 1
});
if (string.IsNullOrWhiteSpace(component.KeyPhrase))
return;
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString("verb-trigger-voice-clear"),
Act = () =>
{
component.KeyPhrase = null;
component.IsRecording = false;
RemComp<ActiveListenerComponent>(uid);
}
});
}
public void ToggleRecord(TriggerOnVoiceComponent component, EntityUid user, bool recorded = false)
public void StartRecording(TriggerOnVoiceComponent component, EntityUid user)
{
component.IsRecording ^= true;
component.IsRecording = true;
EnsureComp<ActiveListenerComponent>(component.Owner).Range = component.ListenRange;
if (recorded) //recording success popup
{
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-recorded"), component.Owner, Filter.Entities(user));
_adminLogger.Add(LogType.Trigger, LogImpact.Low,
$"A voice-trigger on {ToPrettyString(component.Owner):entity} has started recording. User: {ToPrettyString(user):user}");
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-start-recording"), component.Owner, Filter.Pvs(component.Owner));
}
else if (component.IsRecording) //recording start popup
public void StopRecording(TriggerOnVoiceComponent component)
{
component.Activator = user;
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-start-recording"), component.Owner, Filter.Entities(user));
component.IsRecording = false;
if (string.IsNullOrWhiteSpace(component.KeyPhrase))
RemComp<ActiveListenerComponent>(component.Owner);
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-stop-recording"), component.Owner, Filter.Pvs(component.Owner));
}
else //recording stopped manually popup
public void FinishRecording(TriggerOnVoiceComponent component, EntityUid source, string message)
{
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-stop-recording"), component.Owner, Filter.Entities(user));
}
component.KeyPhrase = message;
component.IsRecording = false;
_adminLogger.Add(LogType.Trigger, LogImpact.Low,
$"A voice-trigger on {ToPrettyString(component.Owner):entity} has recorded a new keyphrase: '{component.KeyPhrase}'. Recorded from {ToPrettyString(source):speaker}");
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-recorded", ("keyphrase", component.KeyPhrase!)), component.Owner, Filter.Pvs(component.Owner));
}
private void OnVoiceExamine(EntityUid uid, TriggerOnVoiceComponent component, ExaminedEvent args)
{
args.PushText(Loc.GetString("examine-trigger-voice"));
if (args.IsInDetailsRange)
args.PushText(Loc.GetString("examine-trigger-voice", ("keyphrase", component.KeyPhrase?? Loc.GetString("trigger-voice-uninitialized"))));
{
if (component.KeyPhrase == null)
args.PushText(string.IsNullOrWhiteSpace(component.KeyPhrase)
? Loc.GetString("examine-trigger-voice-blank")
: Loc.GetString("examine-trigger-voice-keyphrase", ("keyphrase", component.KeyPhrase)));
}
}
}
}

View File

@@ -1,56 +0,0 @@
using Content.Server.Radio.Components;
using Content.Server.VoiceMask;
using Content.Shared.Chat;
using Content.Shared.IdentityManagement;
using Content.Shared.Radio;
using Robust.Server.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
using Robust.Shared.Utility;
namespace Content.Server.Ghost.Components
{
/// <summary>
/// Add to a particular entity to let it receive messages from the specified channels.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IRadio))]
public sealed class IntrinsicRadioComponent : Component, IRadio
{
// TODO: This class is yuck
[Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>), required: true)]
private HashSet<string> _channels = new();
public void Receive(string message, RadioChannelPrototype channel, EntityUid speaker)
{
if (!_channels.Contains(channel.ID) || !_entMan.TryGetComponent(Owner, out ActorComponent? actor))
return;
var playerChannel = actor.PlayerSession.ConnectedClient;
var name = _entMan.GetComponent<MetaDataComponent>(speaker).EntityName;
if (_entMan.TryGetComponent(speaker, out VoiceMaskComponent? mask) && mask.Enabled)
{
name = Identity.Name(speaker, _entMan);
}
message = FormattedMessage.EscapeText(message);
name = FormattedMessage.EscapeText(name);
var msg = new MsgChatMessage
{
Channel = ChatChannel.Radio,
Message = message,
WrappedMessage = Loc.GetString("chat-radio-message-wrap", ("color", channel.Color), ("channel", $"\\[{channel.LocalizedName}\\]"), ("name", name), ("message", message))
};
_netManager.ServerSendMessage(msg, playerChannel);
}
public void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel) { }
}
}

View File

@@ -1,106 +0,0 @@
using Content.Server.Chat.Systems;
using Content.Server.Radio.Components;
using Content.Server.Radio.EntitySystems;
using Content.Server.VoiceMask;
using Content.Shared.Chat;
using Content.Shared.IdentityManagement;
using Content.Shared.Radio;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Network;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
using Robust.Shared.Utility;
namespace Content.Server.Headset
{
[RegisterComponent]
[ComponentReference(typeof(IRadio))]
[ComponentReference(typeof(IListen))]
#pragma warning disable 618
public sealed class HeadsetComponent : Component, IListen, IRadio
#pragma warning restore 618
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IServerNetManager _netManager = default!;
private ChatSystem _chatSystem = default!;
private RadioSystem _radioSystem = default!;
[DataField("channels", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public HashSet<string> Channels = new()
{
"Common"
};
[ViewVariables(VVAccess.ReadWrite)]
[DataField("listenRange")]
public int ListenRange { get; private set; }
public bool RadioRequested { get; set; }
protected override void Initialize()
{
base.Initialize();
_chatSystem = EntitySystem.Get<ChatSystem>();
_radioSystem = EntitySystem.Get<RadioSystem>();
}
public bool CanListen(string message, EntityUid source, RadioChannelPrototype? prototype)
{
return prototype != null && Channels.Contains(prototype.ID) && RadioRequested;
}
public void Receive(string message, RadioChannelPrototype channel, EntityUid source)
{
if (!Channels.Contains(channel.ID) || !Owner.TryGetContainer(out var container)) return;
if (!_entMan.TryGetComponent(container.Owner, out ActorComponent? actor)) return;
var playerChannel = actor.PlayerSession.ConnectedClient;
var name = _entMan.GetComponent<MetaDataComponent>(source).EntityName;
if (_entMan.TryGetComponent(source, out VoiceMaskComponent? mask) && mask.Enabled)
{
name = mask.VoiceName;
}
message = _chatSystem.TransformSpeech(source, message);
if (message.Length == 0)
return;
message = FormattedMessage.EscapeText(message);
name = FormattedMessage.EscapeText(name);
var msg = new MsgChatMessage
{
Channel = ChatChannel.Radio,
Message = message,
WrappedMessage = Loc.GetString("chat-radio-message-wrap", ("color", channel.Color), ("channel", $"\\[{channel.LocalizedName}\\]"), ("name", name), ("message", message))
};
_netManager.ServerSendMessage(msg, playerChannel);
}
public void Listen(string message, EntityUid speaker, RadioChannelPrototype? channel)
{
if (channel == null)
{
return;
}
Broadcast(message, speaker, channel);
}
public void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel)
{
if (!Channels.Contains(channel.ID)) return;
_radioSystem.SpreadMessage(this, speaker, message, channel);
RadioRequested = false;
}
}
}

View File

@@ -1,39 +0,0 @@
using Content.Shared.Examine;
using Content.Shared.Radio;
using Robust.Shared.Prototypes;
namespace Content.Server.Headset
{
public sealed class HeadsetSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HeadsetComponent, ExaminedEvent>(OnExamined);
}
private void OnExamined(EntityUid uid, HeadsetComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
// args.PushMarkup(Loc.GetString("examine-radio-frequency", ("frequency", component.BroadcastFrequency)));
args.PushMarkup(Loc.GetString("examine-headset"));
foreach (var id in component.Channels)
{
if (id == "Common") continue;
var proto = _protoManager.Index<RadioChannelPrototype>(id);
args.PushMarkup(Loc.GetString("examine-headset-channel",
("color", proto.Color),
("key", proto.KeyCode),
("id", proto.LocalizedName),
("freq", proto.Frequency)));
}
args.PushMarkup(Loc.GetString("examine-headset-chat-prefix", ("prefix", ";")));
}
}
}

View File

@@ -70,7 +70,7 @@ public sealed class MedibotInjectOperator : HTNOperator
_solutionSystem.TryAddReagent(target, injectable, botComp.EmergencyMed, botComp.EmergencyMedInjectAmount, out var accepted);
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), target);
_chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, false);
_chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, hideChat: false, hideGlobalGhostChat: true);
return HTNOperatorStatus.Finished;
}
@@ -79,7 +79,7 @@ public sealed class MedibotInjectOperator : HTNOperator
_solutionSystem.TryAddReagent(target, injectable, botComp.StandardMed, botComp.StandardMedInjectAmount, out var accepted);
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), target);
_chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, false);
_chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, hideChat: false, hideGlobalGhostChat: true);
return HTNOperatorStatus.Finished;
}

View File

@@ -0,0 +1,17 @@
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Radio.Components;
/// <summary>
/// This component is required to receive radio message events.
/// </summary>
[RegisterComponent]
public sealed class ActiveRadioComponent : Component
{
/// <summary>
/// The channels that this radio is listening on.
/// </summary>
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public HashSet<string> Channels = new();
}

View File

@@ -1,114 +0,0 @@
using System.Linq;
using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Radio.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Radio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Radio.Components
{
[RegisterComponent]
[ComponentProtoName("Radio")]
[ComponentReference(typeof(IRadio))]
[ComponentReference(typeof(IListen))]
#pragma warning disable 618
public sealed class HandheldRadioComponent : Component, IListen, IRadio
#pragma warning restore 618
{
private ChatSystem? _chatSystem;
private RadioSystem? _radioSystem;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private bool _radioOn;
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
private HashSet<string> _channels = new();
public int BroadcastFrequency => IoCManager.Resolve<IPrototypeManager>()
.Index<RadioChannelPrototype>(BroadcastChannel).Frequency;
// TODO: Assert in componentinit that channels has this.
[ViewVariables(VVAccess.ReadWrite)]
[DataField("broadcastChannel", customTypeSerializer: typeof(PrototypeIdSerializer<RadioChannelPrototype>))]
public string BroadcastChannel { get; set; } = "Common";
[ViewVariables(VVAccess.ReadWrite)] [DataField("listenRange")] public int ListenRange { get; private set; } = 7;
[ViewVariables(VVAccess.ReadWrite)]
public bool RadioOn
{
get => _radioOn;
private set
{
_radioOn = value;
Dirty();
}
}
protected override void Initialize()
{
base.Initialize();
_radioSystem = EntitySystem.Get<RadioSystem>();
_chatSystem = EntitySystem.Get<ChatSystem>();
RadioOn = false;
}
public void Speak(string message)
{
_chatSystem?.TrySendInGameICMessage(Owner, message, InGameICChatType.Speak, false);
}
public bool Use(EntityUid user)
{
RadioOn = !RadioOn;
var message = Loc.GetString("handheld-radio-component-on-use",
("radioState", Loc.GetString(RadioOn ? "handheld-radio-component-on-state" : "handheld-radio-component-off-state")));
Owner.PopupMessage(user, message);
return true;
}
public bool CanListen(string message, EntityUid source, RadioChannelPrototype? prototype)
{
if (prototype != null && !_channels.Contains(prototype.ID)
|| !_prototypeManager.HasIndex<RadioChannelPrototype>(BroadcastChannel))
{
return false;
}
return RadioOn
&& EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(Owner, source, range: ListenRange);
}
public void Receive(string message, RadioChannelPrototype channel, EntityUid speaker)
{
if (_channels.Contains(channel.ID) && RadioOn)
{
Speak(message);
}
}
public void Listen(string message, EntityUid speaker, RadioChannelPrototype? prototype)
{
// if we can't get the channel, we need to just use the broadcast frequency
if (prototype == null
&& !_prototypeManager.TryIndex(BroadcastChannel, out prototype))
{
return;
}
Broadcast(message, speaker, prototype);
}
public void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel)
{
_radioSystem?.SpreadMessage(this, speaker, message, channel);
}
}
}

View File

@@ -0,0 +1,25 @@
using Content.Server.Radio.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Radio.Components;
/// <summary>
/// This component relays radio messages to the parent entity's chat when equipped.
/// </summary>
[RegisterComponent]
[Access(typeof(HeadsetSystem))]
public sealed class HeadsetComponent : Component
{
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public readonly HashSet<string> Channels = new() { "Common" };
[DataField("enabled")]
public bool Enabled = true;
public bool IsEquipped = false;
[DataField("requiredSlot")]
public SlotFlags RequiredSlot = SlotFlags.EARS;
}

View File

@@ -1,17 +0,0 @@
using Content.Shared.Radio;
namespace Content.Server.Radio.Components
{
/// <summary>
/// Interface for objects such as radios meant to have an effect when speech is
/// heard. Requires component reference.
/// </summary>
public interface IListen : IComponent
{
int ListenRange { get; }
bool CanListen(string message, EntityUid source, RadioChannelPrototype? channelPrototype);
void Listen(string message, EntityUid speaker, RadioChannelPrototype? channel);
}
}

View File

@@ -1,11 +0,0 @@
using Content.Shared.Radio;
namespace Content.Server.Radio.Components
{
public interface IRadio : IComponent
{
void Receive(string message, RadioChannelPrototype channel, EntityUid speaker);
void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel);
}
}

View File

@@ -0,0 +1,11 @@
namespace Content.Server.Radio.Components;
/// <summary>
/// This component allows an entity to directly translate radio messages into chat messages. Note that this does not
/// automatically add an <see cref="ActiveRadioComponent"/>, which is required to receive radio messages on specific
/// channels.
/// </summary>
[RegisterComponent]
public sealed class IntrinsicRadioReceiverComponent : Component
{
}

View File

@@ -0,0 +1,15 @@
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Radio.Components;
/// <summary>
/// This component allows an entity to directly translate spoken text into radio messages (effectively an intrinsic
/// radio headset).
/// </summary>
[RegisterComponent]
public sealed class IntrinsicRadioTransmitterComponent : Component
{
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public readonly HashSet<string> Channels = new() { "Common" };
}

View File

@@ -0,0 +1,24 @@
using Content.Server.Radio.EntitySystems;
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Radio.Components;
/// <summary>
/// Listens for local chat messages and relays them to some radio frequency
/// </summary>
[RegisterComponent]
[Access(typeof(RadioDeviceSystem))]
public sealed class RadioMicrophoneComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("broadcastChannel", customTypeSerializer: typeof(PrototypeIdSerializer<RadioChannelPrototype>))]
public string BroadcastChannel = "Common";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("listenRange")]
public int ListenRange = 4;
[DataField("enabled")]
public bool Enabled = false;
}

View File

@@ -0,0 +1,19 @@
using Content.Server.Radio.EntitySystems;
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Radio.Components;
/// <summary>
/// Listens for radio messages and relays them to local chat.
/// </summary>
[RegisterComponent]
[Access(typeof(RadioDeviceSystem))]
public sealed class RadioSpeakerComponent : Component
{
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public HashSet<string> Channels = new () { "Common" };
[DataField("enabled")]
public bool Enabled;
}

View File

@@ -0,0 +1,13 @@
using Content.Server.Radio.EntitySystems;
namespace Content.Server.Radio.Components;
/// <summary>
/// This component is used to tag players that are currently wearing an ACTIVE headset.
/// </summary>
[RegisterComponent]
public sealed class WearingHeadsetComponent : Component
{
[DataField("headset")]
public EntityUid Headset;
}

View File

@@ -0,0 +1,106 @@
using Content.Server.Chat.Systems;
using Content.Server.Radio.Components;
using Content.Shared.Examine;
using Content.Shared.Inventory.Events;
using Content.Shared.Radio;
using Robust.Server.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Server.Radio.EntitySystems;
public sealed class HeadsetSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly RadioSystem _radio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HeadsetComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<HeadsetComponent, RadioReceiveEvent>(OnHeadsetReceive);
SubscribeLocalEvent<HeadsetComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<HeadsetComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<WearingHeadsetComponent, EntitySpokeEvent>(OnSpeak);
}
private void OnSpeak(EntityUid uid, WearingHeadsetComponent component, EntitySpokeEvent args)
{
if (args.Channel != null
&& TryComp(component.Headset, out HeadsetComponent? headset)
&& headset.Channels.Contains(args.Channel.ID))
{
_radio.SendRadioMessage(uid, args.Message, args.Channel);
args.Channel = null; // prevent duplicate messages from other listeners.
}
}
private void OnGotEquipped(EntityUid uid, HeadsetComponent component, GotEquippedEvent args)
{
component.IsEquipped = args.SlotFlags.HasFlag(component.RequiredSlot);
if (component.IsEquipped && component.Enabled)
{
EnsureComp<WearingHeadsetComponent>(args.Equipee).Headset = uid;
EnsureComp<ActiveRadioComponent>(uid).Channels.UnionWith(component.Channels);
}
}
private void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args)
{
component.IsEquipped = false;
RemCompDeferred<ActiveRadioComponent>(uid);
RemCompDeferred<WearingHeadsetComponent>(args.Equipee);
}
public void SetEnabled(EntityUid uid, bool value, HeadsetComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.Enabled == value)
return;
if (!value)
{
RemCompDeferred<ActiveRadioComponent>(uid);
if (component.IsEquipped)
RemCompDeferred<WearingHeadsetComponent>(Transform(uid).ParentUid);
}
else if (component.IsEquipped)
{
EnsureComp<WearingHeadsetComponent>(Transform(uid).ParentUid).Headset = uid;
EnsureComp<ActiveRadioComponent>(uid).Channels.UnionWith(component.Channels);
}
}
private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, RadioReceiveEvent args)
{
if (TryComp(Transform(uid).ParentUid, out ActorComponent? actor))
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.ConnectedClient);
}
private void OnExamined(EntityUid uid, HeadsetComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(Loc.GetString("examine-headset"));
foreach (var id in component.Channels)
{
if (id == "Common") continue;
var proto = _protoManager.Index<RadioChannelPrototype>(id);
args.PushMarkup(Loc.GetString("examine-headset-channel",
("color", proto.Color),
("key", proto.KeyCode),
("id", proto.LocalizedName),
("freq", proto.Frequency)));
}
args.PushMarkup(Loc.GetString("examine-headset-chat-prefix", ("prefix", ";")));
}
}

View File

@@ -1,23 +0,0 @@
using Content.Server.Radio.Components;
using Content.Shared.Radio;
using JetBrains.Annotations;
namespace Content.Server.Radio.EntitySystems
{
[UsedImplicitly]
public sealed class ListeningSystem : EntitySystem
{
public void PingListeners(EntityUid source, string message, RadioChannelPrototype? channel)
{
foreach (var listener in EntityManager.EntityQuery<IListen>(true))
{
// TODO: Listening code is hella stinky so please refactor it someone.
// TODO: Map Position distance
if (listener.CanListen(message, source, channel))
{
listener.Listen(message, source, channel);
}
}
}
}
}

View File

@@ -0,0 +1,148 @@
using Content.Server.Chat.Systems;
using Content.Server.Popups;
using Content.Server.Radio.Components;
using Content.Server.Speech;
using Content.Server.Speech.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Radio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Radio.EntitySystems;
/// <summary>
/// This system handles radio speakers and microphones (which together form a hand-held radio).
/// </summary>
public sealed class RadioDeviceSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly RadioSystem _radio = default!;
// Used to prevent a shitter from using a bunch of radios to spam chat.
private HashSet<(string, EntityUid)> _recentlySent = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RadioMicrophoneComponent, ComponentInit>(OnMicrophoneInit);
SubscribeLocalEvent<RadioMicrophoneComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<RadioMicrophoneComponent, ActivateInWorldEvent>(OnActivateMicrophone);
SubscribeLocalEvent<RadioMicrophoneComponent, ListenEvent>(OnListen);
SubscribeLocalEvent<RadioSpeakerComponent, ComponentInit>(OnSpeakerInit);
SubscribeLocalEvent<RadioSpeakerComponent, ActivateInWorldEvent>(OnActivateSpeaker);
SubscribeLocalEvent<RadioSpeakerComponent, RadioReceiveEvent>(OnReceiveRadio);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_recentlySent.Clear();
}
#region Component Init
private void OnMicrophoneInit(EntityUid uid, RadioMicrophoneComponent component, ComponentInit args)
{
if (component.Enabled)
EnsureComp<ActiveListenerComponent>(uid).Range = component.ListenRange;
else
RemCompDeferred<ActiveListenerComponent>(uid);
}
private void OnSpeakerInit(EntityUid uid, RadioSpeakerComponent component, ComponentInit args)
{
if (component.Enabled)
EnsureComp<ActiveRadioComponent>(uid).Channels.UnionWith(component.Channels);
else
RemCompDeferred<ActiveRadioComponent>(uid);
}
#endregion
#region Toggling
private void OnActivateMicrophone(EntityUid uid, RadioMicrophoneComponent component, ActivateInWorldEvent args)
{
ToggleRadioMicrophone(uid, args.User, args.Handled, component);
args.Handled = true;
}
private void OnActivateSpeaker(EntityUid uid, RadioSpeakerComponent component, ActivateInWorldEvent args)
{
ToggleRadioSpeaker(uid, args.User, args.Handled, component);
args.Handled = true;
}
public void ToggleRadioMicrophone(EntityUid uid, EntityUid user, bool quiet = false, RadioMicrophoneComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.Enabled = !component.Enabled;
if (!quiet)
{
var state = Loc.GetString(component.Enabled ? "handheld-radio-component-on-state" : "handheld-radio-component-off-state");
var message = Loc.GetString("handheld-radio-component-on-use", ("radioState", state));
_popup.PopupEntity(message, user, Filter.Entities(user));
}
if (component.Enabled)
EnsureComp<ActiveListenerComponent>(uid).Range = component.ListenRange;
else
RemCompDeferred<ActiveListenerComponent>(uid);
}
public void ToggleRadioSpeaker(EntityUid uid, EntityUid user, bool quiet = false, RadioSpeakerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.Enabled = !component.Enabled;
if (!quiet)
{
var state = Loc.GetString(component.Enabled ? "handheld-radio-component-on-state" : "handheld-radio-component-off-state");
var message = Loc.GetString("handheld-radio-component-on-use", ("radioState", state));
_popup.PopupEntity(message, user, Filter.Entities(user));
}
if (component.Enabled)
EnsureComp<ActiveRadioComponent>(uid).Channels.UnionWith(component.Channels);
else
RemCompDeferred<ActiveRadioComponent>(uid);
}
#endregion
private void OnExamine(EntityUid uid, RadioMicrophoneComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
var freq = _protoMan.Index<RadioChannelPrototype>(component.BroadcastChannel).Frequency;
args.PushMarkup(Loc.GetString("handheld-radio-component-on-examine", ("frequency", freq)));
}
private void OnListen(EntityUid uid, RadioMicrophoneComponent component, ListenEvent args)
{
if (HasComp<RadioSpeakerComponent>(args.Source))
return; // no feedback loops please.
if (_recentlySent.Add((args.Message, args.Source)))
_radio.SendRadioMessage(args.Source, args.Message, _protoMan.Index<RadioChannelPrototype>(component.BroadcastChannel));
}
private void OnReceiveRadio(EntityUid uid, RadioSpeakerComponent component, RadioReceiveEvent args)
{
var nameEv = new TransformSpeakerNameEvent(args.Source, Name(args.Source));
RaiseLocalEvent(args.Source, nameEv);
var name = Loc.GetString("speech-name-relay", ("speaker", Name(uid)),
("originalName", nameEv.Name));
var hideGlobalGhostChat = true; // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios
_chat.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Speak, false, nameOverride: name, hideGlobalGhostChat:hideGlobalGhostChat);
}
}

View File

@@ -1,52 +1,89 @@
using System.Linq;
using Content.Shared.Examine;
using Content.Server.Chat.Systems;
using Content.Server.Radio.Components;
using Content.Server.Speech;
using Content.Server.VoiceMask;
using Content.Shared.Chat;
using Content.Shared.IdentityManagement;
using Content.Shared.Radio;
using JetBrains.Annotations;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Utility;
namespace Content.Server.Radio.EntitySystems
namespace Content.Server.Radio.EntitySystems;
/// <summary>
/// This system handles radio speakers and microphones (which together form a hand-held radio).
/// </summary>
public sealed class RadioSystem : EntitySystem
{
[UsedImplicitly]
public sealed class RadioSystem : EntitySystem
{
private readonly List<string> _messages = new();
[Dependency] private readonly INetManager _netMan = default!;
// set used to prevent radio feedback loops.
private readonly HashSet<string> _messages = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HandheldRadioComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<HandheldRadioComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<IntrinsicRadioReceiverComponent, RadioReceiveEvent>(OnIntrinsicReceive);
SubscribeLocalEvent<IntrinsicRadioTransmitterComponent, EntitySpokeEvent>(OnIntrinsicSpeak);
}
private void OnActivate(EntityUid uid, HandheldRadioComponent component, ActivateInWorldEvent args)
private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent component, EntitySpokeEvent args)
{
if (args.Handled)
if (args.Channel != null && component.Channels.Contains(args.Channel.ID))
{
SendRadioMessage(uid, args.Message, args.Channel);
args.Channel = null; // prevent duplicate messages from other listeners.
}
}
private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, RadioReceiveEvent args)
{
if (TryComp(uid, out ActorComponent? actor))
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.ConnectedClient);
}
public void SendRadioMessage(EntityUid source, string message, RadioChannelPrototype channel)
{
// TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this.
if (!_messages.Add(message))
return;
args.Handled = true;
component.Use(args.User);
var name = TryComp(source, out VoiceMaskComponent? mask) && mask.Enabled
? Identity.Name(source, EntityManager)
: MetaData(source).EntityName;
name = FormattedMessage.EscapeText(name);
// most radios are relayed to chat, so lets parse the chat message beforehand
var chatMsg = new MsgChatMessage
{
Channel = ChatChannel.Radio,
Message = message,
//Square brackets are added here to avoid issues with escaping
WrappedMessage = Loc.GetString("chat-radio-message-wrap", ("color", channel.Color), ("channel", $"\\[{channel.LocalizedName}\\]"), ("name", name), ("message", FormattedMessage.EscapeText(message)))
};
var ev = new RadioReceiveEvent(message, source, channel, chatMsg);
var attemptEv = new RadioReceiveAttemptEvent(message, source, channel);
foreach (var radio in EntityQuery<ActiveRadioComponent>())
{
// TODO map/station/range checks?
if (!radio.Channels.Contains(channel.ID))
continue;
RaiseLocalEvent(radio.Owner, attemptEv);
if (attemptEv.Cancelled)
{
attemptEv.Uncancel();
continue;
}
private void OnExamine(EntityUid uid, HandheldRadioComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(Loc.GetString("handheld-radio-component-on-examine",("frequency", component.BroadcastFrequency)));
}
public void SpreadMessage(IRadio source, EntityUid speaker, string message, RadioChannelPrototype channel)
{
if (_messages.Contains(message)) return;
_messages.Add(message);
foreach (var radio in EntityManager.EntityQuery<IRadio>(true))
{
radio.Receive(message, channel, speaker);
RaiseLocalEvent(radio.Owner, ev);
}
_messages.Remove(message);
}
}
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.Chat;
using Content.Shared.Radio;
namespace Content.Server.Radio;
public sealed class RadioReceiveEvent : EntityEventArgs
{
public readonly string Message;
public readonly EntityUid Source;
public readonly RadioChannelPrototype Channel;
public readonly MsgChatMessage ChatMsg;
public RadioReceiveEvent(string message, EntityUid source, RadioChannelPrototype channel, MsgChatMessage chatMsg)
{
Message = message;
Source = source;
Channel = channel;
ChatMsg = chatMsg;
}
}
public sealed class RadioReceiveAttemptEvent : CancellableEntityEventArgs
{
public readonly string Message;
public readonly EntityUid Source;
public readonly RadioChannelPrototype Channel;
public RadioReceiveAttemptEvent(string message, EntityUid source, RadioChannelPrototype channel)
{
Message = message;
Source = source;
Channel = channel;
}
}

View File

@@ -1,9 +1,13 @@
using Content.Server.GameTicking;
using Content.Server.Radio.Components;
using Content.Server.Radio.EntitySystems;
using Content.Shared.CCVar;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Radio;
using Content.Shared.Salvage;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
@@ -12,10 +16,6 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using System.Linq;
using Content.Server.Ghost.Components;
using Content.Server.Radio.EntitySystems;
using Content.Shared.Radio;
using Robust.Server.GameObjects;
namespace Content.Server.Salvage
{
@@ -354,11 +354,11 @@ namespace Content.Server.Salvage
private void Report(EntityUid source, string channelName, string messageKey, params (string, object)[] args)
{
if (!TryComp<IntrinsicRadioComponent>(source, out var radio)) return;
if (!TryComp<IntrinsicRadioReceiverComponent>(source, out var radio)) return;
var message = args.Length == 0 ? Loc.GetString(messageKey) : Loc.GetString(messageKey, args);
var channel = _prototypeManager.Index<RadioChannelPrototype>(channelName);
_radioSystem.SpreadMessage(radio, source, message, channel);
_radioSystem.SendRadioMessage(source, message, channel);
}
private void Transition(SalvageMagnetComponent magnet, TimeSpan currentTime)

View File

@@ -0,0 +1,13 @@
using Content.Server.Chat.Systems;
namespace Content.Server.Speech.Components;
/// <summary>
/// This component is used to relay speech events to other systems.
/// </summary>
[RegisterComponent]
public sealed class ActiveListenerComponent : Component
{
[DataField("range")]
public float Range = ChatSystem.VoiceRange;
}

View File

@@ -0,0 +1,61 @@
using Content.Server.Chat.Systems;
using Content.Server.Speech.Components;
namespace Content.Server.Speech.EntitySystems;
/// <summary>
/// This system redirects local chat messages to listening entities (e.g., radio microphones).
/// </summary>
public sealed class ListeningSystem : EntitySystem
{
[Dependency] private readonly SharedTransformSystem _xforms = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EntitySpokeEvent>(OnSpeak);
}
private void OnSpeak(EntitySpokeEvent ev)
{
PingListeners(ev.Source, ev.Message, ev.ObfuscatedMessage);
}
public void PingListeners(EntityUid source, string message, string? obfuscatedMessage)
{
// TODO whispering / audio volume? Microphone sensitivity?
// for now, whispering just arbitrarily reduces the listener's max range.
var xformQuery = GetEntityQuery<TransformComponent>();
var sourceXform = xformQuery.GetComponent(source);
var sourcePos = _xforms.GetWorldPosition(sourceXform, xformQuery);
var attemptEv = new ListenAttemptEvent(source);
var ev = new ListenEvent(message, source);
var obfuscatedEv = obfuscatedMessage == null ? null : new ListenEvent(obfuscatedMessage, source);
foreach (var (listener, xform) in EntityQuery<ActiveListenerComponent, TransformComponent>())
{
if (xform.MapID != sourceXform.MapID)
return;
// range checks
// TODO proper speech occlusion
var distance = (sourcePos - _xforms.GetWorldPosition(xform, xformQuery)).LengthSquared;
if (distance > listener.Range * listener.Range)
continue;
RaiseLocalEvent(listener.Owner, attemptEv);
if (attemptEv.Cancelled)
{
attemptEv.Uncancel();
continue;
}
if (obfuscatedEv != null && distance > ChatSystem.WhisperRange)
RaiseLocalEvent(listener.Owner, obfuscatedEv);
else
RaiseLocalEvent(listener.Owner, ev);
}
}
}

View File

@@ -0,0 +1,23 @@
namespace Content.Server.Speech;
public sealed class ListenEvent : EntityEventArgs
{
public readonly string Message;
public readonly EntityUid Source;
public ListenEvent(string message, EntityUid source)
{
Message = message;
Source = source;
}
}
public sealed class ListenAttemptEvent : CancellableEntityEventArgs
{
public readonly EntityUid Source;
public ListenAttemptEvent(EntityUid source)
{
Source = source;
}
}

View File

@@ -1,6 +1,3 @@
using Content.Server.Radio.Components;
using Content.Shared.Interaction;
using Content.Shared.Radio;
using Content.Shared.Whitelist;
namespace Content.Server.SurveillanceCamera;
@@ -10,9 +7,9 @@ namespace Content.Server.SurveillanceCamera;
/// environment. All surveillance camera monitors have speakers for this.
/// </summary>
[RegisterComponent]
[ComponentReference(typeof(IListen))]
public sealed class SurveillanceCameraMicrophoneComponent : Component, IListen
public sealed class SurveillanceCameraMicrophoneComponent : Component
{
[DataField("enabled")]
public bool Enabled { get; set; } = true;
/// <summary>
@@ -21,27 +18,9 @@ public sealed class SurveillanceCameraMicrophoneComponent : Component, IListen
/// Used to avoid things like feedback loops, or radio spam.
/// </summary>
[DataField("blacklist")]
public EntityWhitelist BlacklistedComponents { get; } = new();
public EntityWhitelist Blacklist { get; } = new();
// TODO: Once IListen is removed, **REMOVE THIS**
private SurveillanceCameraMicrophoneSystem? _microphoneSystem;
protected override void Initialize()
{
base.Initialize();
_microphoneSystem = EntitySystem.Get<SurveillanceCameraMicrophoneSystem>();
}
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);
}
[ViewVariables(VVAccess.ReadWrite)]
[DataField("range")]
public int Range { get; } = 10;
}

View File

@@ -12,7 +12,5 @@ public sealed class SurveillanceCameraSpeakerComponent : Component
[ViewVariables] public float SpeechSoundCooldown = 0.5f;
[ViewVariables] public readonly Queue<string> LastSpokenNames = new();
public TimeSpan LastSoundPlayed = TimeSpan.Zero;
}

View File

@@ -1,37 +1,97 @@
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Server.Chat.Systems;
using Content.Server.Speech;
using Content.Server.Speech.Components;
using Robust.Server.GameObjects;
using static Content.Server.Chat.Systems.ChatSystem;
namespace Content.Server.SurveillanceCamera;
public sealed class SurveillanceCameraMicrophoneSystem : EntitySystem
{
[Dependency] private SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedTransformSystem _xforms = default!;
public bool CanListen(EntityUid source, EntityUid speaker, SurveillanceCameraMicrophoneComponent? microphone = null)
public override void Initialize()
{
if (!Resolve(source, ref microphone))
{
return false;
base.Initialize();
SubscribeLocalEvent<SurveillanceCameraMicrophoneComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SurveillanceCameraMicrophoneComponent, ListenEvent>(RelayEntityMessage);
SubscribeLocalEvent<SurveillanceCameraMicrophoneComponent, ListenAttemptEvent>(CanListen);
SubscribeLocalEvent<ExpandICChatRecipientstEvent>(OnExpandRecipients);
}
return microphone.Enabled
&& !microphone.BlacklistedComponents.IsValid(speaker)
&& _interactionSystem.InRangeUnobstructed(source, speaker, range: microphone.ListenRange);
private void OnExpandRecipients(ExpandICChatRecipientstEvent ev)
{
var xformQuery = GetEntityQuery<TransformComponent>();
var sourceXform = Transform(ev.Source);
var sourcePos = _xforms.GetWorldPosition(sourceXform, xformQuery);
// This function ensures that chat popups appear on camera views that have connected microphones.
foreach (var (_, __, camera, xform) in EntityQuery<SurveillanceCameraMicrophoneComponent, ActiveListenerComponent, SurveillanceCameraComponent, TransformComponent>())
{
if (camera.ActiveViewers.Count == 0)
continue;
// get range to camera. This way wispers will still appear as obfuscated if they are too far from the camera's microphone
var range = (xform.MapID != sourceXform.MapID)
? -1
: (sourcePos - _xforms.GetWorldPosition(xform, xformQuery)).Length;
if (range < 0 || range > ev.VoiceRange)
continue;
foreach (var viewer in camera.ActiveViewers)
{
// if the player has not already received the chat message, send it to them but don't log it to the chat
// window. This is simply so that it appears in camera.
if (TryComp(viewer, out ActorComponent? actor))
ev.Recipients.TryAdd(actor.PlayerSession, new ICChatRecipientData(range, false, true));
}
public void RelayEntityMessage(EntityUid source, EntityUid speaker, string message, SurveillanceCameraComponent? camera = null)
}
}
private void OnInit(EntityUid uid, SurveillanceCameraMicrophoneComponent component, ComponentInit args)
{
if (!Resolve(source, ref camera))
if (component.Enabled)
EnsureComp<ActiveListenerComponent>(uid).Range = component.Range;
else
RemCompDeferred<ActiveListenerComponent>(uid);
}
public void CanListen(EntityUid uid, SurveillanceCameraMicrophoneComponent microphone, ListenAttemptEvent args)
{
// TODO maybe just make this a part of ActiveListenerComponent?
if (microphone.Blacklist.IsValid(args.Source))
args.Cancel();
}
public void RelayEntityMessage(EntityUid uid, SurveillanceCameraMicrophoneComponent component, ListenEvent args)
{
if (!TryComp(uid, out SurveillanceCameraComponent? camera))
return;
}
var ev = new SurveillanceCameraSpeechSendEvent(speaker, message);
var ev = new SurveillanceCameraSpeechSendEvent(args.Source, args.Message);
foreach (var monitor in camera.ActiveMonitors)
{
RaiseLocalEvent(monitor, ev);
}
}
public void SetEnabled(EntityUid uid, bool value, SurveillanceCameraMicrophoneComponent? microphone = null)
{
if (!Resolve(uid, ref microphone))
return;
if (value == microphone.Enabled)
return;
microphone.Enabled = value;
if (value)
EnsureComp<ActiveListenerComponent>(uid).Range = microphone.Range;
else
RemCompDeferred<ActiveListenerComponent>(uid);
}
}
public sealed class SurveillanceCameraSpeechSendEvent : EntityEventArgs

View File

@@ -21,7 +21,6 @@ public sealed class SurveillanceCameraSpeakerSystem : EntitySystem
public override void Initialize()
{
SubscribeLocalEvent<SurveillanceCameraSpeakerComponent, SurveillanceCameraSpeechSendEvent>(OnSpeechSent);
SubscribeLocalEvent<SurveillanceCameraSpeakerComponent, TransformSpeakerNameEvent>(OnTransformSpeech);
}
private void OnSpeechSent(EntityUid uid, SurveillanceCameraSpeakerComponent component,
@@ -71,15 +70,11 @@ public sealed class SurveillanceCameraSpeakerSystem : EntitySystem
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);
}
var name = Loc.GetString("speech-name-relay", ("speaker", Name(uid)),
("originalName", nameEv.Name));
private void OnTransformSpeech(EntityUid uid, SurveillanceCameraSpeakerComponent component,
TransformSpeakerNameEvent args)
{
args.Name = Loc.GetString("surveillance-camera-microphone-message", ("speaker", Name(uid)),
("originalName", component.LastSpokenNames.Dequeue()));
var hideGlobalGhostChat = true; // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios
_chatSystem.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Speak, false, hideGlobalGhostChat, nameOverride: name);
}
}

View File

@@ -132,6 +132,8 @@ namespace Content.Server.Zombies
return;
if (_robustRandom.Prob(0.5f)) //this message is never seen by players so it just says this for admins
// What? Is this REALLY the best way we have of letting admins know there are zombies in a round?
// [automated maintainer groan]
_chat.TrySendInGameICMessage(uid, "[automated zombie groan]", InGameICChatType.Speak, false);
else
_vocal.TryScream(uid);

View File

@@ -1,4 +1,4 @@
using Content.Shared.Tag;
using Content.Shared.Tag;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
@@ -26,6 +26,7 @@ namespace Content.Shared.Whitelist
/// Component names that are allowed in the whitelist.
/// </summary>
[DataField("components")] public string[]? Components = null;
// TODO yaml validation
[NonSerialized]
private List<ComponentRegistration>? _registrations = null;

View File

@@ -0,0 +1,2 @@
# used by camera microphones and hand-held radios
speech-name-relay = {$speaker} ({$originalName})

View File

@@ -1 +0,0 @@
surveillance-camera-microphone-message = {$speaker} ({$originalName})

View File

@@ -23,13 +23,13 @@
- Flare
- CableCoil
- CigPack
- Radio
components:
- AirlockPainter
- SignalLinker
- RCD
- RCDAmmo
- Welder
- Radio
- PowerCell
- type: ItemMapper
mapLayers:
@@ -86,6 +86,7 @@
- Powerdrill
- JawsOfLife
- CigPack
- Radio
components:
- AirlockPainter
- SignalLinker
@@ -93,7 +94,6 @@
- RCDAmmo
- Welder
- Flash
- Radio
- Handcuff
- PowerCell
- type: ItemMapper
@@ -229,9 +229,9 @@
- CigPack
- Pill
- PillCanister
- Radio
components:
- Hypospray
- Radio
- Injector
- type: ItemMapper
mapLayers:
@@ -338,6 +338,7 @@
- CigPack
- Taser
- SecBeltEquip
- Radio
components:
- Stunbaton
- FlashOnTrigger

View File

@@ -29,7 +29,8 @@
- type: Examiner
skipChecks: true
- type: Ghost
- type: IntrinsicRadio
- type: IntrinsicRadioReceiver
- type: ActiveRadio
channels:
- Common
- Command

View File

@@ -1,16 +1,12 @@
- type: entity
name: radio
parent: BaseItem
id: RadioBase
abstract: true
- type: entity
name: handheld radio
description: A handy handheld radio.
parent: RadioBase
parent: BaseItem
id: RadioHandheld
components:
- type: Radio
- type: RadioMicrophone
- type: RadioSpeaker
- type: Speech
- type: Sprite
sprite: Objects/Devices/communication.rsi
layers:
@@ -20,3 +16,6 @@
- type: Item
sprite: Objects/Devices/communication.rsi
heldPrefix: walkietalkie
- type: Tag
tags:
- Radio

View File

@@ -33,7 +33,8 @@
event: !type:OpenUiActionEvent
key: enum.InstrumentUiKey.Key
- type: Examiner
- type: IntrinsicRadio
- type: IntrinsicRadioReceiver
- type: ActiveRadio
channels:
- Common
- type: DoAfter

View File

@@ -54,7 +54,8 @@
- type: Rotatable
- type: Transform
noRot: false
- type: IntrinsicRadio
- type: IntrinsicRadioReceiver
- type: ActiveRadio
channels:
- Supply
- type: SalvageMagnet

View File

@@ -27,6 +27,9 @@
components:
- SurveillanceCamera
- SurveillanceCameraMonitor
- RadioSpeaker
- type: ActiveListener
range: 10
- type: UserInterface
interfaces:
- key: enum.SurveillanceCameraSetupUiKey.Camera

View File

@@ -437,6 +437,9 @@
- type: Tag
id: PussyWagonKeys
- type: Tag
id: Radio
- type: Tag
id: RawMaterial