using System.Linq; using System.Text; using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Server.Ghost.Components; using Content.Server.Players; using Content.Server.Popups; using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Shared.ActionBlocker; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Database; using Content.Shared.IdentityManagement; using Content.Shared.Inventory; using Content.Shared.Mobs.Systems; using Content.Shared.Radio; using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Players; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Replays; using Robust.Shared.Utility; namespace Content.Server.Chat.Systems; /// /// ChatSystem is responsible for in-simulation chat handling, such as whispering, speaking, emoting, etc. /// ChatSystem depends on ChatManager to actually send the messages. /// public sealed partial class ChatSystem : SharedChatSystem { [Dependency] private readonly IReplayRecordingManager _replay = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IChatSanitizationManager _sanitizer = default!; [Dependency] private readonly IAdminManager _adminManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly StationSystem _stationSystem = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; public const int VoiceRange = 10; // how far voice goes in world units public const int WhisperRange = 2; // how far whisper goes in world units public const string DefaultAnnouncementSound = "/Audio/Announcements/announce.ogg"; private bool _loocEnabled = true; private bool _deadLoocEnabled = false; private readonly bool _adminLoocEnabled = true; public override void Initialize() { base.Initialize(); InitializeEmotes(); _configurationManager.OnValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged, true); _configurationManager.OnValueChanged(CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged, true); SubscribeLocalEvent(OnGameChange); } public override void Shutdown() { base.Shutdown(); ShutdownEmotes(); _configurationManager.UnsubValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged); _configurationManager.UnsubValueChanged(CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged); } private void OnLoocEnabledChanged(bool val) { if (_loocEnabled == val) return; _loocEnabled = val; _chatManager.DispatchServerAnnouncement( Loc.GetString(val ? "chat-manager-looc-chat-enabled-message" : "chat-manager-looc-chat-disabled-message")); } private void OnDeadLoocEnabledChanged(bool val) { if (_deadLoocEnabled == val) return; _deadLoocEnabled = val; _chatManager.DispatchServerAnnouncement( Loc.GetString(val ? "chat-manager-dead-looc-chat-enabled-message" : "chat-manager-dead-looc-chat-disabled-message")); } private void OnGameChange(GameRunLevelChangedEvent ev) { switch (ev.New) { case GameRunLevel.InRound: if (!_configurationManager.GetCVar(CCVars.OocEnableDuringRound)) _configurationManager.SetCVar(CCVars.OocEnabled, false); break; case GameRunLevel.PostRound: if (!_configurationManager.GetCVar(CCVars.OocEnableDuringRound)) _configurationManager.SetCVar(CCVars.OocEnabled, true); break; } } /// /// Sends an in-character chat message to relevant clients. /// /// The entity that is speaking /// 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 adminlog window /// /// 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 hideLog = false, IConsoleShell? shell = null, IPlayerSession? player = null, string? nameOverride = null, bool checkRadioPrefix = true) { TrySendInGameICMessage(source, message, desiredType, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, hideLog, 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, bool hideLog = false, 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, range == ChatTransmitRange.HideChat, shell, player); return; } // Sus if (player?.AttachedEntity is { Valid: true } entity && source != entity) { return; } if (!CanSendInGame(message, shell, player)) return; if (desiredType == InGameICChatType.Speak && message.StartsWith(LocalPrefix)) { // prevent radios and remove prefix. checkRadioPrefix = false; message = message[1..]; } bool shouldCapitalize = (desiredType != InGameICChatType.Emote); bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation); message = SanitizeInGameICMessage(source, message, out var emoteStr, shouldCapitalize, shouldPunctuate); // Was there an emote in the message? If so, send it. if (player != null && emoteStr != message && emoteStr != null) { SendEntityEmote(source, emoteStr, range, nameOverride); } // This can happen if the entire string is sanitized out. if (string.IsNullOrEmpty(message)) return; // This message may have a radio prefix, and should then be whispered to the resolved radio channel if (checkRadioPrefix) { if (TryProccessRadioMessage(source, message, out var modMessage, out var channel)) { SendEntityWhisper(source, modMessage, range, channel, nameOverride); return; } } // Otherwise, send whatever type. switch (desiredType) { case InGameICChatType.Speak: SendEntitySpeak(source, message, range, nameOverride, hideLog); break; case InGameICChatType.Whisper: SendEntityWhisper(source, message, range, null, nameOverride, hideLog); break; case InGameICChatType.Emote: SendEntityEmote(source, message, range, nameOverride, hideLog); break; } } public void TrySendInGameOOCMessage(EntityUid source, string message, InGameOOCChatType type, bool hideChat, IConsoleShell? shell = null, IPlayerSession? player = null) { if (!CanSendInGame(message, shell, player)) return; // It doesn't make any sense for a non-player to send in-game OOC messages, whereas non-players may be sending // in-game IC messages. if (player?.AttachedEntity is not { Valid: true } entity || source != entity) return; message = SanitizeInGameOOCMessage(message); var sendType = type; // If dead player LOOC is disabled, unless you are an aghost, send dead messages to dead chat if (!_adminManager.IsAdmin(player) && !_deadLoocEnabled && (HasComp(source) || _mobStateSystem.IsDead(source))) sendType = InGameOOCChatType.Dead; switch (sendType) { case InGameOOCChatType.Dead: SendDeadChat(source, player, message, hideChat); break; case InGameOOCChatType.Looc: SendLOOC(source, player, message, hideChat); break; } } #region Announcements /// /// Dispatches an announcement to all. /// /// The contents of the message /// The sender (Communications Console in Communications Console Announcement) /// Play the announcement sound /// Optional color for the announcement message public void DispatchGlobalAnnouncement(string message, string sender = "Central Command", bool playSound = true, SoundSpecifier? announcementSound = null, Color? colorOverride = null) { var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message))); _chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride); if (playSound) { SoundSystem.Play(announcementSound?.GetSound() ?? DefaultAnnouncementSound, Filter.Broadcast(), AudioParams.Default.WithVolume(-2f)); } _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}"); } /// /// Dispatches an announcement on a specific station /// /// The entity making the announcement (used to determine the station) /// The contents of the message /// The sender (Communications Console in Communications Console Announcement) /// Play the announcement sound /// Optional color for the announcement message public void DispatchStationAnnouncement(EntityUid source, string message, string sender = "Central Command", bool playDefaultSound = true, SoundSpecifier? announcementSound = null, Color? colorOverride = null) { var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message))); var station = _stationSystem.GetOwningStation(source); if (station == null) { // you can't make a station announcement without a station return; } if (!EntityManager.TryGetComponent(station, out var stationDataComp)) return; var filter = _stationSystem.GetInStation(stationDataComp); _chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source, false, true, colorOverride); if (playDefaultSound) { SoundSystem.Play(announcementSound?.GetSound() ?? DefaultAnnouncementSound, filter, AudioParams.Default.WithVolume(-2f)); } _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement on {station} from {sender}: {message}"); } #endregion #region Private API private void SendEntitySpeak(EntityUid source, string originalMessage, ChatTransmitRange range, string? nameOverride, bool hideLog = false) { if (!_actionBlocker.CanSpeak(source)) return; var message = TransformSpeech(source, originalMessage); if (message.Length == 0) return; // get the entity's apparent name (if no override provided). string name; if (nameOverride != null) { name = nameOverride; } else { var nameEv = new TransformSpeakerNameEvent(source, Name(source)); RaiseLocalEvent(source, nameEv); name = nameEv.Name; } name = FormattedMessage.EscapeText(name); var wrappedMessage = Loc.GetString("chat-manager-entity-say-wrap-message", ("entityName", name), ("message", FormattedMessage.EscapeText(message))); SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range); var ev = new EntitySpokeEvent(source, message, null, null); RaiseLocalEvent(source, ev, true); // To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc. // Also doesn't log if hideLog is true. if (!HasComp(source) || hideLog == true) return; if (originalMessage == message) { if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user} as {name}: {originalMessage}."); else _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}: {originalMessage}."); } else { if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}."); else _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}."); } } private void SendEntityWhisper(EntityUid source, string originalMessage, ChatTransmitRange range, RadioChannelPrototype? channel, string? nameOverride, bool hideLog = false) { if (!_actionBlocker.CanSpeak(source)) return; var message = TransformSpeech(source, originalMessage); if (message.Length == 0) return; var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f); // 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-whisper-wrap-message", ("entityName", name), ("message", FormattedMessage.EscapeText(message))); var wrappedobfuscatedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message", ("entityName", name), ("message", FormattedMessage.EscapeText(obfuscatedMessage))); foreach (var (session, data) in GetRecipients(source, VoiceRange)) { if (session.AttachedEntity is not { Valid: true } playerEntity) continue; 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, false, session.ConnectedClient); else _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.ConnectedClient); } _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, source, MessageRangeHideChatForReplay(range))); var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage); RaiseLocalEvent(source, ev, true); if (!hideLog) if (originalMessage == message) { if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user} as {name}: {originalMessage}."); else _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}: {originalMessage}."); } else { if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}."); else _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}."); } } private void SendEntityEmote(EntityUid source, string action, ChatTransmitRange range, string? nameOverride, bool hideLog = false, bool checkEmote = true) { if (!_actionBlocker.CanEmote(source)) return; // get the entity's apparent name (if no override provided). string name = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(source, EntityManager)); // Emotes use Identity.Name, since it doesn't actually involve your voice at all. var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message", ("entityName", name), ("message", FormattedMessage.EscapeText(action))); if (checkEmote) TryEmoteChatInput(source, action); SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range); if (!hideLog) if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}"); else _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}"); } // ReSharper disable once InconsistentNaming private void SendLOOC(EntityUid source, IPlayerSession player, string message, bool hideChat) { var name = FormattedMessage.EscapeText(Identity.Name(source, EntityManager)); if (_adminManager.IsAdmin(player)) { if (!_adminLoocEnabled) return; } else if (!_loocEnabled) return; var wrappedMessage = Loc.GetString("chat-manager-entity-looc-wrap-message", ("entityName", name), ("message", FormattedMessage.EscapeText(message))); SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}"); } private void SendDeadChat(EntityUid source, IPlayerSession player, string message, bool hideChat) { var clients = GetDeadChatClients(); var playerName = Name(source); string wrappedMessage; if (_adminManager.IsAdmin(player)) { wrappedMessage = Loc.GetString("chat-manager-send-admin-dead-chat-wrap-message", ("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("userName", player.ConnectedClient.UserName), ("message", FormattedMessage.EscapeText(message))); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {player:Player}: {message}"); } else { wrappedMessage = Loc.GetString("chat-manager-send-dead-chat-wrap-message", ("deadChannelName", Loc.GetString("chat-manager-dead-channel-name")), ("playerName", (playerName)), ("message", FormattedMessage.EscapeText(message))); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {player:Player}: {message}"); } _chatManager.ChatMessageToMany(ChatChannel.Dead, message, wrappedMessage, source, hideChat, false, clients.ToList()); } #endregion #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, ChatTransmitRange range) { foreach (var (session, data) in GetRecipients(source, VoiceRange)) { 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.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, source, MessageRangeHideChatForReplay(range))); } /// /// Returns true if the given player is 'allowed' to send the given message, false otherwise. /// private bool CanSendInGame(string message, IConsoleShell? shell = null, IPlayerSession? player = null) { // Non-players don't have to worry about these restrictions. if (player == null) return true; var mindContainerComponent = player.ContentData()?.Mind; if (mindContainerComponent == null) { shell?.WriteError("You don't have a mind!"); return false; } if (player.AttachedEntity is not { Valid: true } _) { shell?.WriteError("You don't have an entity!"); return false; } return !_chatManager.MessageCharacterLimit(player, message); } // ReSharper disable once InconsistentNaming private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false) { var newMessage = message.Trim(); if (capitalize) newMessage = SanitizeMessageCapital(newMessage); if (punctuate) newMessage = SanitizeMessagePeriod(newMessage); _sanitizer.TrySanitizeOutSmilies(newMessage, source, out newMessage, out emoteStr); return newMessage; } private string SanitizeInGameOOCMessage(string message) { var newMessage = message.Trim(); newMessage = FormattedMessage.EscapeText(newMessage); return newMessage; } public string TransformSpeech(EntityUid sender, string message) { var ev = new TransformSpeechEvent(sender, message); RaiseLocalEvent(ev); return ev.Message; } private IEnumerable GetDeadChatClients() { return Filter.Empty() .AddWhereAttachedEntity(HasComp) .Recipients .Union(_adminManager.ActiveAdmins) .Select(p => p.ConnectedClient); } private string SanitizeMessagePeriod(string message) { if (string.IsNullOrEmpty(message)) return message; // Adds a period if the last character is a letter. if (char.IsLetter(message[^1])) message += "."; return message; } /// /// Returns list of players and ranges for all players withing some range. Also returns observers with a range of -1. /// private Dictionary GetRecipients(EntityUid source, float voiceRange) { // TODO proper speech occlusion var recipients = new Dictionary(); var ghosts = GetEntityQuery(); var xforms = GetEntityQuery(); var transformSource = xforms.GetComponent(source); var sourceMapId = transformSource.MapID; var sourceCoords = transformSource.Coordinates; foreach (var player in _playerManager.Sessions) { if (player.AttachedEntity is not {Valid: true} playerEntity) continue; var transformEntity = xforms.GetComponent(playerEntity); if (transformEntity.MapID != sourceMapId) continue; 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) { var modifiedMessage = new StringBuilder(message); for (var i = 0; i < message.Length; i++) { if (char.IsWhiteSpace((modifiedMessage[i]))) { continue; } if (_random.Prob(1 - chance)) { modifiedMessage[i] = '~'; } } return modifiedMessage.ToString(); } #endregion } /// /// 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). /// public record ExpandICChatRecipientstEvent(EntityUid Source, float VoiceRange, Dictionary Recipients) { } public sealed class TransformSpeakerNameEvent : EntityEventArgs { public EntityUid Sender; public string Name; public TransformSpeakerNameEvent(EntityUid sender, string name) { Sender = sender; Name = name; } } /// /// Raised broadcast in order to transform speech.transmit /// public sealed class TransformSpeechEvent : EntityEventArgs { public EntityUid Sender; public string Message; public TransformSpeechEvent(EntityUid sender, string message) { Sender = sender; Message = message; } } /// /// Raised on an entity when it speaks, either through 'say' or 'whisper'. /// public sealed class EntitySpokeEvent : EntityEventArgs { public readonly EntityUid Source; public readonly string Message; public readonly string? ObfuscatedMessage; // not null if this was a whisper /// /// 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. /// public RadioChannelPrototype? Channel; public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, string? obfuscatedMessage) { Source = source; Message = message; Channel = channel; ObfuscatedMessage = obfuscatedMessage; } } /// /// InGame IC chat is for chat that is specifically ingame (not lobby) but is also in character, i.e. speaking. /// // ReSharper disable once InconsistentNaming public enum InGameICChatType : byte { Speak, Emote, Whisper } /// /// InGame OOC chat is for chat that is specifically ingame (not lobby) but is OOC, like deadchat or LOOC. /// 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 }