mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-17 13:37:47 +03:00
<!-- This is a semi-strict format, you can add/remove sections as needed but the order/format should be kept the same Remove these comments before submitting --> # Description <!-- Explain this PR in as much detail as applicable Some example prompts to consider: How might this affect the game? The codebase? What might be some alternatives to this? How/Who does this benefit/hurt [the game/codebase]? --> With protection against flashes a bit more easily obtainable than before (welding masks, sunglasses, engineering goggles, cyber eye traits, etc.) and having thought about this idea before, I'd like to do a quick poll on an idea I've had and would be willing to implement: Instead of a Flash, give HeadRevolutionaries a Manifesto. They use this (with a short doafter) on a person to convert them, spouting Rev Ideology at them as the doafter runs. This will only be blockable by * Mindshields * Not being an intelligent creature As a side-effect, Epistemics won't necessarily be the Prime First Target to Rev anymore. Unless they want more books and they're in the library. A head revolutionary will spawn with this book. It may also be found in maintenance or bookshelves, though this is not common. This is to ensure that _having_ the book does not immediately out you as a revolutionary. The book has no charges, as opposed to flashes. This is balanced out by the fact that you audibly spout revolutionary ideology and propaganda at a target and that it takes a few seconds to do the conversion. --- <!-- This is default collapsed, readers click to expand it and see all your media The PR media section can get very large at times, so this is a good way to keep it clean The title is written using HTML tags The title must be within the <summary> tags or you won't see it --> <details><summary><h1>Media</h1></summary> <p> https://github.com/user-attachments/assets/089d707b-9178-45b1-a38a-99f06ae5d9b1 </p> </details> --- # Changelog <!-- You can add an author after the `🆑` to change the name that appears in the changelog (ex: `🆑 Death`) Leaving it blank will default to your GitHub display name This includes all available types for the changelog --> 🆑 - tweak: Changed the way Revolutionaries convert people. Instead of flashes, they now use the Revolutionary Manifesto to 'persuade' new conspirators. This has a small delay (three seconds) and will make you speak propaganda at the target. Note that the book itself is not contraband, and may also be found in other places. Only a Head Revolutionary will be able to make use of its persuasive power, however... <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new in-game item—the Revolutionary Manifesto—which replaces previous flash-based conversion tools. It features distinctive visual design and sound effects. - Added a new method for sending in-game chat messages to all users, enhancing communication capabilities. - **Gameplay Updates** - Head Revolutionary roles now convert others using the manifesto, with updated narrative text, motivational speeches, and revised starting gear. - **Communication Enhancements** - Improved in-game messaging systems streamline chat interactions for a smoother experience. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Timfa <timfalken@hotmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: VMSolidus <evilexecutive@gmail.com> (cherry picked from commit 4f4c5be744332ba03245de0a5da8fd36255855f5)
416 lines
14 KiB
C#
416 lines
14 KiB
C#
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Xml.Linq;
|
|
using Content.Shared.Decals;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Radio;
|
|
using Content.Shared.Speech;
|
|
using Robust.Shared.Console;
|
|
using Robust.Shared.Player;
|
|
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';
|
|
// WD EDIT START
|
|
public const int VoiceRange = 10;
|
|
public const int WhisperClearRange = 2;
|
|
public const int WhisperMuffledRange = 5;
|
|
// WD EDIT END
|
|
|
|
[ValidatePrototypeId<RadioChannelPrototype>]
|
|
public const string CommonChannel = "Common";
|
|
|
|
public static string DefaultChannelPrefix = $"{RadioChannelPrefix}{DefaultChannelKey}";
|
|
|
|
[ValidatePrototypeId<SpeechVerbPrototype>]
|
|
public const string DefaultSpeechVerb = "Default";
|
|
|
|
protected static string[] _chatNameColors = Array.Empty<string>(); // WWDP EDIT
|
|
|
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
|
|
|
/// <summary>
|
|
/// Cache of the keycodes for faster lookup.
|
|
/// </summary>
|
|
private readonly Dictionary<char, RadioChannelPrototype> _keyCodes = new(); // WD EDIT
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
DebugTools.Assert(_prototypeManager.HasIndex<RadioChannelPrototype>(CommonChannel));
|
|
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
|
|
CacheRadios();
|
|
CacheNameColors(); // WWDP EDIT
|
|
}
|
|
|
|
protected virtual void OnPrototypeReload(PrototypesReloadedEventArgs obj)
|
|
{
|
|
if (obj.WasModified<RadioChannelPrototype>())
|
|
CacheRadios();
|
|
if (obj.WasModified<ColorPalettePrototype>()) // WWDP EDIT
|
|
CacheNameColors(); // WWDP EDIT
|
|
}
|
|
|
|
// WWDP EDIT START
|
|
private void CacheNameColors()
|
|
{
|
|
var nameColors = _prototypeManager.Index<ColorPalettePrototype>("ChatNames").Colors.Values.ToArray();
|
|
_chatNameColors = new string[nameColors.Length];
|
|
for (var i = 0; i < nameColors.Length; i++)
|
|
{
|
|
_chatNameColors[i] = nameColors[i].ToHex();
|
|
}
|
|
}
|
|
// WWDP EDIT END
|
|
|
|
private void CacheRadios()
|
|
{
|
|
// WD EDIT START
|
|
_keyCodes.Clear();
|
|
|
|
foreach (var proto in _prototypeManager.EnumeratePrototypes<RadioChannelPrototype>())
|
|
{
|
|
foreach (var keycode in proto.KeyCodes.Where(keycode => !_keyCodes.ContainsKey(keycode)))
|
|
{
|
|
_keyCodes.Add(keycode, proto);
|
|
}
|
|
}
|
|
// WD EDIT 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<SpeechVerbPrototype>(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(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 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 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
|
|
) { }
|
|
|
|
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;
|
|
}
|
|
|
|
// WWDP EDIT START
|
|
public static string InjectTagAroundTag(ChatMessage message, string innerTag, string outerTag, string? tagParameter)
|
|
{
|
|
var rawmsg = message.WrappedMessage;
|
|
var tagStart = rawmsg.IndexOf($"[{innerTag}]");
|
|
var tagEnd = rawmsg.IndexOf($"[/{innerTag}]");
|
|
if (tagStart < 0 || tagEnd < 0) //If the inner tag is not found, the injection is not performed
|
|
return rawmsg;
|
|
tagEnd += innerTag.Length + 3;
|
|
|
|
string innerTagProcessed = tagParameter != null ? $"[{outerTag}={tagParameter}]" : $"[{outerTag}]";
|
|
|
|
rawmsg = rawmsg.Insert(tagEnd, $"[/{outerTag}]");
|
|
rawmsg = rawmsg.Insert(tagStart, innerTagProcessed);
|
|
|
|
return rawmsg;
|
|
}
|
|
// WWDP EDIT END
|
|
|
|
/// <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);
|
|
}
|
|
|
|
// WD EDIT START - Moved from ClatUIController
|
|
/// <summary>
|
|
/// Returns the chat name color for a mob
|
|
/// </summary>
|
|
/// <param name="name">Name of the mob</param>
|
|
/// <returns>Hex value of the color</returns>
|
|
public static string GetNameColor(string name)
|
|
{
|
|
if (_chatNameColors.Length == 0)
|
|
return "#FFFFFFFF";
|
|
var colorIdx = Math.Abs(name.GetHashCode() % _chatNameColors.Length);
|
|
return _chatNameColors[colorIdx];
|
|
}
|
|
// WD EDIT END
|
|
}
|
|
|
|
/// <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
|
|
}
|