Files
ss14-wega/Content.Shared/Chat/SharedChatSystem.cs
Zekins 99bc2edc32 МбКульт & Gambling^3 (#279)
* refmbcult

* fixrule

* minich

* yeee

* linter

* somefixes

* cultext&gamb

* linterfix

* fixtarot
2025-12-31 18:55:15 +03:00

553 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Frozen;
using System.Text.RegularExpressions;
using Content.Shared.ActionBlocker;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Mind; // Corvax-Wega-MindChat
using Content.Shared.Popups;
using Content.Shared.Radio;
using Content.Shared.Speech;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Console;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Shared.Chat;
public abstract partial class SharedChatSystem : EntitySystem
{
public const char RadioCommonPrefix = ';';
public const char RadioChannelPrefix = ':';
public const char RadioChannelAltPrefix = '.';
public const char LocalPrefix = '>';
public const char ConsolePrefix = '/';
public const char DeadPrefix = '\\';
public const char LOOCPrefix = '_'; // Corvax-Localization
public const char OOCPrefix = '[';
public const char EmotesPrefix = '%'; // Corvax-Localization
public const char EmotesAltPrefix = '*';
public const char AdminPrefix = ']';
public const char WhisperPrefix = ',';
public const char MindPrefix = '+'; // Corvax-Wega-MindChat
public const char DefaultChannelKey = 'а'; // Corvax-Wega-Edit
// Corvax-TTS-Start: Moved from Server to Shared
public const int VoiceRange = 10; // how far voice goes in world units
public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units
public const int WhisperMuffledRange = 5; // how far whisper goes at all, in world units
public const int MindChatRange = 1000; // Corvax-Wega-MindChat
public static readonly SoundSpecifier DefaultAnnouncementSound
= new SoundPathSpecifier("/Audio/Announcements/announce.ogg");
public static readonly ProtoId<RadioChannelPrototype> CommonChannel = "Common";
public static readonly string DefaultChannelPrefix = $"{RadioChannelPrefix}{DefaultChannelKey}";
public static readonly ProtoId<SpeechVerbPrototype> DefaultSpeechVerb = "Default";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly INetManager _net = default!;
/// <summary>
/// Cache of the keycodes for faster lookup.
/// </summary>
private FrozenDictionary<char, RadioChannelPrototype> _keyCodes = default!;
private FrozenDictionary<char, MindChannelPrototype> _mindKeyCodes = default!; // Corvax-Wega-MindChat
public override void Initialize()
{
base.Initialize();
DebugTools.Assert(_prototypeManager.HasIndex(CommonChannel));
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
CacheRadios();
CacheEmotes();
CacheMindChannels(); // Corvax-Wega-MindChat
}
protected virtual void OnPrototypeReload(PrototypesReloadedEventArgs obj)
{
if (obj.WasModified<RadioChannelPrototype>())
CacheRadios();
if (obj.WasModified<EmotePrototype>())
CacheEmotes();
// Corvax-Wega-MindChat-start
if (obj.WasModified<MindChannelPrototype>())
CacheMindChannels();
// Corvax-Wega-MindChat-end
}
private void CacheRadios()
{
_keyCodes = _prototypeManager.EnumeratePrototypes<RadioChannelPrototype>()
.ToFrozenDictionary(x => x.KeyCode);
}
// Corvax-Wega-MindChat-start
private void CacheMindChannels()
{
_mindKeyCodes = _prototypeManager.EnumeratePrototypes<MindChannelPrototype>()
.ToFrozenDictionary(x => x.KeyCode);
}
// Corvax-Wega-MindChat-end
/// <summary>
/// Attempts to find an applicable <see cref="SpeechVerbPrototype"/> for a speaking entity's message.
/// If one is not found, returns <see cref="DefaultSpeechVerb"/>.
/// </summary>
public SpeechVerbPrototype GetSpeechVerb(EntityUid source, string message, SpeechComponent? speech = null)
{
if (!Resolve(source, ref speech, false))
return _prototypeManager.Index(DefaultSpeechVerb);
// check for a suffix-applicable speech verb
SpeechVerbPrototype? current = null;
foreach (var (str, id) in speech.SuffixSpeechVerbs)
{
var proto = _prototypeManager.Index(id);
if (message.EndsWith(Loc.GetString(str)) && proto.Priority >= (current?.Priority ?? 0))
{
current = proto;
}
}
// if no applicable suffix verb return the normal one used by the entity
return current ?? _prototypeManager.Index(speech.SpeechVerb);
}
/// <summary>
/// Splits the input message into a radio prefix part and the rest to preserve it during sanitization.
/// </summary>
/// <remarks>
/// This is primarily for the chat emote sanitizer, which can match against ":b" as an emote, which is a valid radio keycode.
/// </remarks>
public void GetRadioKeycodePrefix(EntityUid source,
string input,
out string output,
out string prefix)
{
prefix = string.Empty;
output = input;
// If the string is less than 2, then it's probably supposed to be an emote.
// No one is sending empty radio messages!
if (input.Length <= 2)
return;
if (!(input.StartsWith(RadioChannelPrefix) || input.StartsWith(RadioChannelAltPrefix)))
return;
if (!_keyCodes.TryGetValue(char.ToLower(input[1]), out _))
return;
prefix = input[..2];
output = input[2..];
}
/// <summary>
/// Attempts to resolve radio prefixes in chat messages (e.g., remove a leading ":e" and resolve the requested
/// channel. Returns true if a radio message was attempted, even if the channel is invalid.
/// </summary>
/// <param name="source">Source of the message</param>
/// <param name="input">The message to be modified</param>
/// <param name="output">The modified message</param>
/// <param name="channel">The channel that was requested, if any</param>
/// <param name="quiet">Whether or not to generate an informative pop-up message.</param>
/// <returns></returns>
public bool TryProcessRadioMessage(
EntityUid source,
string input,
out string output,
out RadioChannelPrototype? channel,
bool quiet = false)
{
output = input.Trim();
channel = null;
if (input.Length == 0)
return false;
if (input.StartsWith(RadioCommonPrefix))
{
output = SanitizeMessageCapital(input[1..].TrimStart());
channel = _prototypeManager.Index<RadioChannelPrototype>(CommonChannel);
return true;
}
if (!(input.StartsWith(RadioChannelPrefix) || input.StartsWith(RadioChannelAltPrefix)))
return false;
if (input.Length < 2 || char.IsWhiteSpace(input[1]))
{
output = SanitizeMessageCapital(input[1..].TrimStart());
if (!quiet)
_popup.PopupEntity(Loc.GetString("chat-manager-no-radio-key"), source, source);
return true;
}
var channelKey = input[1];
channelKey = char.ToLower(channelKey);
output = SanitizeMessageCapital(input[2..].TrimStart());
if (channelKey == DefaultChannelKey)
{
var ev = new GetDefaultRadioChannelEvent();
RaiseLocalEvent(source, ev);
if (ev.Channel != null)
_prototypeManager.TryIndex(ev.Channel, out channel);
return true;
}
if (!_keyCodes.TryGetValue(channelKey, out channel) && !quiet)
{
var msg = Loc.GetString("chat-manager-no-such-channel", ("key", channelKey));
_popup.PopupEntity(msg, source, source);
}
return true;
}
// Corvax-Wega-MindChat-start
public bool TryProcessMindMessage(
EntityUid source,
string input,
out string output,
out MindChannelPrototype? channel,
bool quiet = false)
{
output = input.Trim();
channel = null;
if (input.Length == 0 || !input.StartsWith(MindPrefix))
return false;
if (input.Length < 2 || char.IsWhiteSpace(input[1]))
{
output = SanitizeMessageCapital(input[1..].TrimStart());
if (!quiet)
_popup.PopupEntity(Loc.GetString("chat-manager-no-mind-key"), source, source);
return true;
}
var channelKey = input[1];
channelKey = char.ToLower(channelKey);
output = SanitizeMessageCapital(input[2..].TrimStart());
if (!_mindKeyCodes.TryGetValue(channelKey, out channel) && !quiet)
{
var msg = Loc.GetString("chat-manager-no-such-mind-channel", ("key", channelKey));
_popup.PopupEntity(msg, source, source);
}
return true;
}
// Corvax-Wega-MindChat-end
public string SanitizeMessageCapital(string message)
{
if (string.IsNullOrEmpty(message))
return message;
// Capitalize first letter
message = OopsConcat(char.ToUpper(message[0]).ToString(), message.Remove(0, 1));
return message;
}
private static string OopsConcat(string a, string b)
{
// This exists to prevent Roslyn being clever and compiling something that fails sandbox checks.
return a + b;
}
public string SanitizeMessageCapitalizeTheWordI(string message, string theWordI = "i")
{
if (string.IsNullOrEmpty(message))
return message;
for
(
var index = message.IndexOf(theWordI);
index != -1;
index = message.IndexOf(theWordI, index + 1)
)
{
// Stops the code If It's tryIng to capItalIze the letter I In the mIddle of words
// Repeating the code twice is the simplest option
if (index + 1 < message.Length && char.IsLetter(message[index + 1]))
continue;
if (index - 1 >= 0 && char.IsLetter(message[index - 1]))
continue;
var beforeTarget = message.Substring(0, index);
var target = message.Substring(index, theWordI.Length);
var afterTarget = message.Substring(index + theWordI.Length);
message = beforeTarget + target.ToUpper() + afterTarget;
}
return message;
}
public static string SanitizeAnnouncement(string message, int maxLength = 0, int maxNewlines = 2)
{
var trimmed = message.Trim();
if (maxLength > 0 && trimmed.Length > maxLength)
{
trimmed = $"{message[..maxLength]}...";
}
// No more than max newlines, other replaced to spaces
if (maxNewlines > 0)
{
var chars = trimmed.ToCharArray();
var newlines = 0;
for (var i = 0; i < chars.Length; i++)
{
if (chars[i] != '\n')
continue;
if (newlines >= maxNewlines)
chars[i] = ' ';
newlines++;
}
return new string(chars);
}
return trimmed;
}
public static string InjectTagInsideTag(ChatMessage message, string outerTag, string innerTag, string? tagParameter)
{
var rawmsg = message.WrappedMessage;
var tagStart = rawmsg.IndexOf($"[{outerTag}]");
var tagEnd = rawmsg.IndexOf($"[/{outerTag}]");
if (tagStart < 0 || tagEnd < 0) //If the outer tag is not found, the injection is not performed
return rawmsg;
tagStart += outerTag.Length + 2;
string innerTagProcessed = tagParameter != null ? $"[{innerTag}={tagParameter}]" : $"[{innerTag}]";
rawmsg = rawmsg.Insert(tagEnd, $"[/{innerTag}]");
rawmsg = rawmsg.Insert(tagStart, innerTagProcessed);
return rawmsg;
}
/// <summary>
/// Injects a tag around all found instances of a specific string in a ChatMessage.
/// Excludes strings inside other tags and brackets.
/// </summary>
public static string InjectTagAroundString(ChatMessage message, string targetString, string tag, string? tagParameter)
{
var rawmsg = message.WrappedMessage;
rawmsg = Regex.Replace(rawmsg, "(?i)(" + targetString + ")(?-i)(?![^[]*])", $"[{tag}={tagParameter}]$1[/{tag}]");
return rawmsg;
}
public static string GetStringInsideTag(ChatMessage message, string tag)
{
var rawmsg = message.WrappedMessage;
var tagStart = rawmsg.IndexOf($"[{tag}]");
var tagEnd = rawmsg.IndexOf($"[/{tag}]");
if (tagStart < 0 || tagEnd < 0)
return "";
tagStart += tag.Length + 2;
return rawmsg.Substring(tagStart, tagEnd - tagStart);
}
protected virtual void SendEntityEmote(
EntityUid source,
string action,
ChatTransmitRange range,
string? nameOverride,
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
NetUserId? author = null
)
{ }
/// <summary>
/// Sends an in-character chat message to relevant clients.
/// </summary>
/// <param name="source">The entity that is speaking.</param>
/// <param name="message">The message being spoken or emoted.</param>
/// <param name="desiredType">The chat type.</param>
/// <param name="hideChat">Whether or not this message should appear in the chat window.</param>
/// <param name="hideLog">Whether or not this message should appear in the adminlog window.</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>
/// <param name="checkRadioPrefix">Whether or not <paramref name="message"/> should be parsed with consideration of radio channel prefix text at start the start.</param>
/// <param name="ignoreActionBlocker">If set to true, action blocker will not be considered for whether an entity can send this message.</param>
public virtual void TrySendInGameICMessage(
EntityUid source,
string message,
InGameICChatType desiredType,
bool hideChat,
bool hideLog = false,
IConsoleShell? shell = null,
ICommonSession? player = null,
string? nameOverride = null,
bool checkRadioPrefix = true,
bool ignoreActionBlocker = false)
{ }
/// <summary>
/// Sends an in-character chat message to relevant clients.
/// </summary>
/// <param name="source">The entity that is speaking.</param>
/// <param name="message">The message being spoken or emoted.</param>
/// <param name="desiredType">The chat type.</param>
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
/// <param name="hideLog">Disables the admin log for this message if true. Used for entities that are not players, like vendors, cloning, etc.</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>
/// <param name="ignoreActionBlocker">If set to true, action blocker will not be considered for whether an entity can send this message.</param>
public virtual void TrySendInGameICMessage(
EntityUid source,
string message,
InGameICChatType desiredType,
ChatTransmitRange range,
bool hideLog = false,
IConsoleShell? shell = null,
ICommonSession? player = null,
string? nameOverride = null,
bool checkRadioPrefix = true,
bool ignoreActionBlocker = false
)
{ }
/// <summary>
/// Sends an out-of-character chat message to relevant clients.
/// </summary>
/// <param name="source">The entity that is speaking.</param>
/// <param name="message">The message being spoken or emoted.</param>
/// <param name="type">The chat type.</param>
/// <param name="hideChat">Whether or not to show the message in the chat window.</param>
/// <param name="shell"></param>
/// <param name="player">The player doing the speaking.</param>
public virtual void TrySendInGameOOCMessage(
EntityUid source,
string message,
InGameOOCChatType type,
bool hideChat,
IConsoleShell? shell = null,
ICommonSession? player = null
)
{ }
/// <summary>
/// Dispatches an announcement to all.
/// </summary>
/// <param name="message">The contents of the message.</param>
/// <param name="sender">The sender (Communications Console in Communications Console Announcement).</param>
/// <param name="playSound">Play the announcement sound.</param>
/// <param name="announcementSound">Sound to play.</param>
/// <param name="colorOverride">Optional color for the announcement message.</param>
public virtual void DispatchGlobalAnnouncement(
string message,
string? sender = null,
bool playSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null
)
{ }
/// <summary>
/// Dispatches an announcement to players selected by filter.
/// </summary>
/// <param name="filter">Filter to select players who will recieve the announcement.</param>
/// <param name="message">The contents of the message.</param>
/// <param name="source">The entity making the announcement (used to determine the station).</param>
/// <param name="sender">The sender (Communications Console in Communications Console Announcement).</param>
/// <param name="playSound">Play the announcement sound.</param>
/// <param name="announcementSound">Sound to play.</param>
/// <param name="colorOverride">Optional color for the announcement message.</param>
public virtual void DispatchFilteredAnnouncement(
Filter filter,
string message,
EntityUid? source = null,
string? sender = null,
bool playSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null)
{ }
/// <summary>
/// Dispatches an announcement on a specific station.
/// </summary>
/// <param name="source">The entity making the announcement (used to determine the station).</param>
/// <param name="message">The contents of the message.</param>
/// <param name="sender">The sender (Communications Console in Communications Console Announcement).</param>
/// <param name="playDefaultSound">Play the announcement sound.</param>
/// <param name="announcementSound">Sound to play.</param>
/// <param name="colorOverride">Optional color for the announcement message.</param>
public virtual void DispatchStationAnnouncement(
EntityUid source,
string message,
string? sender = null,
bool playDefaultSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null)
{ }
// Corvax-Wega-MindChat-start
public virtual void SendMindMessage(
EntityUid source,
string message,
MindChannelPrototype channel,
bool ignoreActionBlocker = false)
{ }
// Corvax-Wega-MindChat-end
}
/// <summary>
/// Controls transmission of chat.
/// </summary>
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
}
/// <summary>
/// InGame IC chat is for chat that is specifically ingame (not lobby) but is also in character, i.e. speaking.
/// </summary>
// ReSharper disable once InconsistentNaming
public enum InGameICChatType : byte
{
Speak,
Emote,
Whisper
}
/// <summary>
/// InGame OOC chat is for chat that is specifically ingame (not lobby) but is OOC, like deadchat or LOOC.
/// </summary>
public enum InGameOOCChatType : byte
{
Looc,
Dead
}