* Перемещение определения работы BlueShieldEnsign и BlueShieldOfficer в закомментированный блок * Обновление URL для книги корпоративного права * Update README.md * Добавление новых плюшевых игрушек CE vulpa и Engie xeno * fix(maps): update centcomm map path and name template Update map path to use _Fish variant and modify station name template to include special operations HQ * add IAA job assets and uniform variants add new job icons, law department badge, and centcom officer uniform variants include localization files and prototype configurations for new assets modify IAA role loadout to use new badge and uniform * fix(localization): correct jumpskirts uniform file path and content Move jumpskirts uniform localization file to correct directory and ensure content is properly formatted * New Ghosts Add a new ghosts from the old fish station * Добавление новых ролей и загрузок для CentComm и BlueShield, включая описание и иконки. * Оперативник «Синий щит» (гост-роль сопровождения ПЦК и тд) и исправления локализации «Синий щит» и ИСН (#2608) * ERT Uplink Tweaks (#2600) * Automatic changelog update * Печать штаба спецопераций (#2599) * Имплант "Жуткий Смех" в набор вора (#2607) * Респрайт Мехов (#2617) * Automatic changelog update * Очередной фикс маркингов (#2635) * fix editor (#2641) * dispacement-drawing-fix (#2642) * reset pos on client restart (#2643) * save pos when closing by button (#2644) * Добавлены новые роли: шеф и врач. Обновлены иконки для новых ролей. * Updated centcom map * Добавлены описания и названия для новых ролей Центкома и «Синий Щит» в английском и русском языках. * add turtle ghost theme and update ghost assets - Add new turtle ghost theme with animations and localization - Replace default ghost assets with custom _Fish variants - Update logo manifest and references to use new ghost sprites - Remove old _Sunrise ghost assets and consolidate under _Fish * update splash logo comment in manifest.yml * remove slippery component from honkbot and update descriptions The slippery component was causing unintended behavior with honkbot movement. Also updated the bot's description in both English and Russian locales to better reflect its nature. * Добавлены новые роли для СЦК и печать Секторального судьи и прочие изменение. * Refactor Centcom Blue Shield identifiers and update job configurations - Changed SpawnPointCentcomBlueShield to SpawnPointCentcommBlueShield for consistency. - Updated centcom_blueshield_officer.yml to change icon from JobIconBlueShield to JobIconNanotrasen. - Adjusted delta.yml to maintain consistent naming conventions for BlueShield roles and added NtrOfficer. - Cleaned up migration.yml by removing unused references to BlueShield lockers and suits. * Обновлены роли в дельте. Изменено количество роли на дельте. * Удалены устаревшие имя роли Центком и добавлены новые имя роли для английского и русского языков * moved duplicate ghost theme file and consolidate strings * Изменено название роли "Офицер Синего Щита Центком" на "оперативник «Синий щит»" для улучшения точности перевода. * Обновлены роли и идентификаторы Центком для улучшения точности и актуальности * update ghost theme sprite path and localization * remove extra blank line in ghost_themes.yml * add von Valantius painting and related assets Add new painting texture, localization files, and prototype for Centcomm von Valantius painting. Includes English and Russian translations. Also reorganize some existing localization files for sectoral judge items. * minor update elizabeth von valantius painting texture * add new audio, textures, and prototypes, major changes in grid spawning and docking system - Add new lobby music tracks and sound effects with proper licensing - Implement new job roles: Oversight Officer and SpecOps Officer - Add new shuttle docking system for special operations teams - Include localization files for new content - Update station maps and job configurations - Add new equipment and weapons for special operations personnel - Implemented updated automatic grid spawning and docking system * update centcom oversight equipment to use correct headset Change headset from Magistrate to FishCentCom variant to match intended equipment specification * update logo-fish-splash.png texture --------- Co-authored-by: VladSukr <vladsukr@gmail.com> Co-authored-by: ReWAFFlution <153686236+ReWAFFlution@users.noreply.github.com> Co-authored-by: SplikZerys <tronezero700@yandex.ru> Co-authored-by: pixarF <sadfrogisfrog@gmail.com> Co-authored-by: KaiserMaus <kaiser.ratte@gmail.com> Co-authored-by: Sunrise-Bot <sunrise.project.top@gmail.com> Co-authored-by: Kendrick <133131761+Kendrick15@users.noreply.github.com> Co-authored-by: haiwwkes <49613070+rhailrake@users.noreply.github.com>
1188 lines
47 KiB
C#
1188 lines
47 KiB
C#
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Content.Server._Sunrise.Chat;
|
|
using Content.Server._Sunrise.Chat.Sanitization;
|
|
using Content.Server.Administration.Logs;
|
|
using Content.Server.Administration.Managers;
|
|
using Content.Server.Chat.Managers;
|
|
using Content.Server.GameTicking;
|
|
using Content.Server.Speech.Prototypes;
|
|
using Content.Server.Speech.EntitySystems;
|
|
using Content.Server.Station.Components;
|
|
using Content.Server.Station.Systems;
|
|
using Content.Shared._Sunrise.Antags.Abductor;
|
|
using Content.Shared._Sunrise.CollectiveMind;
|
|
using Content.Shared.ActionBlocker;
|
|
using Content.Shared.Administration;
|
|
using Content.Shared.CCVar;
|
|
using Content.Shared.Chat;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.Examine;
|
|
using Content.Shared.Ghost;
|
|
using Content.Shared.IdentityManagement;
|
|
using Content.Shared.Mobs.Systems;
|
|
using Content.Shared.Players;
|
|
using Content.Shared.Players.RateLimiting;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Radio;
|
|
using Content.Shared.Whitelist;
|
|
using Robust.Server.Player;
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Configuration;
|
|
using Robust.Shared.Console;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Replays;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Server.Chat.Systems;
|
|
|
|
// TODO refactor whatever active warzone this class and chatmanager have become
|
|
/// <summary>
|
|
/// ChatSystem is responsible for in-simulation chat handling, such as whispering, speaking, emoting, etc.
|
|
/// ChatSystem depends on ChatManager to actually send the messages.
|
|
/// </summary>
|
|
public sealed partial class ChatSystem : SharedChatSystem
|
|
{
|
|
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
|
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
|
[Dependency] private readonly IChatManager _chatManager = default!;
|
|
[Dependency] private readonly IChatSanitizationManager _sanitizer = default!;
|
|
[Dependency] private readonly IAdminManager _adminManager = default!;
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
[Dependency] private readonly IRobustRandom _random = default!;
|
|
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
|
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
|
[Dependency] private readonly StationSystem _stationSystem = default!;
|
|
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
[Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!;
|
|
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
|
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
|
|
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
|
|
|
public const string DefaultAnnouncementSound = "/Audio/Announcements/announce.ogg"; // Fish-edit
|
|
|
|
private bool _loocEnabled = true;
|
|
private bool _deadLoocEnabled;
|
|
private bool _critLoocEnabled;
|
|
private readonly bool _adminLoocEnabled = true;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
CacheEmotes();
|
|
Subs.CVar(_configurationManager, CCVars.LoocEnabled, OnLoocEnabledChanged, true);
|
|
Subs.CVar(_configurationManager, CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged, true);
|
|
Subs.CVar(_configurationManager, CCVars.CritLoocEnabled, OnCritLoocEnabledChanged, true);
|
|
|
|
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameChange);
|
|
}
|
|
|
|
private void OnLoocEnabledChanged(bool val)
|
|
{
|
|
if (_loocEnabled == val) return;
|
|
|
|
_loocEnabled = val;
|
|
_chatManager.DispatchServerAnnouncement(
|
|
Loc.GetString(val ? "chat-manager-looc-chat-enabled-message" : "chat-manager-looc-chat-disabled-message"));
|
|
}
|
|
|
|
private void OnDeadLoocEnabledChanged(bool val)
|
|
{
|
|
if (_deadLoocEnabled == val) return;
|
|
|
|
_deadLoocEnabled = val;
|
|
_chatManager.DispatchServerAnnouncement(
|
|
Loc.GetString(val ? "chat-manager-dead-looc-chat-enabled-message" : "chat-manager-dead-looc-chat-disabled-message"));
|
|
}
|
|
|
|
private void OnCritLoocEnabledChanged(bool val)
|
|
{
|
|
if (_critLoocEnabled == val)
|
|
return;
|
|
|
|
_critLoocEnabled = val;
|
|
_chatManager.DispatchServerAnnouncement(
|
|
Loc.GetString(val ? "chat-manager-crit-looc-chat-enabled-message" : "chat-manager-crit-looc-chat-disabled-message"));
|
|
}
|
|
|
|
private void OnGameChange(GameRunLevelChangedEvent ev)
|
|
{
|
|
switch (ev.New)
|
|
{
|
|
case GameRunLevel.InRound:
|
|
if (!_configurationManager.GetCVar(CCVars.OocEnableDuringRound))
|
|
_configurationManager.SetCVar(CCVars.OocEnabled, false);
|
|
break;
|
|
case GameRunLevel.PostRound:
|
|
case GameRunLevel.PreRoundLobby:
|
|
if (!_configurationManager.GetCVar(CCVars.OocEnableDuringRound))
|
|
_configurationManager.SetCVar(CCVars.OocEnabled, true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
public 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,
|
|
bool isFormatted = false) // sunrise-edit
|
|
{
|
|
TrySendInGameICMessage(source, message, desiredType, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, hideLog, shell, player, nameOverride, checkRadioPrefix, ignoreActionBlocker, isFormatted); //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="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="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 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,
|
|
bool isFormatted = false //sunrise-edit
|
|
)
|
|
{
|
|
if (TryComp<AbductorComponent>(source, out var comp))
|
|
{
|
|
_prototypeManager.TryIndex<CollectiveMindPrototype>(comp.AbductorCollectiveMindProto, out var channel);
|
|
SendCollectiveMindChat(source, message, channel);
|
|
return;
|
|
}
|
|
if (HasComp<GhostComponent>(source))
|
|
{
|
|
// Ghosts can only send dead chat messages, so we'll forward it to InGame OOC.
|
|
TrySendInGameOOCMessage(source, message, InGameOOCChatType.Dead, range == ChatTransmitRange.HideChat, shell, player);
|
|
return;
|
|
}
|
|
|
|
if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed)
|
|
return;
|
|
|
|
// Sus
|
|
if (player?.AttachedEntity is { Valid: true } entity && source != entity)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanSendInGame(message, shell, player))
|
|
return;
|
|
|
|
// Sunrise added start - для санитизации чата
|
|
var trySendEvent = new TrySendChatMessageEvent(message, desiredType);
|
|
RaiseLocalEvent(source, trySendEvent);
|
|
|
|
if (trySendEvent.Cancelled)
|
|
return;
|
|
|
|
message = trySendEvent.Message;
|
|
// Sunrise added end
|
|
|
|
ignoreActionBlocker = CheckIgnoreSpeechBlocker(source, ignoreActionBlocker);
|
|
|
|
// this method is a disaster
|
|
// every second i have to spend working with this code is fucking agony
|
|
// scientists have to wonder how any of this was merged
|
|
// coding any game admin feature that involves chat code is pure torture
|
|
// changing even 10 lines of code feels like waterboarding myself
|
|
// and i dont feel like vibe checking 50 code paths
|
|
// so we set this here
|
|
// todo free me from chat code
|
|
if (player != null)
|
|
{
|
|
_chatManager.EnsurePlayer(player.UserId).AddEntity(GetNetEntity(source));
|
|
}
|
|
|
|
if (desiredType == InGameICChatType.Speak && message.StartsWith(LocalPrefix))
|
|
{
|
|
// prevent radios and remove prefix.
|
|
checkRadioPrefix = false;
|
|
message = message[1..];
|
|
}
|
|
|
|
bool shouldCapitalize = (desiredType != InGameICChatType.Emote);
|
|
bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation);
|
|
// Capitalizing the word I only happens in English, so we check language here
|
|
bool shouldCapitalizeTheWordI = (!CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Parent.Name == "en")
|
|
|| (CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Name == "en");
|
|
|
|
message = SanitizeInGameICMessage(source, message, out var emoteStr, shouldCapitalize, shouldPunctuate, shouldCapitalizeTheWordI);
|
|
|
|
// Was there an emote in the message? If so, send it.
|
|
if (player != null && emoteStr != message && emoteStr != null)
|
|
{
|
|
SendEntityEmote(source, emoteStr, range, nameOverride, ignoreActionBlocker, isFormatted); //sunrise-edit
|
|
}
|
|
|
|
// This can happen if the entire string is sanitized out.
|
|
if (string.IsNullOrEmpty(message))
|
|
return;
|
|
|
|
// This message may have a radio prefix, and should then be whispered to the resolved radio channel
|
|
if (checkRadioPrefix)
|
|
{
|
|
if (TryProccessRadioMessage(source, message, out var modMessage, out var channel))
|
|
{
|
|
SendEntityWhisper(source, modMessage, range, channel, nameOverride, hideLog, ignoreActionBlocker, isFormatted); //sunrise-edit
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Sunrise-Start
|
|
if (desiredType == InGameICChatType.CollectiveMind)
|
|
{
|
|
if (TryProccessCollectiveMindMessage(source, message, out var modMessage, out var channel))
|
|
{
|
|
SendCollectiveMindChat(source, modMessage, channel);
|
|
return;
|
|
}
|
|
}
|
|
// Sunrise-End
|
|
|
|
// Otherwise, send whatever type.
|
|
switch (desiredType)
|
|
{
|
|
case InGameICChatType.Speak:
|
|
SendEntitySpeak(source, message, range, nameOverride, hideLog, ignoreActionBlocker, isFormatted); //sunrise-edit-hg
|
|
break;
|
|
case InGameICChatType.Whisper:
|
|
SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker, isFormatted); // sunrise-edit
|
|
break;
|
|
case InGameICChatType.Emote:
|
|
SendEntityEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker, isFormatted: isFormatted); //sunrise-edit
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void TrySendInGameOOCMessage(
|
|
EntityUid source,
|
|
string message,
|
|
InGameOOCChatType type,
|
|
bool hideChat,
|
|
IConsoleShell? shell = null,
|
|
ICommonSession? player = null
|
|
)
|
|
{
|
|
if (!CanSendInGame(message, shell, player))
|
|
return;
|
|
|
|
if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed)
|
|
return;
|
|
|
|
// It doesn't make any sense for a non-player to send in-game OOC messages, whereas non-players may be sending
|
|
// in-game IC messages.
|
|
if (player?.AttachedEntity is not { Valid: true } entity || source != entity)
|
|
return;
|
|
|
|
// Sunrise added start - для санитизации чата
|
|
var trySendEvent = new TrySendChatMessageEvent(message, oocChatType: type);
|
|
RaiseLocalEvent(source, trySendEvent);
|
|
|
|
if (trySendEvent.Cancelled)
|
|
return;
|
|
|
|
message = trySendEvent.Message;
|
|
// Sunrise added end
|
|
|
|
message = SanitizeInGameOOCMessage(message);
|
|
|
|
var sendType = type;
|
|
// If dead player LOOC is disabled, unless you are an admin with Moderator perms, send dead messages to dead chat
|
|
if ((_adminManager.IsAdmin(player) && _adminManager.HasAdminFlag(player, AdminFlags.Moderator)) // Override if admin
|
|
|| _deadLoocEnabled
|
|
|| (!HasComp<GhostComponent>(source) && !_mobStateSystem.IsDead(source))) // Check that player is not dead
|
|
{
|
|
}
|
|
else
|
|
sendType = InGameOOCChatType.Dead;
|
|
|
|
// If crit player LOOC is disabled, don't send the message at all.
|
|
if (!_critLoocEnabled && _mobStateSystem.IsCritical(source))
|
|
return;
|
|
|
|
switch (sendType)
|
|
{
|
|
case InGameOOCChatType.Dead:
|
|
SendDeadChat(source, player, message, hideChat);
|
|
break;
|
|
case InGameOOCChatType.Looc:
|
|
SendLOOC(source, player, message, hideChat);
|
|
break;
|
|
}
|
|
}
|
|
|
|
#region Announcements
|
|
|
|
/// <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="playTts"></param>
|
|
/// <param name="colorOverride">Optional color for the announcement message</param>
|
|
public void DispatchGlobalAnnouncement(
|
|
string message,
|
|
string? sender = null,
|
|
bool playDefault = true,
|
|
SoundSpecifier? announcementSound = null,
|
|
bool playTts = true, // Sunrise-edit,
|
|
string? announceVoice = null,
|
|
Color? colorOverride = null
|
|
)
|
|
{
|
|
sender ??= Loc.GetString("chat-manager-sender-announcement");
|
|
|
|
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
|
|
_chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
|
|
|
|
// Sunrise-start
|
|
if (playDefault && announcementSound == null)
|
|
{
|
|
announcementSound ??= new SoundPathSpecifier(DefaultAnnouncementSound);
|
|
}
|
|
|
|
if (playTts && announcementSound != null)
|
|
{
|
|
var announcementEv = new AnnouncementSpokeEvent(Filter.Broadcast(), message, _audio.ResolveSound(announcementSound), announceVoice);
|
|
RaiseLocalEvent(announcementEv);
|
|
}
|
|
// Sunrise-end
|
|
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}");
|
|
}
|
|
|
|
/// <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="playDefaultSound">Play the announcement sound</param>
|
|
/// <param name="announcementSound">Sound to play</param>
|
|
/// <param name="colorOverride">Optional color for the announcement message</param>
|
|
public void DispatchFilteredAnnouncement(
|
|
Filter filter,
|
|
string message,
|
|
EntityUid? source = null,
|
|
string? sender = null,
|
|
bool playDefault = true, // Sunrise-edit
|
|
bool playTts = true, // Sunrise-edit
|
|
string? announceVoice = null, // Sunrise-edit
|
|
SoundSpecifier? announcementSound = null,
|
|
Color? colorOverride = null)
|
|
{
|
|
sender ??= Loc.GetString("chat-manager-sender-announcement");
|
|
|
|
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
|
|
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source ?? default, false, true, colorOverride);
|
|
// Sunrise-start
|
|
if (playDefault && announcementSound == null)
|
|
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
|
|
|
|
if (playTts && announcementSound != null)
|
|
{
|
|
RaiseLocalEvent(new AnnouncementSpokeEvent(filter, message, _audio.ResolveSound(announcementSound), announceVoice));
|
|
}
|
|
// Sunrise-edit
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement from {sender}: {message}");
|
|
}
|
|
|
|
/// <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="playSound"></param>
|
|
/// <param name="playTts"></param>
|
|
/// <param name="colorOverride">Optional color for the announcement message</param>
|
|
public void DispatchStationAnnouncement(
|
|
EntityUid source,
|
|
string message,
|
|
string? sender = null,
|
|
bool playDefault = true, // Sunrise-edit
|
|
bool playTts = true, // Sunrise-edit
|
|
Color? colorOverride = null,
|
|
string? announceVoice = null, // Sunrise-edit
|
|
SoundSpecifier? announcementSound = null)
|
|
{
|
|
sender ??= Loc.GetString("chat-manager-sender-announcement");
|
|
|
|
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
|
|
var station = _stationSystem.GetOwningStation(source);
|
|
|
|
if (station == null)
|
|
{
|
|
// you can't make a station announcement without a station
|
|
return;
|
|
}
|
|
|
|
if (!EntityManager.TryGetComponent<StationDataComponent>(station, out var stationDataComp)) return;
|
|
|
|
var filter = _stationSystem.GetInStation(stationDataComp);
|
|
|
|
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source, false, true, colorOverride);
|
|
|
|
// Sunrise-start
|
|
if (playDefault && announcementSound == null)
|
|
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
|
|
|
|
if (playTts && announcementSound != null)
|
|
{
|
|
RaiseLocalEvent(new AnnouncementSpokeEvent(filter, message, _audio.ResolveSound(announcementSound), announceVoice));
|
|
}
|
|
// Sunrise-edit
|
|
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement on {station} from {sender}: {message}");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private API
|
|
|
|
// Sunrise-Start
|
|
private void SendCollectiveMindChat(EntityUid source, string message, CollectiveMindPrototype? collectiveMind)
|
|
{
|
|
if (_mobStateSystem.IsDead(source))
|
|
return;
|
|
|
|
if (collectiveMind == null || message == "")
|
|
return;
|
|
|
|
if (!TryComp<CollectiveMindComponent>(source, out var sourseCollectiveMindComp))
|
|
return;
|
|
|
|
if (!sourseCollectiveMindComp.Minds.Contains(collectiveMind.ID))
|
|
return;
|
|
|
|
// Sunrise added start - для санитизации чата
|
|
var trySendEvent = new TrySendChatMessageEvent(message, InGameICChatType.CollectiveMind);
|
|
RaiseLocalEvent(source, trySendEvent);
|
|
|
|
if (trySendEvent.Cancelled)
|
|
return;
|
|
|
|
message = trySendEvent.Message;
|
|
// Sunrise added end
|
|
|
|
var clients = Filter.Empty();
|
|
var mindQuery = EntityQueryEnumerator<CollectiveMindComponent, ActorComponent>();
|
|
while (mindQuery.MoveNext(out var uid, out var collectMindComp, out var actorComp))
|
|
{
|
|
if (_mobStateSystem.IsDead(uid))
|
|
continue;
|
|
|
|
if (collectMindComp.Minds.Contains(collectiveMind.ID))
|
|
{
|
|
clients.AddPlayer(actorComp.PlayerSession);
|
|
}
|
|
}
|
|
|
|
var admins = _adminManager.ActiveAdmins
|
|
.Select(p => p.Channel);
|
|
string messageWrap;
|
|
string adminMessageWrap;
|
|
|
|
messageWrap = Loc.GetString("collective-mind-chat-wrap-message",
|
|
("message", message),
|
|
("channel", collectiveMind.LocalizedName));
|
|
|
|
adminMessageWrap = Loc.GetString("collective-mind-chat-wrap-message-admin",
|
|
("source", source),
|
|
("message", message),
|
|
("channel", collectiveMind.LocalizedName));
|
|
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"CollectiveMind chat from {ToPrettyString(source):Player}: {message}");
|
|
|
|
_chatManager.ChatMessageToManyFiltered(clients,
|
|
ChatChannel.CollectiveMind,
|
|
message,
|
|
messageWrap,
|
|
source,
|
|
false,
|
|
true,
|
|
collectiveMind.Color);
|
|
|
|
_chatManager.ChatMessageToMany(ChatChannel.CollectiveMind,
|
|
message,
|
|
adminMessageWrap,
|
|
source,
|
|
false,
|
|
true,
|
|
admins,
|
|
collectiveMind.Color);
|
|
}
|
|
// Sunrise-End
|
|
|
|
private void SendEntitySpeak(
|
|
EntityUid source,
|
|
string originalMessage,
|
|
ChatTransmitRange range,
|
|
string? nameOverride,
|
|
bool hideLog = false,
|
|
bool ignoreActionBlocker = false,
|
|
bool isFormatted = false //sunrise-edit
|
|
)
|
|
{
|
|
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
|
|
return;
|
|
|
|
var message = TransformSpeech(source, originalMessage);
|
|
|
|
if (message.Length == 0)
|
|
return;
|
|
|
|
var speech = GetSpeechVerb(source, message);
|
|
|
|
// get the entity's apparent name (if no override provided).
|
|
string name;
|
|
if (nameOverride != null)
|
|
{
|
|
name = nameOverride;
|
|
}
|
|
else
|
|
{
|
|
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
|
|
RaiseLocalEvent(source, nameEv);
|
|
name = nameEv.VoiceName;
|
|
// Check for a speech verb override
|
|
if (nameEv.SpeechVerb != null && _prototypeManager.TryIndex(nameEv.SpeechVerb, out var proto))
|
|
speech = proto;
|
|
}
|
|
|
|
name = FormattedMessage.EscapeText(name);
|
|
if (HasComp<CanFormatMessagesComponent>(source)) isFormatted = true; //sunrise-edit
|
|
var wrappedMessage = Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message",
|
|
("entityName", name),
|
|
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
|
|
("fontType", speech.FontId),
|
|
("fontSize", speech.FontSize),
|
|
("message", isFormatted ? message : FormattedMessage.EscapeText(message))); //sunrise-edit
|
|
|
|
SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range);
|
|
|
|
var ev = new EntitySpokeEvent(source, message, originalMessage, null, null);
|
|
RaiseLocalEvent(source, ev, true);
|
|
|
|
// To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc.
|
|
// Also doesn't log if hideLog is true.
|
|
if (!HasComp<ActorComponent>(source) || hideLog)
|
|
return;
|
|
|
|
if (originalMessage == message)
|
|
{
|
|
if (name != Name(source))
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user} as {name}: {originalMessage}.");
|
|
else
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}: {originalMessage}.");
|
|
}
|
|
else
|
|
{
|
|
if (name != Name(source))
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
|
$"Say from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}.");
|
|
else
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
|
$"Say from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
|
|
}
|
|
}
|
|
|
|
private void SendEntityWhisper(
|
|
EntityUid source,
|
|
string originalMessage,
|
|
ChatTransmitRange range,
|
|
RadioChannelPrototype? channel,
|
|
string? nameOverride,
|
|
bool hideLog = false,
|
|
bool ignoreActionBlocker = false,
|
|
bool isFormatted = false //sunrise-edit
|
|
)
|
|
{
|
|
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
|
|
return;
|
|
|
|
var message = TransformSpeech(source, FormattedMessage.RemoveMarkupOrThrow(originalMessage));
|
|
if (message.Length == 0)
|
|
return;
|
|
|
|
var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f);
|
|
|
|
// get the entity's name by visual identity (if no override provided).
|
|
string nameIdentity = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(source, EntityManager));
|
|
// get the entity's name by voice (if no override provided).
|
|
string name;
|
|
if (nameOverride != null)
|
|
{
|
|
name = nameOverride;
|
|
}
|
|
else
|
|
{
|
|
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
|
|
RaiseLocalEvent(source, nameEv);
|
|
name = nameEv.VoiceName;
|
|
}
|
|
name = FormattedMessage.EscapeText(name);
|
|
if (HasComp<CanFormatMessagesComponent>(source)) isFormatted = true; //sunrise-edit
|
|
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
|
|
("entityName", name), ("message", isFormatted ? message : FormattedMessage.EscapeText(message))); //sunrise-edit
|
|
|
|
var wrappedobfuscatedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
|
|
("entityName", nameIdentity), ("message", isFormatted ? obfuscatedMessage : FormattedMessage.EscapeText(obfuscatedMessage))); //sunrise-edit
|
|
|
|
var wrappedUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message",
|
|
("message", isFormatted ? obfuscatedMessage : FormattedMessage.EscapeText(obfuscatedMessage))); //sunrise-edit
|
|
|
|
|
|
foreach (var (session, data) in GetRecipients(source, WhisperMuffledRange))
|
|
{
|
|
EntityUid listener;
|
|
|
|
if (session.AttachedEntity is not { Valid: true } playerEntity)
|
|
continue;
|
|
listener = session.AttachedEntity.Value;
|
|
|
|
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full)
|
|
continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them.
|
|
|
|
if (data.Range <= WhisperClearRange)
|
|
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel);
|
|
//If listener is too far, they only hear fragments of the message
|
|
else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange))
|
|
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.Channel);
|
|
//If listener is too far and has no line of sight, they can't identify the whisperer's identity
|
|
else
|
|
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.Channel);
|
|
}
|
|
|
|
_replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
|
|
|
|
var ev = new EntitySpokeEvent(source, message, originalMessage, channel, obfuscatedMessage);
|
|
RaiseLocalEvent(source, ev, true);
|
|
if (!hideLog)
|
|
if (originalMessage == message)
|
|
{
|
|
if (name != Name(source))
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user} as {name}: {originalMessage}.");
|
|
else
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}: {originalMessage}.");
|
|
}
|
|
else
|
|
{
|
|
if (name != Name(source))
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
|
$"Whisper from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}.");
|
|
else
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
|
$"Whisper from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
|
|
}
|
|
}
|
|
|
|
private 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
|
|
)
|
|
{
|
|
if (!_actionBlocker.CanEmote(source) && !ignoreActionBlocker)
|
|
return;
|
|
|
|
// get the entity's apparent name (if no override provided).
|
|
var ent = Identity.Entity(source, EntityManager);
|
|
string name = FormattedMessage.EscapeText(nameOverride ?? Name(ent));
|
|
if (HasComp<CanFormatMessagesComponent>(source)) isFormatted = true; //sunrise-edit
|
|
// Emotes use Identity.Name, since it doesn't actually involve your voice at all.
|
|
var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
|
|
("entityName", isFormatted ? "" : name), //sunrise-edit
|
|
("entity", ent),
|
|
("message", isFormatted ? action : FormattedMessage.RemoveMarkupOrThrow(action)));
|
|
|
|
if (checkEmote)
|
|
TryEmoteChatInput(source, action); // sunrise-start
|
|
|
|
foreach (var (session, data) in GetRecipients(source, VoiceRange))
|
|
{
|
|
EntityUid listener;
|
|
|
|
if (session.AttachedEntity is not { Valid: true } playerEntity)
|
|
continue;
|
|
|
|
listener = session.AttachedEntity.Value;
|
|
|
|
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full)
|
|
continue;
|
|
|
|
if (_examineSystem.InRangeUnOccluded(source, listener, VoiceRange))
|
|
{
|
|
_chatManager.ChatMessageToOne(ChatChannel.Emotes, action, wrappedMessage, source, false, session.Channel);
|
|
}
|
|
} // sunrise-end
|
|
if (!hideLog)
|
|
if (name != Name(source))
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}");
|
|
else
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}");
|
|
}
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
private void SendLOOC(EntityUid source, ICommonSession player, string message, bool hideChat)
|
|
{
|
|
var name = FormattedMessage.EscapeText(Identity.Name(source, EntityManager));
|
|
|
|
if (_adminManager.IsAdmin(player))
|
|
{
|
|
if (!_adminLoocEnabled) return;
|
|
}
|
|
else if (!_loocEnabled) return;
|
|
|
|
// If crit player LOOC is disabled, don't send the message at all.
|
|
if (!_critLoocEnabled && _mobStateSystem.IsCritical(source))
|
|
return;
|
|
|
|
var wrappedMessage = Loc.GetString("chat-manager-entity-looc-wrap-message",
|
|
("entityName", name),
|
|
("message", FormattedMessage.EscapeText(message)));
|
|
|
|
SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, player.UserId);
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}");
|
|
}
|
|
|
|
private void SendDeadChat(EntityUid source, ICommonSession player, string message, bool hideChat)
|
|
{
|
|
var clients = GetDeadChatClients();
|
|
var playerName = Name(source);
|
|
string wrappedMessage;
|
|
if (_adminManager.IsAdmin(player))
|
|
{
|
|
wrappedMessage = Loc.GetString("chat-manager-send-admin-dead-chat-wrap-message",
|
|
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
|
("userName", player.Channel.UserName),
|
|
("message", FormattedMessage.EscapeText(message)));
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {player:Player}: {message}");
|
|
}
|
|
else
|
|
{
|
|
wrappedMessage = Loc.GetString("chat-manager-send-dead-chat-wrap-message",
|
|
("deadChannelName", Loc.GetString("chat-manager-dead-channel-name")),
|
|
("playerName", (playerName)),
|
|
("message", FormattedMessage.EscapeText(message)));
|
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {player:Player}: {message}");
|
|
}
|
|
|
|
_chatManager.ChatMessageToMany(ChatChannel.Dead, message, wrappedMessage, source, hideChat, true, clients.ToList(), author: player.UserId);
|
|
}
|
|
#endregion
|
|
|
|
#region Utility
|
|
|
|
private enum MessageRangeCheckResult
|
|
{
|
|
Disallowed,
|
|
HideChat,
|
|
Full
|
|
}
|
|
|
|
/// <summary>
|
|
/// If hideChat should be set as far as replays are concerned.
|
|
/// </summary>
|
|
private bool MessageRangeHideChatForReplay(ChatTransmitRange range)
|
|
{
|
|
return range == ChatTransmitRange.HideChat;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a target as returned from GetRecipients should receive the message.
|
|
/// Keep in mind data.Range is -1 for out of range observers.
|
|
/// </summary>
|
|
private MessageRangeCheckResult MessageRangeCheck(ICommonSession session, ICChatRecipientData data, ChatTransmitRange range)
|
|
{
|
|
var initialResult = MessageRangeCheckResult.Full;
|
|
switch (range)
|
|
{
|
|
case ChatTransmitRange.Normal:
|
|
initialResult = MessageRangeCheckResult.Full;
|
|
break;
|
|
case ChatTransmitRange.GhostRangeLimit:
|
|
initialResult = (data.Observer && data.Range < 0 && !_adminManager.IsAdmin(session)) ? MessageRangeCheckResult.HideChat : MessageRangeCheckResult.Full;
|
|
break;
|
|
case ChatTransmitRange.HideChat:
|
|
initialResult = MessageRangeCheckResult.HideChat;
|
|
break;
|
|
case ChatTransmitRange.NoGhosts:
|
|
initialResult = (data.Observer && !_adminManager.IsAdmin(session)) ? MessageRangeCheckResult.Disallowed : MessageRangeCheckResult.Full;
|
|
break;
|
|
}
|
|
var insistHideChat = data.HideChatOverride ?? false;
|
|
var insistNoHideChat = !(data.HideChatOverride ?? true);
|
|
if (insistHideChat && initialResult == MessageRangeCheckResult.Full)
|
|
return MessageRangeCheckResult.HideChat;
|
|
if (insistNoHideChat && initialResult == MessageRangeCheckResult.HideChat)
|
|
return MessageRangeCheckResult.Full;
|
|
return initialResult;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends a chat message to the given players in range of the source entity.
|
|
/// </summary>
|
|
public void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, ChatTransmitRange range, NetUserId? author = null, Color? color = null)
|
|
{
|
|
foreach (var (session, data) in GetRecipients(source, VoiceRange))
|
|
{
|
|
var entRange = MessageRangeCheck(session, data, range);
|
|
if (entRange == MessageRangeCheckResult.Disallowed)
|
|
continue;
|
|
var entHideChat = entRange == MessageRangeCheckResult.HideChat;
|
|
_chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author, colorOverride: color);
|
|
}
|
|
|
|
_replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given player is 'allowed' to send the given message, false otherwise.
|
|
/// </summary>
|
|
private bool CanSendInGame(string message, IConsoleShell? shell = null, ICommonSession? player = null)
|
|
{
|
|
// Non-players don't have to worry about these restrictions.
|
|
if (player == null)
|
|
return true;
|
|
|
|
var mindContainerComponent = player.ContentData()?.Mind;
|
|
|
|
if (mindContainerComponent == null)
|
|
{
|
|
shell?.WriteError("You don't have a mind!");
|
|
return false;
|
|
}
|
|
|
|
if (player.AttachedEntity is not { Valid: true } _)
|
|
{
|
|
shell?.WriteError("You don't have an entity!");
|
|
return false;
|
|
}
|
|
|
|
return !_chatManager.MessageCharacterLimit(player, message);
|
|
}
|
|
|
|
// ReSharper disable once InconsistentNaming
|
|
private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false, bool capitalizeTheWordI = true)
|
|
{
|
|
var newMessage = SanitizeMessageReplaceWords(message.Trim());
|
|
|
|
GetRadioKeycodePrefix(source, newMessage, out newMessage, out var prefix);
|
|
|
|
// Sanitize it first as it might change the word order
|
|
_sanitizer.TrySanitizeEmoteShorthands(newMessage, source, out newMessage, out emoteStr);
|
|
|
|
if (capitalize)
|
|
newMessage = SanitizeMessageCapital(newMessage);
|
|
if (capitalizeTheWordI)
|
|
newMessage = SanitizeMessageCapitalizeTheWordI(newMessage, "i");
|
|
if (punctuate)
|
|
newMessage = SanitizeMessagePeriod(newMessage);
|
|
|
|
return prefix + newMessage;
|
|
}
|
|
|
|
private string SanitizeInGameOOCMessage(string message)
|
|
{
|
|
var newMessage = message.Trim();
|
|
newMessage = FormattedMessage.EscapeText(newMessage);
|
|
|
|
return newMessage;
|
|
}
|
|
|
|
public string TransformSpeech(EntityUid sender, string message)
|
|
{
|
|
var ev = new TransformSpeechEvent(sender, message);
|
|
RaiseLocalEvent(ev);
|
|
|
|
return ev.Message;
|
|
}
|
|
|
|
public bool CheckIgnoreSpeechBlocker(EntityUid sender, bool ignoreBlocker)
|
|
{
|
|
if (ignoreBlocker)
|
|
return ignoreBlocker;
|
|
|
|
var ev = new CheckIgnoreSpeechBlockerEvent(sender, ignoreBlocker);
|
|
RaiseLocalEvent(sender, ev, true);
|
|
|
|
return ev.IgnoreBlocker;
|
|
}
|
|
|
|
private IEnumerable<INetChannel> GetDeadChatClients()
|
|
{
|
|
return Filter.Empty()
|
|
.AddWhereAttachedEntity(HasComp<GhostComponent>)
|
|
.Recipients
|
|
.Union(_adminManager.ActiveAdmins)
|
|
.Select(p => p.Channel);
|
|
}
|
|
|
|
private string SanitizeMessagePeriod(string message)
|
|
{
|
|
if (string.IsNullOrEmpty(message))
|
|
return message;
|
|
// Adds a period if the last character is a letter.
|
|
if (char.IsLetter(message[^1]))
|
|
message += ".";
|
|
return message;
|
|
}
|
|
|
|
[ValidatePrototypeId<ReplacementAccentPrototype>]
|
|
public const string ChatSanitizeAccent = "chatsanitize_sunrise"; // Sunrise-Edit
|
|
|
|
public string SanitizeMessageReplaceWords(string message)
|
|
{
|
|
if (string.IsNullOrEmpty(message)) return message;
|
|
|
|
var msg = message;
|
|
|
|
msg = _wordreplacement.ApplyReplacements(msg, ChatSanitizeAccent);
|
|
|
|
return msg;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns list of players and ranges for all players withing some range. Also returns observers with a range of -1.
|
|
/// </summary>
|
|
private Dictionary<ICommonSession, ICChatRecipientData> GetRecipients(EntityUid source, float voiceGetRange)
|
|
{
|
|
// TODO proper speech occlusion
|
|
|
|
var recipients = new Dictionary<ICommonSession, ICChatRecipientData>();
|
|
var ghostHearing = GetEntityQuery<GhostHearingComponent>();
|
|
var xforms = GetEntityQuery<TransformComponent>();
|
|
|
|
var transformSource = xforms.GetComponent(source);
|
|
var sourceMapId = transformSource.MapID;
|
|
var sourceCoords = transformSource.Coordinates;
|
|
|
|
foreach (var player in _playerManager.Sessions)
|
|
{
|
|
if (player.AttachedEntity is not { Valid: true } playerEntity)
|
|
continue;
|
|
|
|
var transformEntity = xforms.GetComponent(playerEntity);
|
|
|
|
if (transformEntity.MapID != sourceMapId)
|
|
continue;
|
|
|
|
var observer = ghostHearing.HasComponent(playerEntity);
|
|
|
|
// even if they are a ghost hearer, in some situations we still need the range
|
|
if (sourceCoords.TryDistance(EntityManager, transformEntity.Coordinates, out var distance) && distance < voiceGetRange)
|
|
{
|
|
recipients.Add(player, new ICChatRecipientData(distance, observer));
|
|
continue;
|
|
}
|
|
|
|
if (observer)
|
|
recipients.Add(player, new ICChatRecipientData(-1, true));
|
|
}
|
|
|
|
RaiseLocalEvent(new ExpandICChatRecipientsEvent(source, voiceGetRange, recipients));
|
|
return recipients;
|
|
}
|
|
|
|
public readonly record struct ICChatRecipientData(float Range, bool Observer, bool? HideChatOverride = null)
|
|
{
|
|
}
|
|
|
|
private string ObfuscateMessageReadability(string message, float chance)
|
|
{
|
|
var modifiedMessage = new StringBuilder(message);
|
|
|
|
for (var i = 0; i < message.Length; i++)
|
|
{
|
|
if (char.IsWhiteSpace((modifiedMessage[i])))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (_random.Prob(1 - chance))
|
|
{
|
|
modifiedMessage[i] = '~';
|
|
}
|
|
}
|
|
|
|
return modifiedMessage.ToString();
|
|
}
|
|
|
|
public string BuildGibberishString(IReadOnlyList<char> charOptions, int length)
|
|
{
|
|
var sb = new StringBuilder();
|
|
for (var i = 0; i < length; i++)
|
|
{
|
|
sb.Append(_random.Pick(charOptions));
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// This event is raised before chat messages are sent out to clients. This enables some systems to send the chat
|
|
/// messages to otherwise out-of view entities (e.g. for multiple viewports from cameras).
|
|
/// </summary>
|
|
public record ExpandICChatRecipientsEvent(EntityUid Source, float VoiceRange, Dictionary<ICommonSession, ChatSystem.ICChatRecipientData> Recipients)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised broadcast in order to transform speech.transmit
|
|
/// </summary>
|
|
public sealed class TransformSpeechEvent : EntityEventArgs
|
|
{
|
|
public EntityUid Sender;
|
|
public string Message;
|
|
|
|
public TransformSpeechEvent(EntityUid sender, string message)
|
|
{
|
|
Sender = sender;
|
|
Message = message;
|
|
}
|
|
}
|
|
|
|
public sealed class CheckIgnoreSpeechBlockerEvent : EntityEventArgs
|
|
{
|
|
public EntityUid Sender;
|
|
public bool IgnoreBlocker;
|
|
|
|
public CheckIgnoreSpeechBlockerEvent(EntityUid sender, bool ignoreBlocker)
|
|
{
|
|
Sender = sender;
|
|
IgnoreBlocker = ignoreBlocker;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised on an entity when it speaks, either through 'say' or 'whisper'.
|
|
/// </summary>
|
|
public sealed class EntitySpokeEvent : EntityEventArgs
|
|
{
|
|
public readonly EntityUid Source;
|
|
public readonly string Message;
|
|
public readonly string? ObfuscatedMessage; // not null if this was a whisper
|
|
public readonly string OriginalMessage; // Sunrise-TTS
|
|
public readonly bool IsRadio; // Sunrise-TTS
|
|
|
|
/// <summary>
|
|
/// If the entity was trying to speak into a radio, this was the channel they were trying to access. If a radio
|
|
/// message gets sent on this channel, this should be set to null to prevent duplicate messages.
|
|
/// </summary>
|
|
public RadioChannelPrototype? Channel;
|
|
|
|
public EntitySpokeEvent(EntityUid source, string message, string originalMessage, RadioChannelPrototype? channel, string? obfuscatedMessage)
|
|
{
|
|
Source = source;
|
|
Message = message;
|
|
Channel = channel;
|
|
ObfuscatedMessage = obfuscatedMessage;
|
|
OriginalMessage = originalMessage; // Sunrise-TTS
|
|
IsRadio = channel != null; // Sunrise-TTS
|
|
}
|
|
}
|
|
|
|
/// <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, // Sunrise-TTS
|
|
CollectiveMind, // Sunrise-Edit
|
|
}
|
|
|
|
/// <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
|
|
}
|
|
|
|
/// <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
|
|
}
|
|
|
|
// Sunrise-TTS-Start
|
|
public sealed class AnnouncementSpokeEvent(
|
|
Filter source,
|
|
string message,
|
|
ResolvedSoundSpecifier? announcementSound,
|
|
string? announceVoice)
|
|
: EntityEventArgs
|
|
{
|
|
public readonly Filter Source = source;
|
|
public readonly string Message = message;
|
|
public readonly string? AnnounceVoice = announceVoice;
|
|
public readonly ResolvedSoundSpecifier? AnnouncementSound = announcementSound;
|
|
}
|
|
|
|
public sealed class RadioSpokeEvent(EntityUid source, string message, EntityUid[] receivers) : EntityEventArgs
|
|
{
|
|
public readonly EntityUid Source = source;
|
|
public readonly string Message = message;
|
|
public readonly EntityUid[] Receivers = receivers;
|
|
}
|
|
// Sunrise-TTS-End
|