# Conflicts: # .github/CODEOWNERS # .github/PULL_REQUEST_TEMPLATE.md # Content.Client/Changelog/ChangelogTab.xaml.cs # Content.Client/Chemistry/UI/ChemMasterWindow.xaml # Content.Client/Entry/EntryPoint.cs # Content.Client/GameTicking/Managers/ClientGameTicker.cs # Content.Client/Launcher/LauncherConnectingGui.xaml # Content.Client/Lobby/LobbyState.cs # Content.Client/Lobby/UI/LobbyGui.xaml # Content.Client/Paper/UI/PaperWindow.xaml.cs # Content.Client/StationRecords/GeneralStationRecordConsoleWindow.xaml.cs # Content.Client/Stylesheets/StyleNano.cs # Content.Client/UserInterface/Screens/SeparatedChatGameScreen.xaml # Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs # Content.Client/UserInterface/Systems/MenuBar/Widgets/GameTopMenuBar.xaml # Content.IntegrationTests/Tests/Destructible/DestructibleDamageGroupTest.cs # Content.IntegrationTests/Tests/PostMapInitTest.cs # Content.IntegrationTests/Tests/Power/StationPowerTests.cs # Content.Packaging/ServerPackaging.cs # Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs # Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs # Content.Server/Atmos/TileAtmosphere.cs # Content.Server/Bible/BibleSystem.cs # Content.Server/Chat/Systems/ChatSystem.cs # Content.Server/Content.Server.csproj # Content.Server/Discord/DiscordLink/DiscordLink.cs # Content.Server/GameTicking/GameTicker.LobbyBackground.cs # Content.Server/GameTicking/GameTicker.Spawning.cs # Content.Server/Ghost/GhostSystem.cs # Content.Server/Kitchen/Components/MicrowaveComponent.cs # Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs # Content.Server/Mech/Systems/MechSystem.cs # Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs # Content.Server/Medical/DefibrillatorSystem.cs # Content.Server/Medical/HealthAnalyzerSystem.cs # Content.Server/Radio/EntitySystems/HeadsetSystem.cs # Content.Server/Remotes/DoorRemoteSystem.cs # Content.Server/RoundEnd/RoundEndSystem.cs # Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs # Content.Server/Silicons/Borgs/BorgSystem.Transponder.cs # Content.Server/Tips/TipsSystem.cs # Content.Server/Traitor/Uplink/UplinkSystem.cs # Content.Server/Weapons/Ranged/Systems/GunSystem.cs # Content.Server/Zombies/ZombieSystem.Transform.cs # Content.Server/Zombies/ZombieSystem.cs # Content.Shared/Bed/BedSystem.cs # Content.Shared/Blocking/BlockingSystem.User.cs # Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs # Content.Shared/Chat/SharedSuicideSystem.cs # Content.Shared/Chemistry/Components/HyposprayComponent.cs # Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs # Content.Shared/Chemistry/EntitySystems/ScoopableSolutionSystem.cs # Content.Shared/Chemistry/EntitySystems/SolutionTransferSystem.cs # Content.Shared/Chemistry/Events/HyposprayEvents.cs # Content.Shared/Content.Shared.csproj # Content.Shared/Damage/Systems/DamageableSystem.cs # Content.Shared/Friction/TileFrictionController.cs # Content.Shared/GameTicking/SharedGameTicker.cs # Content.Shared/Interaction/InteractHand.cs # Content.Shared/Lock/LockSystem.cs # Content.Shared/Medical/Healing/HealingSystem.cs # Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs # Content.Shared/Nutrition/EntitySystems/HungerSystem.cs # Content.Shared/Silicons/StationAi/SharedStationAiSystem.Airlock.cs # Content.Shared/Throwing/ThrowingSystem.cs # Content.Shared/Trigger/Systems/TriggerOnHitscanSystem.cs # Content.Shared/Weapons/Hitscan/Systems/HitscanBasicDamageSystem.cs # Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs # Content.Shared/Weapons/Ranged/Components/BatteryWeaponFireModesComponent.cs # Content.Shared/Weapons/Ranged/Components/HitscanBatteryAmmoProviderComponent.cs # Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs # Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs # Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Battery.cs # Content.Shared/Zombies/ZombieComponent.cs # Content.Shared/_Sunrise/Lobby/LobbyBackgroundPrototype.cs # README.md # Resources/Locale/en-US/_strings/borg/borg.ftl # Resources/Locale/en-US/_strings/chemistry/components/hypospray-component.ftl # Resources/Locale/en-US/_strings/chemistry/components/injector-component.ftl # Resources/Locale/en-US/_strings/commands/pauseatmos-command.ftl # Resources/Locale/en-US/_strings/commands/quick-inspect-command.ftl # Resources/Locale/en-US/_strings/commands/show-wallmounts-command.ftl # Resources/Locale/en-US/_strings/commands/substepatmos-command.ftl # Resources/Locale/en-US/_strings/fluids/components/equip-spray-component.ftl # Resources/Locale/en-US/_strings/game-ticking/game-presets/preset-revolutionary.ftl # Resources/Locale/en-US/_strings/job/job-names.ftl # Resources/Locale/en-US/_strings/lobby/lobby-state-background.ftl # Resources/Locale/en-US/_strings/reagents/meta/medicine.ftl # Resources/Locale/en-US/_strings/reagents/meta/physical-desc.ftl # Resources/Locale/en-US/_strings/silicons/borg-module.ftl # Resources/Locale/en-US/_strings/store/changeling-catalog.ftl # Resources/Locale/en-US/_strings/store/currency.ftl # Resources/Locale/en-US/_strings/store/store.ftl # Resources/Locale/en-US/_strings/store/uplink-catalog.ftl # Resources/Prototypes/Body/Organs/slime.yml # Resources/Prototypes/Catalog/Fills/Backpacks/backpack.yml # Resources/Prototypes/Catalog/Fills/Backpacks/duffelbag.yml # Resources/Prototypes/Catalog/Fills/Boxes/ammunition.yml # Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml # Resources/Prototypes/Catalog/Fills/Crates/antag.yml # Resources/Prototypes/Catalog/Fills/Crates/armory.yml # Resources/Prototypes/Catalog/Fills/Crates/botany.yml # Resources/Prototypes/Catalog/Fills/Crates/fun.yml # Resources/Prototypes/Catalog/Fills/Crates/medical.yml # Resources/Prototypes/Catalog/Fills/Crates/salvage.yml # Resources/Prototypes/Catalog/Fills/Crates/security.yml # Resources/Prototypes/Catalog/Fills/Crates/service.yml # Resources/Prototypes/Catalog/Fills/Crates/syndicate.yml # Resources/Prototypes/Catalog/Fills/Items/belt.yml # Resources/Prototypes/Catalog/Fills/Items/briefcases.yml # Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml # Resources/Prototypes/Catalog/Fills/Items/toolboxes.yml # Resources/Prototypes/Catalog/Fills/Lockers/dressers.yml # Resources/Prototypes/Catalog/Fills/Lockers/heads.yml # Resources/Prototypes/Catalog/Fills/Lockers/security.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/theater.yml # Resources/Prototypes/Catalog/uplink_catalog.yml # Resources/Prototypes/Entities/Clothing/Back/backpacks.yml # Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml # Resources/Prototypes/Entities/Debugging/debug_sweps.yml # Resources/Prototypes/Entities/Debugging/stress_test.yml # Resources/Prototypes/Entities/Markers/Spawners/Random/anomaly.yml # Resources/Prototypes/Entities/Markers/Spawners/Random/toy.yml # Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml # Resources/Prototypes/Entities/Mobs/Cyborgs/xenoborgs.yml # Resources/Prototypes/Entities/Mobs/NPCs/animals.yml # Resources/Prototypes/Entities/Mobs/NPCs/carp.yml # Resources/Prototypes/Entities/Objects/Devices/base_handheld.yml # Resources/Prototypes/Entities/Objects/Fun/Instruments/instruments_misc.yml # Resources/Prototypes/Entities/Objects/Fun/dice_bag.yml # Resources/Prototypes/Entities/Objects/Fun/snap_pops.yml # Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml # Resources/Prototypes/Entities/Objects/Materials/parts.yml # Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml # Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml # Resources/Prototypes/Entities/Objects/Shields/shields.yml # Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml # Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml # Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml # Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml # Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml # Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml # Resources/Prototypes/Entities/Objects/Specific/Service/barber.yml # Resources/Prototypes/Entities/Objects/Tools/glowstick.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Boxes/antimateriel.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Boxes/magnum.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/heavy_rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/light_rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/magnum.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/pistol.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/shotgun.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/light_rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/magnum.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/pistol.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Magazines/rifle.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/grenade.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/SpeedLoaders/magnum.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/SpeedLoaders/pistol.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/SpeedLoaders/rifle_light.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/explosives.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/LMGs/lmgs.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/hitscan.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml # Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml # Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml # Resources/Prototypes/Entities/Structures/Furniture/dresser.yml # Resources/Prototypes/Entities/Structures/Machines/lathe.yml # Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml # Resources/Prototypes/Entities/Structures/Walls/walls.yml # Resources/Prototypes/GameRules/roundstart.yml # Resources/Prototypes/GameRules/unknown_shuttles.yml # Resources/Prototypes/InventoryTemplates/aghost_inventory_template.yml # Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml # Resources/Prototypes/Loadouts/loadout_groups.yml # Resources/Prototypes/Loadouts/role_loadouts.yml # Resources/Prototypes/Maps/Pools/default.yml # Resources/Prototypes/Maps/amber.yml # Resources/Prototypes/Maps/oasis.yml # Resources/Prototypes/Maps/reach.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/Reactions/medicine.yml # Resources/Prototypes/Roles/Jobs/Security/head_of_security.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/Roles/Jobs/Wildcards/zookeeper.yml # Resources/Prototypes/Roles/play_time_trackers.yml # Resources/Prototypes/Shuttles/shuttle_incoming_event.yml # Resources/Prototypes/Species/species_weights.yml # Resources/Prototypes/Store/presets.yml # Resources/Prototypes/ai_factions.yml # Resources/Prototypes/borg_types.yml # Resources/Prototypes/explosion.yml # Resources/Prototypes/game_presets.yml # Resources/Prototypes/lobbyscreens.yml # Resources/Prototypes/silicon-laws.yml # Resources/Textures/Clothing/Back/Duffels/syndicate.rsi/icon-med.png # Resources/Textures/Clothing/Back/Duffels/syndicate.rsi/meta.json # Resources/Textures/Objects/Specific/Mech/mecha.rsi/meta.json # Resources/Textures/Objects/Specific/Mech/mecha.rsi/ripley-broken.png # Resources/Textures/Objects/Specific/Mech/mecha.rsi/ripley-empty.png # Resources/Textures/Objects/Specific/Mech/mecha.rsi/ripley-open.png # Resources/Textures/Objects/Specific/Mech/mecha.rsi/ripley.png # Resources/Textures/Objects/Specific/Mech/ripley_construction.rsi/ripley_chassis.png # Resources/Textures/Objects/Specific/Mech/ripley_construction.rsi/ripley_harness+o.png # Resources/Textures/Objects/Specific/Mech/ripley_construction.rsi/ripley_harness.png # Resources/Textures/Objects/Specific/Medical/hypospray.rsi/meta.json # Resources/Textures/Objects/Specific/Medical/medical.rsi/meta.json # Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles.rsi/meta.json # Resources/Textures/Objects/Weapons/Guns/Projectiles/projectiles2.rsi/meta.json # Resources/Textures/Structures/Storage/canister.rsi/meta.json # Resources/Textures/Structures/Storage/tanks.rsi/meta.json # Resources/migration.yml # RobustToolbox # SECURITY.md # SpaceStation14.sln
546 lines
20 KiB
C#
546 lines
20 KiB
C#
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.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 = '_'; // 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 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;
|
||
}
|
||
|
||
// 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,
|
||
bool isFormatted = false //sunrise-edit
|
||
)
|
||
{ }
|
||
|
||
/// <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)
|
||
{ }
|
||
}
|
||
|
||
/// <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
|
||
}
|