using System.Linq;
using System.Text;
using Content.Shared.Chat;
using Content.Shared.Ghost;
using Content.Shared.Players;
using Content.Shared.Speech.Prototypes;
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.Server.Chat.Systems;
public sealed partial class ChatSystem
{
private enum MessageRangeCheckResult
{
Disallowed,
HideChat,
Full
}
///
/// If hideChat should be set as far as replays are concerned.
///
private bool MessageRangeHideChatForReplay(ChatTransmitRange range)
{
return range == ChatTransmitRange.HideChat;
}
///
/// Checks if a target as returned from GetRecipients should receive the message.
/// Keep in mind data.Range is -1 for out of range observers.
///
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;
}
///
/// Sends a chat message to the given players in range of the source entity.
///
private void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, ChatTransmitRange range, NetUserId? author = 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);
}
_replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
}
///
/// Returns true if the given player is 'allowed' to send the given message, false otherwise.
///
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(sender, ev, true);
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 GetDeadChatClients()
{
return Filter.Empty()
.AddWhereAttachedEntity(HasComp)
.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;
}
public static readonly ProtoId ChatSanitize_Accent = "chatsanitize";
public string SanitizeMessageReplaceWords(string message)
{
if (string.IsNullOrEmpty(message)) return message;
var msg = message;
msg = _wordreplacement.ApplyReplacements(msg, ChatSanitize_Accent);
return msg;
}
///
/// Returns list of players and ranges for all players withing some range. Also returns observers with a range of -1.
///
private Dictionary GetRecipients(EntityUid source, float voiceGetRange)
{
// TODO proper speech occlusion
var recipients = new Dictionary();
var transformSource = Transform(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 = Transform(playerEntity);
if (transformEntity.MapID != sourceMapId)
continue;
var observer = _ghostHearingQuery.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 charOptions, int length)
{
var sb = new StringBuilder();
for (var i = 0; i < length; i++)
{
sb.Append(_random.Pick(charOptions));
}
return sb.ToString();
}
}