diff --git a/Content.Server/Chat/Commands/MeCommand.cs b/Content.Server/Chat/Commands/MeCommand.cs index a182b1757b..66576d1ea6 100644 --- a/Content.Server/Chat/Commands/MeCommand.cs +++ b/Content.Server/Chat/Commands/MeCommand.cs @@ -38,7 +38,7 @@ namespace Content.Server.Chat.Commands return; IoCManager.Resolve().GetEntitySystem() - .TrySendInGameICMessage(playerEntity, message, InGameICChatType.Emote, false, false, shell, player); + .TrySendInGameICMessage(playerEntity, message, InGameICChatType.Emote, ChatTransmitRange.Normal, shell, player); } } } diff --git a/Content.Server/Chat/Commands/SayCommand.cs b/Content.Server/Chat/Commands/SayCommand.cs index c0e5802411..4cf3d507d2 100644 --- a/Content.Server/Chat/Commands/SayCommand.cs +++ b/Content.Server/Chat/Commands/SayCommand.cs @@ -38,7 +38,7 @@ namespace Content.Server.Chat.Commands return; IoCManager.Resolve().GetEntitySystem() - .TrySendInGameICMessage(playerEntity, message, InGameICChatType.Speak, false, false, shell, player); + .TrySendInGameICMessage(playerEntity, message, InGameICChatType.Speak, ChatTransmitRange.Normal, shell, player); } } } diff --git a/Content.Server/Chat/Commands/WhisperCommand.cs b/Content.Server/Chat/Commands/WhisperCommand.cs index 58f4382e40..dac3592562 100644 --- a/Content.Server/Chat/Commands/WhisperCommand.cs +++ b/Content.Server/Chat/Commands/WhisperCommand.cs @@ -38,7 +38,7 @@ namespace Content.Server.Chat.Commands return; IoCManager.Resolve().GetEntitySystem() - .TrySendInGameICMessage(playerEntity, message, InGameICChatType.Whisper, false, false, shell, player); + .TrySendInGameICMessage(playerEntity, message, InGameICChatType.Whisper, ChatTransmitRange.Normal, shell, player); } } } diff --git a/Content.Server/Chat/Systems/AutoEmoteSystem.cs b/Content.Server/Chat/Systems/AutoEmoteSystem.cs index f139c9d63c..d8d7f952d5 100644 --- a/Content.Server/Chat/Systems/AutoEmoteSystem.cs +++ b/Content.Server/Chat/Systems/AutoEmoteSystem.cs @@ -47,7 +47,7 @@ public sealed class AutoEmoteSystem : EntitySystem if (autoEmotePrototype.WithChat) { - _chatSystem.TryEmoteWithChat(uid, autoEmotePrototype.EmoteId, autoEmotePrototype.HiddenFromChatWindow); + _chatSystem.TryEmoteWithChat(uid, autoEmotePrototype.EmoteId, autoEmotePrototype.HiddenFromChatWindow ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal); } else { diff --git a/Content.Server/Chat/Systems/ChatSystem.Emote.cs b/Content.Server/Chat/Systems/ChatSystem.Emote.cs index a31cde8fa1..5210b366ab 100644 --- a/Content.Server/Chat/Systems/ChatSystem.Emote.cs +++ b/Content.Server/Chat/Systems/ChatSystem.Emote.cs @@ -52,15 +52,13 @@ public partial class ChatSystem /// /// The entity that is speaking /// The id of emote prototype. Should has valid - /// Whether or not this message should appear in the chat window - /// Whether or not this message should appear in the chat window for out-of-range ghosts (which otherwise ignore range restrictions) + /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. - public void TryEmoteWithChat(EntityUid source, string emoteId, bool hideChat = false, - bool hideGlobalGhostChat = false, string? nameOverride = null) + public void TryEmoteWithChat(EntityUid source, string emoteId, ChatTransmitRange range = ChatTransmitRange.Normal, string? nameOverride = null) { if (!_prototypeManager.TryIndex(emoteId, out var proto)) return; - TryEmoteWithChat(source, proto, hideChat, hideGlobalGhostChat, nameOverride); + TryEmoteWithChat(source, proto, range, nameOverride); } /// @@ -68,17 +66,15 @@ public partial class ChatSystem /// /// The entity that is speaking /// The emote prototype. Should has valid - /// Whether or not this message should appear in the chat window - /// Whether or not this message should appear in the chat window for out-of-range ghosts (which otherwise ignore range restrictions) + /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. - public void TryEmoteWithChat(EntityUid source, EmotePrototype emote, bool hideChat = false, - bool hideGlobalGhostChat = false, string? nameOverride = null) + public void TryEmoteWithChat(EntityUid source, EmotePrototype emote, ChatTransmitRange range = ChatTransmitRange.Normal, string? nameOverride = null) { // check if proto has valid message for chat if (emote.ChatMessages.Count != 0) { var action = _random.Pick(emote.ChatMessages); - SendEntityEmote(source, action, hideChat, hideGlobalGhostChat, nameOverride, false); + SendEntityEmote(source, action, range, nameOverride, false); } // do the rest of emote event logic here diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index fd3182e65c..4eda021b2e 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -120,17 +120,32 @@ public sealed partial class ChatSystem : SharedChatSystem /// The message being spoken or emoted /// The chat type /// Whether or not this message should appear in the chat window - /// Whether or not this message should appear in the chat window for out-of-range ghosts (which otherwise ignore range restrictions) /// /// The player doing the speaking /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. - public void TrySendInGameICMessage(EntityUid source, string message, InGameICChatType desiredType, bool hideChat, bool hideGlobalGhostChat = false, + public void TrySendInGameICMessage(EntityUid source, string message, InGameICChatType desiredType, bool hideChat, + IConsoleShell? shell = null, IPlayerSession? player = null, string? nameOverride = null, bool checkRadioPrefix = true) + { + TrySendInGameICMessage(source, message, desiredType, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, shell, player, nameOverride, checkRadioPrefix); + } + + /// + /// Sends an in-character chat message to relevant clients. + /// + /// The entity that is speaking + /// The message being spoken or emoted + /// The chat type + /// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all... + /// + /// The player doing the speaking + /// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised. + public void TrySendInGameICMessage(EntityUid source, string message, InGameICChatType desiredType, ChatTransmitRange range, IConsoleShell? shell = null, IPlayerSession? player = null, string? nameOverride = null, bool checkRadioPrefix = true) { if (HasComp(source)) { // Ghosts can only send dead chat messages, so we'll forward it to InGame OOC. - TrySendInGameOOCMessage(source, message, InGameOOCChatType.Dead, hideChat, shell, player); + TrySendInGameOOCMessage(source, message, InGameOOCChatType.Dead, range == ChatTransmitRange.HideChat, shell, player); return; } @@ -150,7 +165,6 @@ public sealed partial class ChatSystem : SharedChatSystem message = message[1..]; } - hideGlobalGhostChat |= hideChat; bool shouldCapitalize = (desiredType != InGameICChatType.Emote); bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation); @@ -159,7 +173,7 @@ public sealed partial class ChatSystem : SharedChatSystem // Was there an emote in the message? If so, send it. if (player != null && emoteStr != message && emoteStr != null) { - SendEntityEmote(source, emoteStr, hideChat, hideGlobalGhostChat, nameOverride); + SendEntityEmote(source, emoteStr, range, nameOverride); } // This can happen if the entire string is sanitized out. @@ -171,7 +185,7 @@ public sealed partial class ChatSystem : SharedChatSystem { if (TryProccessRadioMessage(source, message, out var modMessage, out var channel)) { - SendEntityWhisper(source, modMessage, hideChat, hideGlobalGhostChat, channel, nameOverride); + SendEntityWhisper(source, modMessage, range, channel, nameOverride); return; } } @@ -180,13 +194,13 @@ public sealed partial class ChatSystem : SharedChatSystem switch (desiredType) { case InGameICChatType.Speak: - SendEntitySpeak(source, message, hideChat, hideGlobalGhostChat, nameOverride); + SendEntitySpeak(source, message, range, nameOverride); break; case InGameICChatType.Whisper: - SendEntityWhisper(source, message, hideChat, hideGlobalGhostChat, null, nameOverride); + SendEntityWhisper(source, message, range, null, nameOverride); break; case InGameICChatType.Emote: - SendEntityEmote(source, message, hideChat, hideGlobalGhostChat, nameOverride); + SendEntityEmote(source, message, range, nameOverride); break; } } @@ -280,7 +294,7 @@ public sealed partial class ChatSystem : SharedChatSystem #region Private API - private void SendEntitySpeak(EntityUid source, string originalMessage, bool hideChat, bool hideGlobalGhostChat, string? nameOverride) + private void SendEntitySpeak(EntityUid source, string originalMessage, ChatTransmitRange range, string? nameOverride) { if (!_actionBlocker.CanSpeak(source)) return; @@ -306,7 +320,7 @@ public sealed partial class ChatSystem : SharedChatSystem var wrappedMessage = Loc.GetString("chat-manager-entity-say-wrap-message", ("entityName", name), ("message", FormattedMessage.EscapeText(message))); - SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, hideChat, hideGlobalGhostChat); + SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range); var ev = new EntitySpokeEvent(source, message, null, null); RaiseLocalEvent(source, ev, true); @@ -333,7 +347,7 @@ public sealed partial class ChatSystem : SharedChatSystem } } - private void SendEntityWhisper(EntityUid source, string originalMessage, bool hideChat, bool hideGlobalGhostChat, RadioChannelPrototype? channel, string? nameOverride) + private void SendEntityWhisper(EntityUid source, string originalMessage, ChatTransmitRange range, RadioChannelPrototype? channel, string? nameOverride) { if (!_actionBlocker.CanSpeak(source)) return; @@ -372,16 +386,16 @@ public sealed partial class ChatSystem : SharedChatSystem if (session.AttachedEntity is not { Valid: true } playerEntity) continue; - if (hideGlobalGhostChat && data.Observer && data.Range < 0) + if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full) 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 (data.Range <= WhisperRange) - _chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, data.HideChatOverride ?? hideChat, session.ConnectedClient); + _chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.ConnectedClient); else - _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, data.HideChatOverride ?? hideChat, session.ConnectedClient); + _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.ConnectedClient); } - _replay.QueueReplayMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, source, hideChat)); + _replay.QueueReplayMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, source, MessageRangeHideChatForReplay(range))); var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage); RaiseLocalEvent(source, ev, true); @@ -404,8 +418,7 @@ public sealed partial class ChatSystem : SharedChatSystem } } - private void SendEntityEmote(EntityUid source, string action, bool hideChat, - bool hideGlobalGhostChat, string? nameOverride, bool checkEmote = true) + private void SendEntityEmote(EntityUid source, string action, ChatTransmitRange range, string? nameOverride, bool checkEmote = true) { if (!_actionBlocker.CanEmote(source)) return; @@ -419,7 +432,7 @@ public sealed partial class ChatSystem : SharedChatSystem if (checkEmote) TryEmoteChatInput(source, action); - SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, hideChat, hideGlobalGhostChat); + SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range); if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}"); @@ -441,7 +454,7 @@ public sealed partial class ChatSystem : SharedChatSystem ("entityName", name), ("message", FormattedMessage.EscapeText(message))); - SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat, false); + SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}"); } @@ -474,18 +487,66 @@ public sealed partial class ChatSystem : SharedChatSystem #region Utility + private enum MessageRangeCheckResult { + Disallowed, + HideChat, + Full + } + + /// + /// If hideChat should be set as far as replays are concerned. + /// + private bool MessageRangeHideChatForReplay(ChatTransmitRange range) + { + return range == ChatTransmitRange.HideChat; + } + + /// + /// Checks if a target as returned from GetRecipients should receive the message. + /// Keep in mind data.Range is -1 for out of range observers. + /// + private MessageRangeCheckResult MessageRangeCheck(ICommonSession session, ICChatRecipientData data, ChatTransmitRange range) + { + var initialResult = MessageRangeCheckResult.Full; + switch (range) + { + case ChatTransmitRange.Normal: + initialResult = MessageRangeCheckResult.Full; + break; + case ChatTransmitRange.GhostRangeLimit: + initialResult = (data.Observer && data.Range < 0 && !_adminManager.IsAdmin((IPlayerSession) session)) ? MessageRangeCheckResult.HideChat : MessageRangeCheckResult.Full; + break; + case ChatTransmitRange.HideChat: + initialResult = MessageRangeCheckResult.HideChat; + break; + case ChatTransmitRange.NoGhosts: + initialResult = (data.Observer && !_adminManager.IsAdmin((IPlayerSession) session)) ? MessageRangeCheckResult.Disallowed : MessageRangeCheckResult.Full; + break; + } + var insistHideChat = data.HideChatOverride ?? false; + var insistNoHideChat = !(data.HideChatOverride ?? true); + if (insistHideChat && initialResult == MessageRangeCheckResult.Full) + return MessageRangeCheckResult.HideChat; + if (insistNoHideChat && initialResult == MessageRangeCheckResult.HideChat) + return MessageRangeCheckResult.Full; + return initialResult; + } + /// /// Sends a chat message to the given players in range of the source entity. /// - private void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool hideGlobalGhostChat) + private void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, ChatTransmitRange range) { foreach (var (session, data) in GetRecipients(source, VoiceRange)) { - var entHideChat = data.HideChatOverride ?? (hideChat || hideGlobalGhostChat && data.Observer && data.Range < 0); + var entRange = MessageRangeCheck(session, data, range); + if (entRange == MessageRangeCheckResult.Disallowed) + continue; + var entHideChat = entRange == MessageRangeCheckResult.HideChat; _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.ConnectedClient); } - _replay.QueueReplayMessage(new ChatMessage(channel, message, wrappedMessage, source, hideChat)); + _replay.QueueReplayMessage(new ChatMessage(channel, message, wrappedMessage, source, MessageRangeHideChatForReplay(range))); } /// @@ -710,3 +771,19 @@ public enum InGameOOCChatType : byte Looc, Dead } + +/// +/// Controls transmission of chat. +/// +public enum ChatTransmitRange : byte +{ + /// Acts normal, ghosts can hear across the map, etc. + Normal, + /// Normal but ghosts are still range-limited. + GhostRangeLimit, + /// Hidden from the chat window. + HideChat, + /// Ghosts can't hear or see it at all. Regular players can if in-range. + NoGhosts +} + diff --git a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs index 956a7eb825..9a435971f2 100644 --- a/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs +++ b/Content.Server/Chat/Systems/EmoteOnDamageSystem.cs @@ -44,7 +44,7 @@ public sealed class EmoteOnDamageSystem : EntitySystem var emote = _random.Pick(emoteOnDamage.Emotes); if (emoteOnDamage.WithChat) { - _chatSystem.TryEmoteWithChat(uid, emote, emoteOnDamage.HiddenFromChatWindow); + _chatSystem.TryEmoteWithChat(uid, emote, emoteOnDamage.HiddenFromChatWindow ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal); } else { diff --git a/Content.Server/Chemistry/ReagentEffects/Emote.cs b/Content.Server/Chemistry/ReagentEffects/Emote.cs index 10df99359d..fa3cf937d0 100644 --- a/Content.Server/Chemistry/ReagentEffects/Emote.cs +++ b/Content.Server/Chemistry/ReagentEffects/Emote.cs @@ -25,7 +25,7 @@ public sealed class Emote : ReagentEffect var chatSys = args.EntityManager.System(); if (ShowInChat) - chatSys.TryEmoteWithChat(args.SolutionEntity, EmoteId, hideGlobalGhostChat: true); + chatSys.TryEmoteWithChat(args.SolutionEntity, EmoteId, ChatTransmitRange.GhostRangeLimit); else chatSys.TryEmoteWithoutChat(args.SolutionEntity, EmoteId); diff --git a/Content.Server/Cluwne/CluwneSystem.cs b/Content.Server/Cluwne/CluwneSystem.cs index e380699259..aca90c3461 100644 --- a/Content.Server/Cluwne/CluwneSystem.cs +++ b/Content.Server/Cluwne/CluwneSystem.cs @@ -91,14 +91,14 @@ public sealed class CluwneSystem : EntitySystem if (_robustRandom.Prob(component.GiggleRandomChance)) { _audio.PlayPvs(component.SpawnSound, uid); - _chat.TrySendInGameICMessage(uid, "honks", InGameICChatType.Emote, false, false); + _chat.TrySendInGameICMessage(uid, "honks", InGameICChatType.Emote, ChatTransmitRange.Normal); } else if (_robustRandom.Prob(component.KnockChance)) { _audio.PlayPvs(component.KnockSound, uid); _stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(component.ParalyzeTime), true); - _chat.TrySendInGameICMessage(uid, "spasms", InGameICChatType.Emote, false, false); + _chat.TrySendInGameICMessage(uid, "spasms", InGameICChatType.Emote, ChatTransmitRange.Normal); } } } diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs index 0b65ac055c..9fa50c919a 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Specific/MedibotInjectOperator.cs @@ -80,7 +80,7 @@ public sealed class MedibotInjectOperator : HTNOperator _solution.TryAddReagent(target, injectable, botComp.EmergencyMed, botComp.EmergencyMedAmount, out var accepted); _popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, target); _audio.PlayPvs(botComp.InjectSound, target); - _chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, hideChat: false, hideGlobalGhostChat: true); + _chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, ChatTransmitRange.GhostRangeLimit); return HTNOperatorStatus.Finished; } @@ -89,7 +89,7 @@ public sealed class MedibotInjectOperator : HTNOperator _solution.TryAddReagent(target, injectable, botComp.StandardMed, botComp.StandardMedAmount, out var accepted); _popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, target); _audio.PlayPvs(botComp.InjectSound, target); - _chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, hideChat: false, hideGlobalGhostChat: true); + _chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, ChatTransmitRange.GhostRangeLimit); return HTNOperatorStatus.Finished; } diff --git a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs index c8de625025..22a7441ea9 100644 --- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs @@ -214,7 +214,7 @@ public sealed class RadioDeviceSystem : EntitySystem 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, checkRadioPrefix: false); + // 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, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false); } } diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs index 0d3d96572c..d40303ab31 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraSpeakerSystem.cs @@ -74,7 +74,7 @@ public sealed class SurveillanceCameraSpeakerSystem : EntitySystem 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 - _chatSystem.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Speak, false, hideGlobalGhostChat, nameOverride: name); + // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios + _chatSystem.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Speak, ChatTransmitRange.GhostRangeLimit, nameOverride: name); } }