fish-station/Content.Shared/Chat/SharedChatSystem.cs
VigersRay 6c158e6f0b Merge remote-tracking branch 'space-wizards/master'
# Conflicts:
#	.github/CODEOWNERS
#	.github/ISSUE_TEMPLATE/config.yml
#	.github/ISSUE_TEMPLATE/issue_report.md
#	Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
#	Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
#	Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml
#	Content.Client/UserInterface/Systems/Ghost/Controls/Roles/MakeGhostRoleWindow.xaml.cs
#	Content.Client/Weapons/Ranged/Systems/GunSystem.cs
#	Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs
#	Content.IntegrationTests/Tests/Destructible/DestructibleDamageTypeTest.cs
#	Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
#	Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeComponent.cs
#	Content.Server/Atmos/EntitySystems/FlammableSystem.cs
#	Content.Server/Chat/Systems/ChatSystem.cs
#	Content.Server/Destructible/Thresholds/DamageThreshold.cs
#	Content.Server/Entry/EntryPoint.cs
#	Content.Server/Fluids/EntitySystems/PuddleSystem.cs
#	Content.Server/GuideGenerator/ReagentEntry.cs
#	Content.Server/IoC/ServerContentIoC.cs
#	Content.Server/Medical/HealthAnalyzerSystem.cs
#	Content.Server/Medical/VomitSystem.cs
#	Content.Server/Radio/EntitySystems/HeadsetSystem.cs
#	Content.Server/Revenant/EntitySystems/RevenantSystem.cs
#	Content.Server/Weapons/Ranged/Systems/GunSystem.cs
#	Content.Shared/Armor/SharedArmorSystem.cs
#	Content.Shared/Bed/Sleep/SleepingSystem.cs
#	Content.Shared/Cabinet/ItemCabinetSystem.cs
#	Content.Shared/Chat/SharedChatSystem.Emote.cs
#	Content.Shared/Chat/SharedChatSystem.cs
#	Content.Shared/Clothing/EntitySystems/MaskSystem.cs
#	Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs
#	Content.Shared/Damage/Systems/DamageableSystem.cs
#	Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs
#	Content.Shared/EntityEffects/Effects/Transform/ExplosionEntityEffect.cs
#	Content.Shared/Humanoid/HumanoidCharacterAppearance.cs
#	Content.Shared/IoC/SharedContentIoC.cs
#	Content.Shared/Lock/LockSystem.cs
#	Content.Shared/Ninja/Components/NinjaSuitComponent.cs
#	Content.Shared/Preferences/HumanoidCharacterProfile.cs
#	Content.Shared/Revenant/Components/RevenantComponent.cs
#	Content.Shared/Silicons/StationAi/SharedStationAiSystem.Airlock.cs
#	Content.Shared/Trigger/Systems/TriggerSystem.Condition.cs
#	Content.Shared/VendingMachines/SharedVendingMachineSystem.cs
#	Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
#	Content.Shared/Weapons/Ranged/HitscanPrototype.cs
#	Content.Shared/Xenoarchaeology/Artifact/XAT/BaseQueryUpdateXATSystem.cs
#	README.md
#	Resources/Audio/Weapons/attributions.yml
#	Resources/Locale/en-US/_strings/ghost/make-ghost-gui.ftl
#	Resources/Locale/en-US/_strings/guidebook/chemistry/conditions.ftl
#	Resources/Locale/en-US/_strings/guidebook/chemistry/effects.ftl
#	Resources/Locale/en-US/_strings/guidebook/chemistry/healthchange.ftl
#	Resources/Locale/en-US/_strings/guidebook/chemistry/statuseffects.ftl
#	Resources/Locale/en-US/_strings/job/job-description.ftl
#	Resources/Locale/en-US/_strings/medical/components/crew-monitoring-component.ftl
#	Resources/Locale/en-US/_strings/npc/hugbot.ftl
#	Resources/Locale/en-US/_strings/npc/npc-debug.ftl
#	Resources/Locale/en-US/_strings/speech/speech-triggers.ftl
#	Resources/Locale/en-US/_strings/ui/stat-values.ftl
#	Resources/Locale/en-US/guidebook/chemistry/conditions.ftl
#	Resources/Locale/en-US/guidebook/chemistry/effects.ftl
#	Resources/Locale/en-US/guidebook/chemistry/healthchange.ftl
#	Resources/Locale/en-US/guidebook/entity-effects/conditions.ftl
#	Resources/Locale/en-US/guidebook/entity-effects/effects.ftl
#	Resources/Locale/en-US/guidebook/entity-effects/healthchange.ftl
#	Resources/Maps/Shuttles/cargo_relic.yml
#	Resources/Maps/_Sunrise/Station/bagel.yml
#	Resources/Maps/_Sunrise/Station/barratry.yml
#	Resources/Maps/_Sunrise/Station/fland.yml
#	Resources/Maps/_Sunrise/Station/marathon.yml
#	Resources/Maps/_Sunrise/Station/oasis.yml
#	Resources/Maps/_Sunrise/Station/packed.yml
#	Resources/Maps/_Sunrise/Station/plasma.yml
#	Resources/Maps/_Sunrise/Station/relic.yml
#	Resources/Maps/_Sunrise/event/clon_mine.yml
#	Resources/Maps/_Sunrise/event/void.yml
#	Resources/Prototypes/Body/Prototypes/vox.yml
#	Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml
#	Resources/Prototypes/Catalog/Fills/Crates/fun.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/heads.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/security.yml
#	Resources/Prototypes/Entities/Clothing/Belt/belts.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml
#	Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml
#	Resources/Prototypes/Entities/Mobs/Species/skeleton.yml
#	Resources/Prototypes/Entities/Objects/Devices/Electronics/base_electronics.yml
#	Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/pistol.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/pistol.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/hitscan.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/access.yml
#	Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
#	Resources/Prototypes/Entities/Structures/Doors/MaterialDoors/material_doors.yml
#	Resources/Prototypes/Entities/Structures/Machines/lathe.yml
#	Resources/Prototypes/Entities/Structures/Specific/Atmospherics/sensor.yml
#	Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml
#	Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml
#	Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml
#	Resources/Prototypes/Loadouts/Miscellaneous/glasses.yml
#	Resources/Prototypes/Loadouts/loadout_groups.yml
#	Resources/Prototypes/Objectives/objectiveGroups.yml
#	Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml
#	Resources/Prototypes/Reagents/medicine.yml
#	Resources/Prototypes/Reagents/narcotics.yml
#	Resources/Prototypes/Reagents/toxins.yml
#	Resources/Prototypes/Recipes/Lathes/Packs/biogen.yml
#	Resources/Prototypes/Recipes/Lathes/Packs/security.yml
#	Resources/Prototypes/Recipes/Lathes/biogen.yml
#	Resources/Prototypes/Roles/Antags/nukeops.yml
#	Resources/Prototypes/Roles/Antags/revolutionary.yml
#	Resources/Prototypes/Roles/Antags/thief.yml
#	Resources/Prototypes/Roles/Antags/traitor.yml
#	Resources/Prototypes/Roles/Antags/wizard.yml
#	Resources/Prototypes/Roles/Antags/zombie.yml
#	Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml
#	Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml
#	Resources/Prototypes/Roles/Jobs/Civilian/chef.yml
#	Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
#	Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml
#	Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml
#	Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml
#	Resources/Prototypes/Roles/Jobs/Medical/chemist.yml
#	Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml
#	Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml
#	Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml
#	Resources/Prototypes/Roles/Jobs/Science/borg.yml
#	Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml
#	Resources/Prototypes/Roles/Jobs/Science/scientist.yml
#	Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml
#	Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
#	Resources/Prototypes/Roles/Jobs/Security/warden.yml
#	Resources/Prototypes/Stacks/Materials/crystals.yml
#	Resources/Prototypes/Stacks/floor_tile_stacks.yml
#	Resources/Prototypes/tags.yml
#	Resources/ServerInfo/Guidebook/Science/ArtifactReports.xml
#	Resources/ServerInfo/Guidebook/Science/Xenoarchaeology.xml
#	Resources/Textures/Clothing/Hands/Gloves/Color/black.rsi/equipped-HAND.png
#	Resources/Textures/Clothing/Hands/Gloves/Color/black.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hardsuits/mime.rsi/meta.json
#	Resources/Textures/Effects/speech.rsi/moth3.png
#	Resources/Textures/Interface/Misc/job_icons.rsi/meta.json
#	Resources/Textures/Structures/Machines/microwave.rsi/meta.json
#	Resources/Textures/Structures/Machines/microwave.rsi/mw.png
#	Resources/Textures/Structures/Machines/microwave.rsi/mw0.png
#	Resources/Textures/Structures/Machines/microwave.rsi/mw_running_unlit.png
#	Resources/Textures/Structures/Machines/microwave.rsi/mw_unlit.png
#	Resources/Textures/Structures/Machines/microwave.rsi/mwb.png
#	Resources/Textures/Structures/Machines/microwave.rsi/mwbloody.png
#	Resources/Textures/Structures/Machines/microwave.rsi/mwbloody0.png
#	Resources/Textures/Structures/Machines/microwave.rsi/mwbloody1.png
#	Resources/Textures/Structures/Machines/microwave.rsi/mwbloodyo.png
#	Resources/Textures/Structures/Machines/microwave.rsi/mwo.png
#	Resources/Textures/Structures/Machines/microwave_syndie.rsi/meta.json
#	Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw.png
#	Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw0.png
#	Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw_running_unlit.png
#	Resources/Textures/Structures/Machines/microwave_syndie.rsi/mw_unlit.png
#	Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwb.png
#	Resources/Textures/Structures/Machines/microwave_syndie.rsi/mwo.png
#	Resources/migration.yml
#	RobustToolbox
2025-10-19 17:38:04 +02:00

