IListener and IRadio purge (#11980)
This commit is contained in:
@@ -37,7 +37,8 @@ namespace Content.Server.Chat.Commands
|
|||||||
if (string.IsNullOrEmpty(message))
|
if (string.IsNullOrEmpty(message))
|
||||||
return;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ namespace Content.Server.Chat.Commands
|
|||||||
if (string.IsNullOrEmpty(message))
|
if (string.IsNullOrEmpty(message))
|
||||||
return;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Content.Server.Chat.Systems;
|
using Content.Server.Chat.Systems;
|
||||||
using Content.Shared.Administration;
|
using Content.Shared.Administration;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Console;
|
using Robust.Shared.Console;
|
||||||
@@ -37,7 +37,8 @@ namespace Content.Server.Chat.Commands
|
|||||||
if (string.IsNullOrEmpty(message))
|
if (string.IsNullOrEmpty(message))
|
||||||
return;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ namespace Content.Server.Chat.Managers
|
|||||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
||||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(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}");
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin announcement from {message}: {message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ namespace Content.Server.Chat.Managers
|
|||||||
_netManager.ServerSendMessage(msg, client);
|
_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();
|
var msg = new MsgChatMessage();
|
||||||
msg.Channel = channel;
|
msg.Channel = channel;
|
||||||
@@ -229,7 +229,7 @@ namespace Content.Server.Chat.Managers
|
|||||||
{
|
{
|
||||||
msg.MessageColorOverride = colorOverride.Value;
|
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,
|
public void ChatMessageToManyFiltered(Filter filter, ChatChannel channel, string message, string wrappedMessage, EntityUid source,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace Content.Server.Chat.Managers
|
|||||||
void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat,
|
void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat,
|
||||||
INetChannel client, Color? colorOverride = null);
|
INetChannel client, Color? colorOverride = null);
|
||||||
void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat,
|
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 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);
|
void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, Color? colorOverride = null);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Content.Server.Headset;
|
using Content.Server.Radio.Components;
|
||||||
using Content.Shared.Radio;
|
using Content.Shared.Radio;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
@@ -76,12 +76,9 @@ public sealed partial class ChatSystem
|
|||||||
// Re-capitalize message since we removed the prefix.
|
// Re-capitalize message since we removed the prefix.
|
||||||
message = SanitizeMessageCapital(message);
|
message = SanitizeMessageCapital(message);
|
||||||
|
|
||||||
if (_inventory.TryGetSlotEntity(source, "ears", out var entityUid) &&
|
var hasHeadset = _inventory.TryGetSlotEntity(source, "ears", out var entityUid) && HasComp<HeadsetComponent>(entityUid);
|
||||||
TryComp(entityUid, out HeadsetComponent? headset))
|
|
||||||
{
|
if (!hasHeadset && !HasComp<IntrinsicRadioTransmitterComponent>(source))
|
||||||
headset.RadioRequested = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
_popup.PopupEntity(Loc.GetString("chat-manager-no-headset-on-message"), source, Filter.Entities(source));
|
_popup.PopupEntity(Loc.GetString("chat-manager-no-headset-on-message"), source, Filter.Entities(source));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ using Robust.Shared.Players;
|
|||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
using Content.Server.Speech.EntitySystems;
|
||||||
|
using Content.Shared.Radio;
|
||||||
|
|
||||||
namespace Content.Server.Chat.Systems;
|
namespace Content.Server.Chat.Systems;
|
||||||
|
|
||||||
@@ -48,15 +50,14 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||||
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||||
[Dependency] private readonly ListeningSystem _listener = default!;
|
|
||||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||||
[Dependency] private readonly PopupSystem _popup = default!;
|
[Dependency] private readonly PopupSystem _popup = default!;
|
||||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||||
|
|
||||||
private const int VoiceRange = 10; // how far voice goes in world units
|
public const int VoiceRange = 10; // how far voice goes in world units
|
||||||
private const int WhisperRange = 2; // how far whisper goes in world units
|
public const int WhisperRange = 2; // how far whisper goes in world units
|
||||||
private const string DefaultAnnouncementSound = "/Audio/Announcements/announce.ogg";
|
public const string DefaultAnnouncementSound = "/Audio/Announcements/announce.ogg";
|
||||||
|
|
||||||
private bool _loocEnabled = true;
|
private bool _loocEnabled = true;
|
||||||
private bool _deadLoocEnabled = false;
|
private bool _deadLoocEnabled = false;
|
||||||
@@ -106,9 +107,19 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
_configurationManager.SetCVar(CCVars.OocEnabled, true);
|
_configurationManager.SetCVar(CCVars.OocEnabled, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReSharper disable once InconsistentNaming
|
/// <summary>
|
||||||
public void TrySendInGameICMessage(EntityUid source, string message, InGameICChatType desiredType, bool hideChat,
|
/// Sends an in-character chat message to relevant clients.
|
||||||
IConsoleShell? shell = null, IPlayerSession? player = null)
|
/// </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))
|
if (HasComp<GhostComponent>(source))
|
||||||
{
|
{
|
||||||
@@ -126,6 +137,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
if (!CanSendInGame(message, shell, player))
|
if (!CanSendInGame(message, shell, player))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
hideGlobalGhostChat |= hideChat;
|
||||||
bool shouldCapitalize = (desiredType != InGameICChatType.Emote);
|
bool shouldCapitalize = (desiredType != InGameICChatType.Emote);
|
||||||
bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation);
|
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.
|
// Was there an emote in the message? If so, send it.
|
||||||
if (player != null && emoteStr != message && emoteStr != null)
|
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.
|
// This can happen if the entire string is sanitized out.
|
||||||
@@ -145,13 +157,13 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
switch (desiredType)
|
switch (desiredType)
|
||||||
{
|
{
|
||||||
case InGameICChatType.Speak:
|
case InGameICChatType.Speak:
|
||||||
SendEntitySpeak(source, message, hideChat);
|
SendEntitySpeak(source, message, hideChat, hideGlobalGhostChat, nameOverride);
|
||||||
break;
|
break;
|
||||||
case InGameICChatType.Whisper:
|
case InGameICChatType.Whisper:
|
||||||
SendEntityWhisper(source, message, hideChat);
|
SendEntityWhisper(source, message, hideChat, hideGlobalGhostChat, null, nameOverride);
|
||||||
break;
|
break;
|
||||||
case InGameICChatType.Emote:
|
case InGameICChatType.Emote:
|
||||||
SendEntityEmote(source, message, hideChat);
|
SendEntityEmote(source, message, hideChat, hideGlobalGhostChat, nameOverride);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +257,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
|
|
||||||
#region Private API
|
#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))
|
if (!_actionBlocker.CanSpeak(source))
|
||||||
return;
|
return;
|
||||||
@@ -254,30 +266,38 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
|
|
||||||
if (channel != null)
|
if (channel != null)
|
||||||
{
|
{
|
||||||
_listener.PingListeners(source, message, channel);
|
SendEntityWhisper(source, message, hideChat, hideGlobalGhostChat, channel, nameOverride);
|
||||||
SendEntityWhisper(source, message, hideChat);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
|
|
||||||
RaiseLocalEvent(source, nameEv);
|
|
||||||
var name = FormattedMessage.EscapeText(nameEv.Name);
|
|
||||||
|
|
||||||
message = TransformSpeech(source, message);
|
message = TransformSpeech(source, message);
|
||||||
if (message.Length == 0)
|
if (message.Length == 0)
|
||||||
return;
|
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",
|
var wrappedMessage = Loc.GetString("chat-manager-entity-say-wrap-message",
|
||||||
("entityName", name), ("message", FormattedMessage.EscapeText(message)));
|
("entityName", name), ("message", FormattedMessage.EscapeText(message)));
|
||||||
|
|
||||||
SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, hideChat);
|
SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, hideChat, hideGlobalGhostChat);
|
||||||
_listener.PingListeners(source, message, null);
|
|
||||||
|
|
||||||
var ev = new EntitySpokeEvent(message);
|
var ev = new EntitySpokeEvent(source, message, channel, null);
|
||||||
RaiseLocalEvent(source, ev);
|
RaiseLocalEvent(source, ev, true);
|
||||||
|
|
||||||
// To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc.
|
// 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;
|
return;
|
||||||
|
|
||||||
if (originalMessage == message)
|
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}.");
|
_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))
|
if (!_actionBlocker.CanSpeak(source))
|
||||||
return;
|
return;
|
||||||
@@ -297,48 +317,47 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
|
|
||||||
var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f);
|
var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f);
|
||||||
|
|
||||||
var transformSource = Transform(source);
|
// get the entity's apparent name (if no override provided).
|
||||||
var sourceCoords = transformSource.Coordinates;
|
string name;
|
||||||
|
if (nameOverride != null)
|
||||||
|
{
|
||||||
|
name = nameOverride;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
|
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
|
||||||
RaiseLocalEvent(source, nameEv);
|
RaiseLocalEvent(source, nameEv);
|
||||||
|
name = nameEv.Name;
|
||||||
|
}
|
||||||
|
name = FormattedMessage.EscapeText(name);
|
||||||
|
|
||||||
var name = FormattedMessage.EscapeText(nameEv.Name);
|
foreach (var (session, data) in GetRecipients(source, VoiceRange))
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
if (session.AttachedEntity is not { Valid: true } playerEntity)
|
if (session.AttachedEntity is not { Valid: true } playerEntity)
|
||||||
continue;
|
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) ||
|
if (data.Range <= WhisperRange)
|
||||||
ghosts.HasComponent(playerEntity))
|
|
||||||
{
|
{
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
|
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
|
||||||
("entityName", name), ("message", FormattedMessage.EscapeText(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
|
else
|
||||||
{
|
{
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
|
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
|
||||||
("entityName", name), ("message", FormattedMessage.EscapeText(obfuscatedMessage)));
|
("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);
|
session.ConnectedClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ev = new EntitySpokeEvent(message);
|
var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage);
|
||||||
RaiseLocalEvent(source, ev, false);
|
RaiseLocalEvent(source, ev, true);
|
||||||
|
|
||||||
if (originalMessage == message)
|
if (originalMessage == message)
|
||||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}: {originalMessage}.");
|
_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}.");
|
_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;
|
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.
|
// Emotes use Identity.Name, since it doesn't actually involve your voice at all.
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
|
var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
|
||||||
("entityName", name),
|
("entityName", name),
|
||||||
("message", FormattedMessage.EscapeText(action)));
|
("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}");
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,7 +395,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
("entityName", name),
|
("entityName", name),
|
||||||
("message", FormattedMessage.EscapeText(message)));
|
("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}");
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,11 +431,13 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a chat message to the given players in range of the source entity.
|
/// Sends a chat message to the given players in range of the source entity.
|
||||||
/// </summary>
|
/// </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>();
|
foreach (var (session, data) in GetRecipients(source, VoiceRange))
|
||||||
ClientDistanceToList(source, VoiceRange, sessions);
|
{
|
||||||
_chatManager.ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, sessions.Select(s => s.ConnectedClient).ToList());
|
var entHideChat = data.HideChatOverride ?? (hideChat || hideGlobalGhostChat && data.Observer && data.Range < 0);
|
||||||
|
_chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.ConnectedClient);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -502,8 +524,14 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
return message;
|
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 ghosts = GetEntityQuery<GhostComponent>();
|
||||||
var xforms = GetEntityQuery<TransformComponent>();
|
var xforms = GetEntityQuery<TransformComponent>();
|
||||||
|
|
||||||
@@ -518,13 +546,28 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
|
|
||||||
var transformEntity = xforms.GetComponent(playerEntity);
|
var transformEntity = xforms.GetComponent(playerEntity);
|
||||||
|
|
||||||
if (transformEntity.MapID != sourceMapId ||
|
if (transformEntity.MapID != sourceMapId)
|
||||||
!ghosts.HasComponent(playerEntity) &&
|
|
||||||
!sourceCoords.InRange(EntityManager, transformEntity.Coordinates, voiceRange))
|
|
||||||
continue;
|
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)
|
private string ObfuscateMessageReadability(string message, float chance)
|
||||||
@@ -550,6 +593,14 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
#endregion
|
#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 sealed class TransformSpeakerNameEvent : EntityEventArgs
|
||||||
{
|
{
|
||||||
public EntityUid Sender;
|
public EntityUid Sender;
|
||||||
@@ -582,11 +633,22 @@ public sealed class TransformSpeechEvent : EntityEventArgs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EntitySpokeEvent : EntityEventArgs
|
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;
|
Message = message;
|
||||||
|
Channel = channel;
|
||||||
|
ObfuscatedMessage = obfuscatedMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ namespace Content.Server.Disposal.Unit.EntitySystems
|
|||||||
var newPosition = destination * progress;
|
var newPosition = destination * progress;
|
||||||
|
|
||||||
// This is some supreme shit code.
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
namespace Content.Server.Explosion.Components
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a trigger when the keyphrase is heard
|
/// Sends a trigger when the keyphrase is heard
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
[ComponentReference(typeof(IListen))]
|
public sealed class TriggerOnVoiceComponent : Component
|
||||||
public sealed class TriggerOnVoiceComponent : Component, IListen
|
|
||||||
{
|
{
|
||||||
private SharedInteractionSystem _sharedInteractionSystem = default!;
|
public bool IsListening => IsRecording || !string.IsNullOrWhiteSpace(KeyPhrase);
|
||||||
private TriggerSystem _triggerSystem = default!;
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
[DataField("keyPhrase")]
|
[DataField("keyPhrase")]
|
||||||
@@ -26,51 +16,13 @@ namespace Content.Server.Explosion.Components
|
|||||||
[DataField("listenRange")]
|
[DataField("listenRange")]
|
||||||
public int ListenRange { get; private set; } = 4;
|
public int ListenRange { get; private set; } = 4;
|
||||||
|
|
||||||
[ViewVariables]
|
[DataField("isRecording")]
|
||||||
public bool IsRecording = false;
|
public bool IsRecording = false;
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
[DataField("minLength")]
|
[DataField("minLength")]
|
||||||
public int MinLength = 3;
|
public int MinLength = 3;
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
[DataField("maxLength")]
|
[DataField("maxLength")]
|
||||||
public int MaxLength = 50;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
using Content.Server.Explosion.Components;
|
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.Examine;
|
||||||
using Content.Shared.Interaction.Events;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Content.Shared.Verbs;
|
using Content.Shared.Verbs;
|
||||||
using Microsoft.CodeAnalysis.Options;
|
|
||||||
using Robust.Shared.GameObjects;
|
|
||||||
using Robust.Shared.Physics.Events;
|
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
using Robust.Shared.Utility;
|
|
||||||
|
|
||||||
namespace Content.Server.Explosion.EntitySystems
|
namespace Content.Server.Explosion.EntitySystems
|
||||||
{
|
{
|
||||||
@@ -16,8 +12,37 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
{
|
{
|
||||||
private void InitializeVoice()
|
private void InitializeVoice()
|
||||||
{
|
{
|
||||||
|
SubscribeLocalEvent<TriggerOnVoiceComponent, ComponentInit>(OnVoiceInit);
|
||||||
SubscribeLocalEvent<TriggerOnVoiceComponent, ExaminedEvent>(OnVoiceExamine);
|
SubscribeLocalEvent<TriggerOnVoiceComponent, ExaminedEvent>(OnVoiceExamine);
|
||||||
SubscribeLocalEvent<TriggerOnVoiceComponent, GetVerbsEvent<AlternativeVerb>>(OnVoiceGetAltVerbs);
|
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)
|
private void OnVoiceGetAltVerbs(EntityUid uid, TriggerOnVoiceComponent component, GetVerbsEvent<AlternativeVerb> args)
|
||||||
@@ -27,35 +52,74 @@ namespace Content.Server.Explosion.EntitySystems
|
|||||||
|
|
||||||
args.Verbs.Add(new AlternativeVerb()
|
args.Verbs.Add(new AlternativeVerb()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("verb-trigger-voice-record"),
|
Text = Loc.GetString(component.IsRecording ? "verb-trigger-voice-record-stop" : "verb-trigger-voice-record"),
|
||||||
Act = () => ToggleRecord(component, args.User),
|
Act = () =>
|
||||||
|
{
|
||||||
|
if (component.IsRecording)
|
||||||
|
StopRecording(component);
|
||||||
|
else
|
||||||
|
StartRecording(component, args.User);
|
||||||
|
},
|
||||||
Priority = 1
|
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
|
_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-recorded"), component.Owner, Filter.Entities(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;
|
component.IsRecording = false;
|
||||||
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-start-recording"), component.Owner, Filter.Entities(user));
|
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)
|
private void OnVoiceExamine(EntityUid uid, TriggerOnVoiceComponent component, ExaminedEvent args)
|
||||||
{
|
{
|
||||||
|
args.PushText(Loc.GetString("examine-trigger-voice"));
|
||||||
if (args.IsInDetailsRange)
|
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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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", ";")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -70,7 +70,7 @@ public sealed class MedibotInjectOperator : HTNOperator
|
|||||||
_solutionSystem.TryAddReagent(target, injectable, botComp.EmergencyMed, botComp.EmergencyMedInjectAmount, out var accepted);
|
_solutionSystem.TryAddReagent(target, injectable, botComp.EmergencyMed, botComp.EmergencyMedInjectAmount, out var accepted);
|
||||||
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
|
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
|
||||||
SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), 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;
|
return HTNOperatorStatus.Finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ public sealed class MedibotInjectOperator : HTNOperator
|
|||||||
_solutionSystem.TryAddReagent(target, injectable, botComp.StandardMed, botComp.StandardMedInjectAmount, out var accepted);
|
_solutionSystem.TryAddReagent(target, injectable, botComp.StandardMed, botComp.StandardMedInjectAmount, out var accepted);
|
||||||
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
|
_popupSystem.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, Filter.Entities(target));
|
||||||
SoundSystem.Play("/Audio/Items/hypospray.ogg", Filter.Pvs(target), 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;
|
return HTNOperatorStatus.Finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
Content.Server/Radio/Components/ActiveRadioComponent.cs
Normal file
17
Content.Server/Radio/Components/ActiveRadioComponent.cs
Normal 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();
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
Content.Server/Radio/Components/HeadsetComponent.cs
Normal file
25
Content.Server/Radio/Components/HeadsetComponent.cs
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -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" };
|
||||||
|
}
|
||||||
24
Content.Server/Radio/Components/RadioMicrophoneComponent.cs
Normal file
24
Content.Server/Radio/Components/RadioMicrophoneComponent.cs
Normal 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;
|
||||||
|
}
|
||||||
19
Content.Server/Radio/Components/RadioSpeakerComponent.cs
Normal file
19
Content.Server/Radio/Components/RadioSpeakerComponent.cs
Normal 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;
|
||||||
|
}
|
||||||
13
Content.Server/Radio/Components/WearingHeadsetComponent.cs
Normal file
13
Content.Server/Radio/Components/WearingHeadsetComponent.cs
Normal 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;
|
||||||
|
}
|
||||||
106
Content.Server/Radio/EntitySystems/HeadsetSystem.cs
Normal file
106
Content.Server/Radio/EntitySystems/HeadsetSystem.cs
Normal 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", ";")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
148
Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs
Normal file
148
Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,89 @@
|
|||||||
using System.Linq;
|
using Content.Server.Chat.Systems;
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Server.Radio.Components;
|
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 Content.Shared.Radio;
|
||||||
using JetBrains.Annotations;
|
using Robust.Server.GameObjects;
|
||||||
using Content.Shared.Interaction;
|
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]
|
[Dependency] private readonly INetManager _netMan = default!;
|
||||||
public sealed class RadioSystem : EntitySystem
|
|
||||||
{
|
// set used to prevent radio feedback loops.
|
||||||
private readonly List<string> _messages = new();
|
private readonly HashSet<string> _messages = new();
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
SubscribeLocalEvent<HandheldRadioComponent, ExaminedEvent>(OnExamine);
|
SubscribeLocalEvent<IntrinsicRadioReceiverComponent, RadioReceiveEvent>(OnIntrinsicReceive);
|
||||||
SubscribeLocalEvent<HandheldRadioComponent, ActivateInWorldEvent>(OnActivate);
|
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;
|
return;
|
||||||
|
|
||||||
args.Handled = true;
|
var name = TryComp(source, out VoiceMaskComponent? mask) && mask.Enabled
|
||||||
component.Use(args.User);
|
? 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)
|
RaiseLocalEvent(radio.Owner, ev);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_messages.Remove(message);
|
_messages.Remove(message);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
34
Content.Server/Radio/RadioReceiveEvent.cs
Normal file
34
Content.Server/Radio/RadioReceiveEvent.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
using Content.Server.GameTicking;
|
using Content.Server.GameTicking;
|
||||||
|
using Content.Server.Radio.Components;
|
||||||
|
using Content.Server.Radio.EntitySystems;
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
using Content.Shared.Examine;
|
using Content.Shared.Examine;
|
||||||
using Content.Shared.Interaction;
|
using Content.Shared.Interaction;
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
|
using Content.Shared.Radio;
|
||||||
using Content.Shared.Salvage;
|
using Content.Shared.Salvage;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Server.Maps;
|
using Robust.Server.Maps;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
using Robust.Shared.Map;
|
using Robust.Shared.Map;
|
||||||
@@ -12,10 +16,6 @@ using Robust.Shared.Prototypes;
|
|||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
using System.Linq;
|
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
|
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)
|
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 message = args.Length == 0 ? Loc.GetString(messageKey) : Loc.GetString(messageKey, args);
|
||||||
var channel = _prototypeManager.Index<RadioChannelPrototype>(channelName);
|
var channel = _prototypeManager.Index<RadioChannelPrototype>(channelName);
|
||||||
_radioSystem.SpreadMessage(radio, source, message, channel);
|
_radioSystem.SendRadioMessage(source, message, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Transition(SalvageMagnetComponent magnet, TimeSpan currentTime)
|
private void Transition(SalvageMagnetComponent magnet, TimeSpan currentTime)
|
||||||
|
|||||||
13
Content.Server/Speech/Components/ActiveListenerComponent.cs
Normal file
13
Content.Server/Speech/Components/ActiveListenerComponent.cs
Normal 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;
|
||||||
|
}
|
||||||
61
Content.Server/Speech/EntitySystems/ListeningSystem.cs
Normal file
61
Content.Server/Speech/EntitySystems/ListeningSystem.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Content.Server/Speech/ListenEvent.cs
Normal file
23
Content.Server/Speech/ListenEvent.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
using Content.Server.Radio.Components;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Radio;
|
|
||||||
using Content.Shared.Whitelist;
|
using Content.Shared.Whitelist;
|
||||||
|
|
||||||
namespace Content.Server.SurveillanceCamera;
|
namespace Content.Server.SurveillanceCamera;
|
||||||
@@ -10,9 +7,9 @@ namespace Content.Server.SurveillanceCamera;
|
|||||||
/// environment. All surveillance camera monitors have speakers for this.
|
/// environment. All surveillance camera monitors have speakers for this.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
[ComponentReference(typeof(IListen))]
|
public sealed class SurveillanceCameraMicrophoneComponent : Component
|
||||||
public sealed class SurveillanceCameraMicrophoneComponent : Component, IListen
|
|
||||||
{
|
{
|
||||||
|
[DataField("enabled")]
|
||||||
public bool Enabled { get; set; } = true;
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -21,27 +18,9 @@ public sealed class SurveillanceCameraMicrophoneComponent : Component, IListen
|
|||||||
/// Used to avoid things like feedback loops, or radio spam.
|
/// Used to avoid things like feedback loops, or radio spam.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("blacklist")]
|
[DataField("blacklist")]
|
||||||
public EntityWhitelist BlacklistedComponents { get; } = new();
|
public EntityWhitelist Blacklist { get; } = new();
|
||||||
|
|
||||||
// TODO: Once IListen is removed, **REMOVE THIS**
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("range")]
|
||||||
private SurveillanceCameraMicrophoneSystem? _microphoneSystem;
|
public int Range { get; } = 10;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,5 @@ public sealed class SurveillanceCameraSpeakerComponent : Component
|
|||||||
|
|
||||||
[ViewVariables] public float SpeechSoundCooldown = 0.5f;
|
[ViewVariables] public float SpeechSoundCooldown = 0.5f;
|
||||||
|
|
||||||
[ViewVariables] public readonly Queue<string> LastSpokenNames = new();
|
|
||||||
|
|
||||||
public TimeSpan LastSoundPlayed = TimeSpan.Zero;
|
public TimeSpan LastSoundPlayed = TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,97 @@
|
|||||||
using Content.Shared.IdentityManagement;
|
using Content.Server.Chat.Systems;
|
||||||
using Content.Shared.Interaction;
|
using Content.Server.Speech;
|
||||||
|
using Content.Server.Speech.Components;
|
||||||
|
using Robust.Server.GameObjects;
|
||||||
|
using static Content.Server.Chat.Systems.ChatSystem;
|
||||||
|
|
||||||
namespace Content.Server.SurveillanceCamera;
|
namespace Content.Server.SurveillanceCamera;
|
||||||
|
|
||||||
public sealed class SurveillanceCameraMicrophoneSystem : EntitySystem
|
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))
|
base.Initialize();
|
||||||
{
|
SubscribeLocalEvent<SurveillanceCameraMicrophoneComponent, ComponentInit>(OnInit);
|
||||||
return false;
|
SubscribeLocalEvent<SurveillanceCameraMicrophoneComponent, ListenEvent>(RelayEntityMessage);
|
||||||
|
SubscribeLocalEvent<SurveillanceCameraMicrophoneComponent, ListenAttemptEvent>(CanListen);
|
||||||
|
SubscribeLocalEvent<ExpandICChatRecipientstEvent>(OnExpandRecipients);
|
||||||
}
|
}
|
||||||
|
|
||||||
return microphone.Enabled
|
private void OnExpandRecipients(ExpandICChatRecipientstEvent ev)
|
||||||
&& !microphone.BlacklistedComponents.IsValid(speaker)
|
{
|
||||||
&& _interactionSystem.InRangeUnobstructed(source, speaker, range: microphone.ListenRange);
|
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;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var ev = new SurveillanceCameraSpeechSendEvent(speaker, message);
|
var ev = new SurveillanceCameraSpeechSendEvent(args.Source, args.Message);
|
||||||
|
|
||||||
foreach (var monitor in camera.ActiveMonitors)
|
foreach (var monitor in camera.ActiveMonitors)
|
||||||
{
|
{
|
||||||
RaiseLocalEvent(monitor, ev);
|
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
|
public sealed class SurveillanceCameraSpeechSendEvent : EntityEventArgs
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ public sealed class SurveillanceCameraSpeakerSystem : EntitySystem
|
|||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
SubscribeLocalEvent<SurveillanceCameraSpeakerComponent, SurveillanceCameraSpeechSendEvent>(OnSpeechSent);
|
SubscribeLocalEvent<SurveillanceCameraSpeakerComponent, SurveillanceCameraSpeechSendEvent>(OnSpeechSent);
|
||||||
SubscribeLocalEvent<SurveillanceCameraSpeakerComponent, TransformSpeakerNameEvent>(OnTransformSpeech);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSpeechSent(EntityUid uid, SurveillanceCameraSpeakerComponent component,
|
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));
|
var nameEv = new TransformSpeakerNameEvent(args.Speaker, Name(args.Speaker));
|
||||||
RaiseLocalEvent(args.Speaker, nameEv);
|
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,
|
var hideGlobalGhostChat = true; // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios
|
||||||
TransformSpeakerNameEvent args)
|
_chatSystem.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Speak, false, hideGlobalGhostChat, nameOverride: name);
|
||||||
{
|
|
||||||
args.Name = Loc.GetString("surveillance-camera-microphone-message", ("speaker", Name(uid)),
|
|
||||||
("originalName", component.LastSpokenNames.Dequeue()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ namespace Content.Server.Zombies
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (_robustRandom.Prob(0.5f)) //this message is never seen by players so it just says this for admins
|
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);
|
_chat.TrySendInGameICMessage(uid, "[automated zombie groan]", InGameICChatType.Speak, false);
|
||||||
else
|
else
|
||||||
_vocal.TryScream(uid);
|
_vocal.TryScream(uid);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Content.Shared.Tag;
|
using Content.Shared.Tag;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
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.
|
/// Component names that are allowed in the whitelist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("components")] public string[]? Components = null;
|
[DataField("components")] public string[]? Components = null;
|
||||||
|
// TODO yaml validation
|
||||||
|
|
||||||
[NonSerialized]
|
[NonSerialized]
|
||||||
private List<ComponentRegistration>? _registrations = null;
|
private List<ComponentRegistration>? _registrations = null;
|
||||||
|
|||||||
2
Resources/Locale/en-US/speech/speech-name-relay.ftl
Normal file
2
Resources/Locale/en-US/speech/speech-name-relay.ftl
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# used by camera microphones and hand-held radios
|
||||||
|
speech-name-relay = {$speaker} ({$originalName})
|
||||||
@@ -1 +0,0 @@
|
|||||||
surveillance-camera-microphone-message = {$speaker} ({$originalName})
|
|
||||||
@@ -23,13 +23,13 @@
|
|||||||
- Flare
|
- Flare
|
||||||
- CableCoil
|
- CableCoil
|
||||||
- CigPack
|
- CigPack
|
||||||
|
- Radio
|
||||||
components:
|
components:
|
||||||
- AirlockPainter
|
- AirlockPainter
|
||||||
- SignalLinker
|
- SignalLinker
|
||||||
- RCD
|
- RCD
|
||||||
- RCDAmmo
|
- RCDAmmo
|
||||||
- Welder
|
- Welder
|
||||||
- Radio
|
|
||||||
- PowerCell
|
- PowerCell
|
||||||
- type: ItemMapper
|
- type: ItemMapper
|
||||||
mapLayers:
|
mapLayers:
|
||||||
@@ -86,6 +86,7 @@
|
|||||||
- Powerdrill
|
- Powerdrill
|
||||||
- JawsOfLife
|
- JawsOfLife
|
||||||
- CigPack
|
- CigPack
|
||||||
|
- Radio
|
||||||
components:
|
components:
|
||||||
- AirlockPainter
|
- AirlockPainter
|
||||||
- SignalLinker
|
- SignalLinker
|
||||||
@@ -93,7 +94,6 @@
|
|||||||
- RCDAmmo
|
- RCDAmmo
|
||||||
- Welder
|
- Welder
|
||||||
- Flash
|
- Flash
|
||||||
- Radio
|
|
||||||
- Handcuff
|
- Handcuff
|
||||||
- PowerCell
|
- PowerCell
|
||||||
- type: ItemMapper
|
- type: ItemMapper
|
||||||
@@ -229,9 +229,9 @@
|
|||||||
- CigPack
|
- CigPack
|
||||||
- Pill
|
- Pill
|
||||||
- PillCanister
|
- PillCanister
|
||||||
|
- Radio
|
||||||
components:
|
components:
|
||||||
- Hypospray
|
- Hypospray
|
||||||
- Radio
|
|
||||||
- Injector
|
- Injector
|
||||||
- type: ItemMapper
|
- type: ItemMapper
|
||||||
mapLayers:
|
mapLayers:
|
||||||
@@ -338,6 +338,7 @@
|
|||||||
- CigPack
|
- CigPack
|
||||||
- Taser
|
- Taser
|
||||||
- SecBeltEquip
|
- SecBeltEquip
|
||||||
|
- Radio
|
||||||
components:
|
components:
|
||||||
- Stunbaton
|
- Stunbaton
|
||||||
- FlashOnTrigger
|
- FlashOnTrigger
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
- type: Examiner
|
- type: Examiner
|
||||||
skipChecks: true
|
skipChecks: true
|
||||||
- type: Ghost
|
- type: Ghost
|
||||||
- type: IntrinsicRadio
|
- type: IntrinsicRadioReceiver
|
||||||
|
- type: ActiveRadio
|
||||||
channels:
|
channels:
|
||||||
- Common
|
- Common
|
||||||
- Command
|
- Command
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
- type: entity
|
|
||||||
name: radio
|
|
||||||
parent: BaseItem
|
|
||||||
id: RadioBase
|
|
||||||
abstract: true
|
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
name: handheld radio
|
name: handheld radio
|
||||||
description: A handy handheld radio.
|
description: A handy handheld radio.
|
||||||
parent: RadioBase
|
parent: BaseItem
|
||||||
id: RadioHandheld
|
id: RadioHandheld
|
||||||
components:
|
components:
|
||||||
- type: Radio
|
- type: RadioMicrophone
|
||||||
|
- type: RadioSpeaker
|
||||||
|
- type: Speech
|
||||||
- type: Sprite
|
- type: Sprite
|
||||||
sprite: Objects/Devices/communication.rsi
|
sprite: Objects/Devices/communication.rsi
|
||||||
layers:
|
layers:
|
||||||
@@ -20,3 +16,6 @@
|
|||||||
- type: Item
|
- type: Item
|
||||||
sprite: Objects/Devices/communication.rsi
|
sprite: Objects/Devices/communication.rsi
|
||||||
heldPrefix: walkietalkie
|
heldPrefix: walkietalkie
|
||||||
|
- type: Tag
|
||||||
|
tags:
|
||||||
|
- Radio
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
event: !type:OpenUiActionEvent
|
event: !type:OpenUiActionEvent
|
||||||
key: enum.InstrumentUiKey.Key
|
key: enum.InstrumentUiKey.Key
|
||||||
- type: Examiner
|
- type: Examiner
|
||||||
- type: IntrinsicRadio
|
- type: IntrinsicRadioReceiver
|
||||||
|
- type: ActiveRadio
|
||||||
channels:
|
channels:
|
||||||
- Common
|
- Common
|
||||||
- type: DoAfter
|
- type: DoAfter
|
||||||
|
|||||||
@@ -54,7 +54,8 @@
|
|||||||
- type: Rotatable
|
- type: Rotatable
|
||||||
- type: Transform
|
- type: Transform
|
||||||
noRot: false
|
noRot: false
|
||||||
- type: IntrinsicRadio
|
- type: IntrinsicRadioReceiver
|
||||||
|
- type: ActiveRadio
|
||||||
channels:
|
channels:
|
||||||
- Supply
|
- Supply
|
||||||
- type: SalvageMagnet
|
- type: SalvageMagnet
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
components:
|
components:
|
||||||
- SurveillanceCamera
|
- SurveillanceCamera
|
||||||
- SurveillanceCameraMonitor
|
- SurveillanceCameraMonitor
|
||||||
|
- RadioSpeaker
|
||||||
|
- type: ActiveListener
|
||||||
|
range: 10
|
||||||
- type: UserInterface
|
- type: UserInterface
|
||||||
interfaces:
|
interfaces:
|
||||||
- key: enum.SurveillanceCameraSetupUiKey.Camera
|
- key: enum.SurveillanceCameraSetupUiKey.Camera
|
||||||
|
|||||||
@@ -437,6 +437,9 @@
|
|||||||
- type: Tag
|
- type: Tag
|
||||||
id: PussyWagonKeys
|
id: PussyWagonKeys
|
||||||
|
|
||||||
|
- type: Tag
|
||||||
|
id: Radio
|
||||||
|
|
||||||
- type: Tag
|
- type: Tag
|
||||||
id: RawMaterial
|
id: RawMaterial
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user