diff --git a/Content.Server/Chat/Managers/ChatSanitizationManager.cs b/Content.Server/Chat/Managers/ChatSanitizationManager.cs index 106e5313e6e..d2ad0ce0ff2 100644 --- a/Content.Server/Chat/Managers/ChatSanitizationManager.cs +++ b/Content.Server/Chat/Managers/ChatSanitizationManager.cs @@ -91,6 +91,7 @@ private static readonly (Regex regex, string emoteKey)[] ShorthandToEmote = Entry("[':", "chatsan-tearfully-smiles"), Entry("('=", "chatsan-tearfully-smiles"), Entry("['=", "chatsan-tearfully-smiles"), + Entry("?", "chatsan-confused"), //DeltaV ]; [Dependency] private readonly IConfigurationManager _configurationManager = default!; diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 89319230f86..dcbd5dada0d 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -40,6 +40,7 @@ using Robust.Shared.Random; using Robust.Shared.Replays; using Robust.Shared.Utility; +using Content.Shared._DV.Chat; // DeltaV - chat enhancements namespace Content.Server.Chat.Systems; @@ -248,7 +249,7 @@ public void TrySendInGameICMessage( // Was there an emote in the message? If so, send it. if (player != null && emoteStr != message && emoteStr != null) { - SendEntityEmote(source, emoteStr, range, nameOverride, ignoreActionBlocker); + SendEntityEmote(source, emoteStr, range, nameOverride, null, ignoreActionBlocker); // DeltaV - Had to change up for SendEntityEmote } // This can happen if the entire string is sanitized out. @@ -311,9 +312,18 @@ public void TrySendInGameICMessage( case InGameICChatType.Whisper: SendEntityWhisper(source, message, range, null, nameOverride, language, hideLog, ignoreActionBlocker); break; - case InGameICChatType.Emote: - SendEntityEmote(source, message, range, nameOverride, hideLog, ignoreActionBlocker: ignoreActionBlocker); - break; + case InGameICChatType.Emote: // DeltaV - Emote now has different types of emotes. + var type = ProcessEmoteMessage(source, message, out var modMessage); + if (type == EmoteType.Audible || type == EmoteType.AudiblePossessive) + { + if (checkRadioPrefix && TryProccessRadioMessage(source, modMessage, out var outputMessage, out var channel, capitalize: false)) + SendAudibleEntityEmote(source, outputMessage, range, nameOverride, channel, type, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker); + else + SendAudibleEntityEmote(source, modMessage, range, nameOverride, null, type, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker); + } + else + SendEntityEmote(source, modMessage, range, nameOverride, type, hideLog, ignoreActionBlocker: ignoreActionBlocker); + break; // DeltaV - End // Floofstation section case InGameICChatType.Subtle: SendEntitySubtle(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker); @@ -819,6 +829,7 @@ protected override void SendEntityEmote( string action, ChatTransmitRange range, string? nameOverride, + EmoteType? emoteType, // DeltaV bool hideLog = false, bool checkEmote = true, bool ignoreActionBlocker = false, @@ -832,23 +843,86 @@ protected override void SendEntityEmote( var ent = Identity.Entity(source, EntityManager); string name = FormattedMessage.EscapeText(nameOverride ?? Name(ent)); + // Begin DeltaV + string wrappedMessage; + // Emotes use Identity.Name, since it doesn't actually involve your voice at all. - var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message", - ("entityName", name), - ("entity", ent), - ("message", FormattedMessage.RemoveMarkupOrThrow(action))); + if (emoteType == EmoteType.Possessive) // DeltaV - Emote types now get checked. + wrappedMessage = Loc.GetString("chat-manager-entity-me-possessive-wrap-message", + ("entityName", name), + ("entity", ent), + ("message", FormattedMessage.RemoveMarkupOrThrow(action))); + else + wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message", + ("entityName", name), + ("entity", ent), + ("message", FormattedMessage.RemoveMarkupOrThrow(action))); + // End DeltaV + + if (checkEmote && + !TryEmoteChatInput(source, action)) + return; + + SendInVoiceRange(ChatChannel.Emotes, name, action, wrappedMessage, obfuscated: "", obfuscatedWrappedMessage: "", source, range, author); + if (!hideLog) + if (name != Name(source)) + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}"); + else + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}"); + } + + // DeltaV - Added this to differentiate between emotes that can be heard over radio and those which can't. + protected override void SendAudibleEntityEmote( + EntityUid source, + string action, + ChatTransmitRange range, + string? nameOverride, + RadioChannelPrototype? channel, + EmoteType? emoteType, + bool hideLog = false, + bool checkEmote = true, + bool ignoreActionBlocker = false, + NetUserId? author = null + ) + { + if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker) + return; + + EmoteType type = emoteType ?? EmoteType.Audible; + + // get the entity's apparent name (if no override provided). + var ent = Identity.Entity(source, EntityManager); + string name = FormattedMessage.EscapeText(nameOverride ?? Name(ent)); + + string wrappedMessage; + + // Audible emotes use Identity.Name, since that is the status quo. Emotes like scream doesn't currently reveal identities so this won't either. [This may be changed with feedback as you can audibly emote over radios] + if (emoteType == EmoteType.AudiblePossessive) + wrappedMessage = Loc.GetString("chat-manager-entity-me-audible-possessive-wrap-message", + ("entityName", name), + ("entity", ent), + ("message", FormattedMessage.RemoveMarkupOrThrow(action))); + else + wrappedMessage = Loc.GetString("chat-manager-entity-me-audible-wrap-message", + ("entityName", name), + ("entity", ent), + ("message", FormattedMessage.RemoveMarkupOrThrow(action))); if (checkEmote && !TryEmoteChatInput(source, action)) return; SendInVoiceRange(ChatChannel.Emotes, name, action, wrappedMessage, obfuscated: "", obfuscatedWrappedMessage: "", source, range, author); + + var ev = new EntityAudiblyEmotedEvent(source, action, channel, type); + RaiseLocalEvent(source, ref ev, true); if (!hideLog) if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}"); else _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}"); } + // DeltaV - End // ReSharper disable once InconsistentNaming private void SendLOOC(EntityUid source, ICommonSession player, string message, bool hideChat) diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index 7ee80a4a749..08827efc3aa 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -10,6 +10,7 @@ using Content.Shared.Radio.EntitySystems; using Robust.Shared.Network; using Robust.Shared.Player; +using Content.Shared._DV.Chat; namespace Content.Server.Radio.EntitySystems; @@ -27,6 +28,7 @@ public override void Initialize() SubscribeLocalEvent(OnKeysChanged); SubscribeLocalEvent(OnSpeak); + SubscribeLocalEvent(OnAudibleEmote); // DeltaV SubscribeLocalEvent(OnEmpPulse); } @@ -51,13 +53,43 @@ private void UpdateRadioChannels(EntityUid uid, HeadsetComponent headset, Encryp EnsureComp(uid).Channels = new(keyHolder.Channels); } + // DeltaV + // WARNING - Be very careful when modifying this method. + // The implementation right now has null forgiving operatiors in OnSpeak() and OnAudibleEmote() since this only returns true if channel is not null + private bool CheckRadioCapable(EntityUid uid, WearingHeadsetComponent headset, RadioChannelPrototype? channel) + { + if (channel != null + && TryComp(headset.Headset, out EncryptionKeyHolderComponent? keys) + && keys.Channels.Contains(channel.ID)) + { + // if (!TryComp(uid, out var hands) || hands.Count < 1 || + // TryComp(uid, out var cuffable) && _cuffable.IsCuffed((uid, cuffable))) + // { + // _popup.PopupEntity(Loc.GetString("headset-cant-reach"), uid, uid, PopupType.SmallCaution); + // return false; + // } + + return true; + } + + return false; + } + + private void OnAudibleEmote(EntityUid uid, WearingHeadsetComponent component, EntityAudiblyEmotedEvent args) + { + if (CheckRadioCapable(uid, component, args.Channel)) + { + _radio.SendRadioMessage(uid, args.Message, args.Channel!, component.Headset, emType: args.Type); + args.Channel = null; + } + } + // DeltaV - End + private void OnSpeak(EntityUid uid, WearingHeadsetComponent component, EntitySpokeEvent args) { - if (args.Channel != null - && TryComp(component.Headset, out EncryptionKeyHolderComponent? keys) - && keys.Channels.Contains(args.Channel.ID)) + if (CheckRadioCapable(uid, component, args.Channel)) // DeltaV - Put all the radio checks into the CheckRadioCapable() method. { - _radio.SendRadioMessage(uid, args.Message, args.Channel, component.Headset); + _radio.SendRadioMessage(uid, args.Message, args.Channel!, component.Headset); // DeltaV - Made the args.Channel null ignorant as CheckRadioCapable() guarantees it not null. args.Channel = null; // prevent duplicate messages from other listeners. } } diff --git a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs index 4f9cff4a4c5..d7fcfc1f127 100644 --- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs @@ -13,6 +13,7 @@ using Content.Shared.Power; using Content.Shared.Radio; using Content.Shared.Chat; +using Content.Shared._DV.Chat; //Delta-V using Content.Shared.UserInterface; // Nuclear-14 using Content.Shared._NC.Radio; // Nuclear-14 using Robust.Server.GameObjects; // Nuclear-14 @@ -207,6 +208,13 @@ private void OnReceiveRadio(EntityUid uid, RadioSpeakerComponent component, ref // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios var message = args.OriginalChatMsg.Message; // The chat system will handle the rest and re-obfuscate if needed. + + // Triad - Prevent speaking emotes + if (args.emType == EmoteType.Audible || args.emType == EmoteType.AudiblePossessive) + {_chat.TrySendInGameICMessage(uid, message, InGameICChatType.Emote, ChatTransmitRange.GhostRangeLimitNoAdminCheck, + nameOverride: name, checkRadioPrefix: false, languageOverride: args.Language); + return;} + _chat.TrySendInGameICMessage(uid, message, component.OutputChatType, ChatTransmitRange.GhostRangeLimitNoAdminCheck, nameOverride: name, checkRadioPrefix: false, languageOverride: args.Language); // Einstein Engines - Languages / Frontier: GhostRangeLimit(OnIntrinsicReceive); SubscribeLocalEvent(OnIntrinsicSpeak); + SubscribeLocalEvent(OnIntrinsicAudibleEmote); // DeltaV - Robots should be allowed to emote over radio. _exemptQuery = GetEntityQuery(); } @@ -90,6 +92,16 @@ private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent c } } + // DeltaV + private void OnIntrinsicAudibleEmote(EntityUid uid, IntrinsicRadioTransmitterComponent component, EntityAudiblyEmotedEvent args) + { + if (args.Channel != null && component.Channels.Contains(args.Channel.ID)) + { + SendRadioMessage(uid, args.Message, args.Channel, uid, emType: args.Type); + } + } + // DeltaV - End + /// /// Send radio message to all active radio listeners /// @@ -100,9 +112,10 @@ public void SendRadioMessage( EntityUid radioSource, int? frequency = null, LanguagePrototype? language = null, - bool escapeMarkup = true) // Frontier: added frequency + bool escapeMarkup = true, + EmoteType? emType = null) // Frontier: added frequency // DeltaV - EmoteType? added. { - SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, frequency: frequency, escapeMarkup: escapeMarkup, language: language); // Frontier: added frequency / Einstein Engines - Language + SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, frequency: frequency, escapeMarkup: escapeMarkup, language: language, emType: emType); // Frontier: added frequency / Einstein Engines - Language } /// @@ -117,7 +130,8 @@ public void SendRadioMessage( EntityUid radioSource, int? frequency = null, LanguagePrototype? language = null, - bool escapeMarkup = true) // Nuclear-14: add frequency + bool escapeMarkup = true, + EmoteType? emType = null) // DeltaV - EmoteType? added. // Nuclear-14: add frequency { // Einstein Engines - Language begin if (language == null) @@ -170,7 +184,7 @@ public void SendRadioMessage( // ("channel", channelText), // Frontier: $"\\[{channel.LocalizedName}\\]" + /// Checks which type of emote the message contains and returns the type while outputting the string without the prefixes. + /// + public static EmoteType? ProcessEmoteMessage(EntityUid source, string input, out string output) + { + output = input.Trim(); + EmoteType? type = null; + + if (input.Length == 0) + return type; + + if (!(input.StartsWith(AudibleEmotePrefix) || input.StartsWith(PossessiveEmotePrefix))) + { + type = EmoteType.Normal; + return type; + } + + var emoteType = input[0]; + output = input[1..].TrimStart(); + + if (emoteType == AudibleEmotePrefix) + { + emoteType = input[1]; // Check for if we want AudiblePossessiveEmote + type = EmoteType.Audible; + } + + if (emoteType == PossessiveEmotePrefix) + { + if (type == EmoteType.Audible) + { + type = EmoteType.AudiblePossessive; + output = input[2..].TrimStart(); + } + else + type = EmoteType.Possessive; + } + + return type; + } + // DeltaV - End + /// /// Creates and raises and then to let other systems do things like play audio. /// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player @@ -274,4 +318,4 @@ private string TrimPunctuation(string textInput) return textInput[trimStart..trimEnd]; } -} \ No newline at end of file +} diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index 4c5e503c5cb..4425ba3409b 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -1,6 +1,7 @@ using System.Collections.Frozen; using Content.Shared._Starlight.CollectiveMind; // Goobstation - Starlight collective mind port using System.Text.RegularExpressions; +using Content.Shared._DV.Chat; using Content.Shared.ActionBlocker; using Content.Shared.Chat.Prototypes; using Content.Shared.Popups; @@ -26,6 +27,8 @@ public abstract partial class SharedChatSystem : EntitySystem public const char OOCPrefix = '['; public const char EmotesPrefix = '@'; public const char EmotesAltPrefix = '*'; + public const char AudibleEmotePrefix = '!'; // DeltaV - You may now scream audibly! + public const char PossessiveEmotePrefix = '\''; // DeltaV - You may now be possessive of things! Whatever that means. public const char AdminPrefix = ']'; public const char WhisperPrefix = ','; public const char CollectiveMindPrefix = '+'; @@ -161,6 +164,7 @@ public bool TryProccessRadioMessage( string input, out string output, out RadioChannelPrototype? channel, + bool capitalize = true, // DeltaV - We might not want to capitalize the first letter if we send in emotes. bool quiet = false) { output = input.Trim(); @@ -171,7 +175,7 @@ public bool TryProccessRadioMessage( if (input.StartsWith(RadioCommonPrefix)) { - output = SanitizeMessageCapital(input[1..].TrimStart()); + output = capitalize ? SanitizeMessageCapital(input[1..].TrimStart()) : input[1..].TrimStart(); // DeltaV channel = _prototypeManager.Index(CommonChannel); return true; } @@ -181,7 +185,7 @@ public bool TryProccessRadioMessage( if (input.Length < 2 || char.IsWhiteSpace(input[1])) { - output = SanitizeMessageCapital(input[1..].TrimStart()); + output = capitalize ? SanitizeMessageCapital(input[1..].TrimStart()) : input[1..].TrimStart(); // DeltaV if (!quiet) _popup.PopupEntity(Loc.GetString("chat-manager-no-radio-key"), source, source); return true; @@ -189,7 +193,7 @@ public bool TryProccessRadioMessage( var channelKey = input[1]; channelKey = char.ToLower(channelKey); - output = SanitizeMessageCapital(input[2..].TrimStart()); + output = capitalize ? SanitizeMessageCapital(input[2..].TrimStart()) : input[2..].TrimStart(); // DeltaV if (channelKey == DefaultChannelKey) { @@ -376,11 +380,29 @@ public static string GetStringInsideTag(ChatMessage message, string tag) return rawmsg.Substring(tagStart, tagEnd - tagStart); } + // DeltaV + protected virtual void SendAudibleEntityEmote( + EntityUid source, + string action, + ChatTransmitRange range, + string? nameOverride, + RadioChannelPrototype? channel, + EmoteType? emoteType, + bool hideLog = false, + bool checkEmote = true, + bool ignoreActionBlocker = false, + NetUserId? author = null + ) + { + } + // DeltaV - End + protected virtual void SendEntityEmote( EntityUid source, string action, ChatTransmitRange range, string? nameOverride, + EmoteType? emoteType, bool hideLog = false, bool checkEmote = true, bool ignoreActionBlocker = false, diff --git a/Content.Shared/_DV/Chat/SharedChatEvents.cs b/Content.Shared/_DV/Chat/SharedChatEvents.cs new file mode 100644 index 00000000000..bc4b7b60cdd --- /dev/null +++ b/Content.Shared/_DV/Chat/SharedChatEvents.cs @@ -0,0 +1,6 @@ +using Content.Shared.Radio; + +namespace Content.Shared._DV.Chat; + +[ByRefEvent] +public record struct EntityAudiblyEmotedEvent(EntityUid Source, string Message, RadioChannelPrototype? Channel, EmoteType? Type); diff --git a/Content.Shared/_DV/Chat/SharedChatSystemEnums.cs b/Content.Shared/_DV/Chat/SharedChatSystemEnums.cs new file mode 100644 index 00000000000..3a4a9e26788 --- /dev/null +++ b/Content.Shared/_DV/Chat/SharedChatSystemEnums.cs @@ -0,0 +1,15 @@ +namespace Content.Shared._DV.Chat; + + +// Note for future: If you want to make this more robust to handle more types of emotes while still being able to check off audible as an option for it then it would +// probably be better to make it a struct and have audible be a flag for it and then the type of emote. This would avoid having to make two different types for audible and visual. +/// +/// Different ways of emoting. For that little extra in RP! +/// +public enum EmoteType : byte +{ + Normal, // Character emotes + Audible, // Character screams + Possessive, // Character's emote + AudiblePossessive // Character's scream +} diff --git a/Resources/Locale/en-US/_DV/headset/headset-component.ftl b/Resources/Locale/en-US/_DV/headset/headset-component.ftl new file mode 100644 index 00000000000..7237b25eb40 --- /dev/null +++ b/Resources/Locale/en-US/_DV/headset/headset-component.ftl @@ -0,0 +1,2 @@ +chat-radio-message-audible-emote-wrap = [color={$color}]{$channel} {$name} {$message}[/color] +chat-radio-message-audible-possessive-emote-wrap = [color={$color}]{$channel} {$name}'s {$message}[/color] diff --git a/Resources/Locale/en-US/_NF/chat/managers/chat_manager.ftl b/Resources/Locale/en-US/_NF/chat/managers/chat_manager.ftl index a1da8555704..f3b171fd2a5 100644 --- a/Resources/Locale/en-US/_NF/chat/managers/chat_manager.ftl +++ b/Resources/Locale/en-US/_NF/chat/managers/chat_manager.ftl @@ -41,3 +41,17 @@ chat-speech-verb-feroxi-1 = blubs chat-speech-verb-feroxi-2 = swishes chat-speech-verb-feroxi-3 = gnashes chat-speech-verb-feroxi-4 = growls + +chat-manager-entity-me-possessive-wrap-message = [italic]{ PROPER($entity) -> + *[false] The {$entityName}'s {$message}[/italic] + [true] {CAPITALIZE($entityName)}'s {$message}[/italic] + } + +chat-manager-entity-me-audible-wrap-message = [italic]{ PROPER($entity) -> + *[false] The {$entityName} {$message}[/italic] + [true] {CAPITALIZE($entityName)} {$message}[/italic] + } +chat-manager-entity-me-audible-possessive-wrap-message = [italic]{ PROPER($entity) -> + *[false] The {$entityName}'s {$message}[/italic] + [true] {CAPITALIZE($entityName)}'s {$message}[/italic] + }