397 lines
14 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._Sunrise.CollectiveMind;
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.Network;
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 = '_'; // Russian-Localization
public const char OOCPrefix = '[';
public const char EmotesPrefix = '%'; // Russian-Localization
public const char EmotesAltPrefix = '*';
public const char AdminPrefix = ']';
public const char WhisperPrefix = ',';
public const char DefaultChannelKey = 'р'; // Russian-Localization
public const char CollectiveMindPrefix = '+'; // Sunrise-Edit
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 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, CollectiveMindPrototype> _mindKeyCodes = default!; // Sunrise-Edit
public override void Initialize()
{
base.Initialize();
DebugTools.Assert(_prototypeManager.HasIndex(CommonChannel));
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
CacheRadios();
CacheEmotes();
CacheCollectiveMinds(); // Sunrise-Edit
}
protected virtual void OnPrototypeReload(PrototypesReloadedEventArgs obj)
{
if (obj.WasModified<RadioChannelPrototype>())
CacheRadios();
if (obj.WasModified<EmotePrototype>())
CacheEmotes();
// Sunrise-Start
if (obj.WasModified<CollectiveMindPrototype>())
CacheCollectiveMinds();
// Sunrise-End
}
private void CacheRadios()
{
_keyCodes = _prototypeManager.EnumeratePrototypes<RadioChannelPrototype>()
.ToFrozenDictionary(x => x.KeyCode);
}
// Sunrise-Start
private void CacheCollectiveMinds()
{
_prototypeManager.PrototypesReloaded -= OnPrototypeReload;
_mindKeyCodes = _prototypeManager.EnumeratePrototypes<CollectiveMindPrototype>()
.ToFrozenDictionary(x => x.KeyCode);
}
// Sunrise-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 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;
}
// Sunrise-Start
public bool TryProccessCollectiveMindMessage(
EntityUid source,
string input,
out string output,
out CollectiveMindPrototype? channel,
bool quiet = false)
{
output = input.Trim();
channel = null;
if (input.Length == 0)
return false;
if (!input.StartsWith(CollectiveMindPrefix))
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 (_mindKeyCodes.TryGetValue(channelKey, out channel) || quiet)
return true;
var msg = Loc.GetString("chat-manager-no-such-channel", ("key", channelKey));
_popup.PopupEntity(msg, source, source);
return false;
}
// Sunrise-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>
/// 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
}