mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-18 05:59:03 +03:00
# Description This refactors #510 and #553. #553 specifically was reverted and re-implemented from scratch. As a consequence to all of this, the chat system was refactored a bit too, hopefully for the best. Changes: - InGameICChatType, InGameOOCChatType, ChatTransmitRange were all moved to shared and made serializable - Added a method to wrap whisper messages to reduce code duplication in chat system - Both WrapPublicMethod and WrapWhisperMessage call the same generic WrapMessage method, which allows to add speech verbs to whispers and more. That method is also fully responsible for adding language markers and deducing speech verbs now. - Everything related to speech was moved out of LanguagePrototype and into SpeechOverrideInfo. LanguagePrototype now holds an instance of that. - Added AllowRadio, RequireSpeech, ChatTypeOverride, SpeechVerbOverrides, MessageWrapOverrides to SpeechOverrideInfo, all of which are used in implementing the sign language. - Suffered a lot # TODO - [X] Cry - [X] Fix the sign language not displaying properly over the character. - [X] Find a way to circumvent being unable to speak?? <details><summary><h1>Media</h1></summary><p>  See below </p></details> # Changelog No cl no fun --------- Signed-off-by: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com> Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com>
286 lines
9.5 KiB
C#
286 lines
9.5 KiB
C#
using System.Collections.Frozen;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Radio;
|
|
using Content.Shared.Speech;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Shared.Chat;
|
|
|
|
public abstract 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 = '(';
|
|
public const char OOCPrefix = '[';
|
|
public const char EmotesPrefix = '@';
|
|
public const char EmotesAltPrefix = '*';
|
|
public const char AdminPrefix = ']';
|
|
public const char WhisperPrefix = ',';
|
|
public const char TelepathicPrefix = '='; //Nyano - Summary: Adds the telepathic channel's prefix.
|
|
public const char DefaultChannelKey = 'h';
|
|
|
|
[ValidatePrototypeId<RadioChannelPrototype>]
|
|
public const string CommonChannel = "Common";
|
|
|
|
public static string DefaultChannelPrefix = $"{RadioChannelPrefix}{DefaultChannelKey}";
|
|
|
|
[ValidatePrototypeId<SpeechVerbPrototype>]
|
|
public const string DefaultSpeechVerb = "Default";
|
|
|
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
|
|
|
/// <summary>
|
|
/// Cache of the keycodes for faster lookup.
|
|
/// </summary>
|
|
private FrozenDictionary<char, RadioChannelPrototype> _keyCodes = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
DebugTools.Assert(_prototypeManager.HasIndex<RadioChannelPrototype>(CommonChannel));
|
|
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
|
|
CacheRadios();
|
|
}
|
|
|
|
protected virtual void OnPrototypeReload(PrototypesReloadedEventArgs obj)
|
|
{
|
|
if (obj.WasModified<RadioChannelPrototype>())
|
|
CacheRadios();
|
|
}
|
|
|
|
private void CacheRadios()
|
|
{
|
|
_keyCodes = _prototypeManager.EnumeratePrototypes<RadioChannelPrototype>()
|
|
.ToFrozenDictionary(x => x.KeyCode);
|
|
}
|
|
|
|
/// <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<SpeechVerbPrototype>(DefaultSpeechVerb);
|
|
|
|
// check for a suffix-applicable speech verb
|
|
SpeechVerbPrototype? current = null;
|
|
foreach (var (str, id) in speech.SuffixSpeechVerbs)
|
|
{
|
|
var proto = _prototypeManager.Index<SpeechVerbPrototype>(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<SpeechVerbPrototype>(speech.SpeechVerb);
|
|
}
|
|
|
|
/// <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 TryProccessRadioMessage(
|
|
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;
|
|
}
|
|
|
|
public string SanitizeMessageCapital(string message)
|
|
{
|
|
if (string.IsNullOrEmpty(message))
|
|
return message;
|
|
// Capitalize first letter
|
|
message = char.ToUpper(message[0]) + message.Remove(0, 1);
|
|
return message;
|
|
}
|
|
|
|
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;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <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
|
|
[Serializable, NetSerializable]
|
|
public enum InGameICChatType : byte
|
|
{
|
|
Speak,
|
|
Emote,
|
|
Whisper,
|
|
Telepathic
|
|
}
|
|
|
|
/// <summary>
|
|
/// InGame OOC chat is for chat that is specifically ingame (not lobby) but is OOC, like deadchat or LOOC.
|
|
/// </summary>
|
|
[Serializable, NetSerializable]
|
|
public enum InGameOOCChatType : byte
|
|
{
|
|
Looc,
|
|
Dead
|
|
}
|
|
|
|
/// <summary>
|
|
/// Controls transmission of chat.
|
|
/// </summary>
|
|
[Serializable, NetSerializable]
|
|
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
|
|
}
|