1
0

ChatUIController.cs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. using System.Globalization;
  2. using System.Linq;
  3. using System.Numerics;
  4. using Content.Client.Administration.Managers;
  5. using Content.Client.Chat;
  6. using Content.Client.Chat.Managers;
  7. using Content.Client.Chat.TypingIndicator;
  8. using Content.Client.Chat.UI;
  9. using Content.Client.Examine;
  10. using Content.Client.Gameplay;
  11. using Content.Client.Ghost;
  12. using Content.Client.Mind;
  13. using Content.Client.Roles;
  14. using Content.Client.Stylesheets;
  15. using Content.Client.UserInterface.Screens;
  16. using Content.Client.UserInterface.Systems.Chat.Widgets;
  17. using Content.Client.UserInterface.Systems.Gameplay;
  18. using Content.Shared.Administration;
  19. using Content.Shared.CCVar;
  20. using Content.Shared.Chat;
  21. using Content.Shared.Damage.ForceSay;
  22. using Content.Shared.Decals;
  23. using Content.Shared.Input;
  24. using Content.Shared.Radio;
  25. using Content.Shared.Roles.RoleCodeword;
  26. using Robust.Client.GameObjects;
  27. using Robust.Client.Graphics;
  28. using Robust.Client.Input;
  29. using Robust.Client.Player;
  30. using Robust.Client.State;
  31. using Robust.Client.UserInterface;
  32. using Robust.Client.UserInterface.Controllers;
  33. using Robust.Client.UserInterface.Controls;
  34. using Robust.Shared.Configuration;
  35. using Robust.Shared.GameObjects.Components.Localization;
  36. using Robust.Shared.Input.Binding;
  37. using Robust.Shared.Map;
  38. using Robust.Shared.Network;
  39. using Robust.Shared.Prototypes;
  40. using Robust.Shared.Replays;
  41. using Robust.Shared.Timing;
  42. using Robust.Shared.Utility;
  43. namespace Content.Client.UserInterface.Systems.Chat;
  44. public sealed class ChatUIController : UIController
  45. {
  46. [Dependency] private readonly IClientAdminManager _admin = default!;
  47. [Dependency] private readonly IChatManager _manager = default!;
  48. [Dependency] private readonly IConfigurationManager _config = default!;
  49. [Dependency] private readonly IEyeManager _eye = default!;
  50. [Dependency] private readonly IEntityManager _ent = default!;
  51. [Dependency] private readonly IInputManager _input = default!;
  52. [Dependency] private readonly IClientNetManager _net = default!;
  53. [Dependency] private readonly IPlayerManager _player = default!;
  54. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  55. [Dependency] private readonly IStateManager _state = default!;
  56. [Dependency] private readonly IGameTiming _timing = default!;
  57. [Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
  58. [UISystemDependency] private readonly ExamineSystem? _examine = default;
  59. [UISystemDependency] private readonly GhostSystem? _ghost = default;
  60. [UISystemDependency] private readonly TypingIndicatorSystem? _typingIndicator = default;
  61. [UISystemDependency] private readonly ChatSystem? _chatSys = default;
  62. [UISystemDependency] private readonly TransformSystem? _transform = default;
  63. [UISystemDependency] private readonly MindSystem? _mindSystem = default!;
  64. [UISystemDependency] private readonly RoleCodewordSystem? _roleCodewordSystem = default!;
  65. [ValidatePrototypeId<ColorPalettePrototype>]
  66. private const string ChatNamePalette = "ChatNames";
  67. private string[] _chatNameColors = default!;
  68. private bool _chatNameColorsEnabled;
  69. private ISawmill _sawmill = default!;
  70. public static readonly Dictionary<char, ChatSelectChannel> PrefixToChannel = new()
  71. {
  72. {SharedChatSystem.LocalPrefix, ChatSelectChannel.Local},
  73. {SharedChatSystem.WhisperPrefix, ChatSelectChannel.Whisper},
  74. {SharedChatSystem.ConsolePrefix, ChatSelectChannel.Console},
  75. {SharedChatSystem.LOOCPrefix, ChatSelectChannel.LOOC},
  76. {SharedChatSystem.OOCPrefix, ChatSelectChannel.OOC},
  77. {SharedChatSystem.EmotesPrefix, ChatSelectChannel.Emotes},
  78. {SharedChatSystem.EmotesAltPrefix, ChatSelectChannel.Emotes},
  79. {SharedChatSystem.AdminPrefix, ChatSelectChannel.Admin},
  80. {SharedChatSystem.RadioCommonPrefix, ChatSelectChannel.Radio},
  81. {SharedChatSystem.DeadPrefix, ChatSelectChannel.Dead}
  82. };
  83. public static readonly Dictionary<ChatSelectChannel, char> ChannelPrefixes = new()
  84. {
  85. {ChatSelectChannel.Local, SharedChatSystem.LocalPrefix},
  86. {ChatSelectChannel.Whisper, SharedChatSystem.WhisperPrefix},
  87. {ChatSelectChannel.Console, SharedChatSystem.ConsolePrefix},
  88. {ChatSelectChannel.LOOC, SharedChatSystem.LOOCPrefix},
  89. {ChatSelectChannel.OOC, SharedChatSystem.OOCPrefix},
  90. {ChatSelectChannel.Emotes, SharedChatSystem.EmotesPrefix},
  91. {ChatSelectChannel.Admin, SharedChatSystem.AdminPrefix},
  92. {ChatSelectChannel.Radio, SharedChatSystem.RadioCommonPrefix},
  93. {ChatSelectChannel.Dead, SharedChatSystem.DeadPrefix}
  94. };
  95. /// <summary>
  96. /// The max amount of chars allowed to fit in a single speech bubble.
  97. /// </summary>
  98. private const int SingleBubbleCharLimit = 100;
  99. /// <summary>
  100. /// Base queue delay each speech bubble has.
  101. /// </summary>
  102. private const float BubbleDelayBase = 0.2f;
  103. /// <summary>
  104. /// Factor multiplied by speech bubble char length to add to delay.
  105. /// </summary>
  106. private const float BubbleDelayFactor = 0.8f / SingleBubbleCharLimit;
  107. /// <summary>
  108. /// The max amount of speech bubbles over a single entity at once.
  109. /// </summary>
  110. private const int SpeechBubbleCap = 4;
  111. private LayoutContainer _speechBubbleRoot = default!;
  112. /// <summary>
  113. /// Speech bubbles that are currently visible on screen.
  114. /// We track them to push them up when new ones get added.
  115. /// </summary>
  116. private readonly Dictionary<EntityUid, List<SpeechBubble>> _activeSpeechBubbles =
  117. new();
  118. /// <summary>
  119. /// Speech bubbles that are to-be-sent because of the "rate limit" they have.
  120. /// </summary>
  121. private readonly Dictionary<EntityUid, SpeechBubbleQueueData> _queuedSpeechBubbles
  122. = new();
  123. private readonly HashSet<ChatBox> _chats = new();
  124. public IReadOnlySet<ChatBox> Chats => _chats;
  125. /// <summary>
  126. /// The max amount of characters an entity can send in one message
  127. /// </summary>
  128. public int MaxMessageLength => _config.GetCVar(CCVars.ChatMaxMessageLength);
  129. /// <summary>
  130. /// For currently disabled chat filters,
  131. /// unread messages (messages received since the channel has been filtered out).
  132. /// </summary>
  133. private readonly Dictionary<ChatChannel, int> _unreadMessages = new();
  134. // TODO add a cap for this for non-replays
  135. public readonly List<(GameTick Tick, ChatMessage Msg)> History = new();
  136. // Maintains which channels a client should be able to filter (for showing in the chatbox)
  137. // and select (for attempting to send on).
  138. // This may not always actually match with what the server will actually allow them to
  139. // send / receive on, it is only what the user can select in the UI. For example,
  140. // if a user is silenced from speaking for some reason this may still contain ChatChannel.Local, it is left up
  141. // to the server to handle invalid attempts to use particular channels and not send messages for
  142. // channels the user shouldn't be able to hear.
  143. //
  144. // Note that Command is an available selection in the chatbox channel selector,
  145. // which is not actually a chat channel but is always available.
  146. public ChatSelectChannel CanSendChannels { get; private set; }
  147. public ChatChannel FilterableChannels { get; private set; }
  148. public ChatSelectChannel SelectableChannels { get; private set; }
  149. private ChatSelectChannel PreferredChannel { get; set; } = ChatSelectChannel.OOC;
  150. public event Action<ChatSelectChannel>? CanSendChannelsChanged;
  151. public event Action<ChatChannel>? FilterableChannelsChanged;
  152. public event Action<ChatSelectChannel>? SelectableChannelsChanged;
  153. public event Action<ChatChannel, int?>? UnreadMessageCountsUpdated;
  154. public event Action<ChatMessage>? MessageAdded;
  155. public override void Initialize()
  156. {
  157. _sawmill = Logger.GetSawmill("chat");
  158. _sawmill.Level = LogLevel.Info;
  159. _admin.AdminStatusUpdated += UpdateChannelPermissions;
  160. _player.LocalPlayerAttached += OnAttachedChanged;
  161. _player.LocalPlayerDetached += OnAttachedChanged;
  162. _state.OnStateChanged += StateChanged;
  163. _net.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
  164. _net.RegisterNetMessage<MsgDeleteChatMessagesBy>(OnDeleteChatMessagesBy);
  165. SubscribeNetworkEvent<DamageForceSayEvent>(OnDamageForceSay);
  166. _config.OnValueChanged(CCVars.ChatEnableColorName, (value) => { _chatNameColorsEnabled = value; });
  167. _chatNameColorsEnabled = _config.GetCVar(CCVars.ChatEnableColorName);
  168. _speechBubbleRoot = new LayoutContainer();
  169. UpdateChannelPermissions();
  170. _input.SetInputCommand(ContentKeyFunctions.FocusChat,
  171. InputCmdHandler.FromDelegate(_ => FocusChat()));
  172. _input.SetInputCommand(ContentKeyFunctions.FocusLocalChat,
  173. InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Local)));
  174. _input.SetInputCommand(ContentKeyFunctions.FocusEmote,
  175. InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Emotes)));
  176. _input.SetInputCommand(ContentKeyFunctions.FocusWhisperChat,
  177. InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Whisper)));
  178. _input.SetInputCommand(ContentKeyFunctions.FocusLOOC,
  179. InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.LOOC)));
  180. _input.SetInputCommand(ContentKeyFunctions.FocusOOC,
  181. InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.OOC)));
  182. _input.SetInputCommand(ContentKeyFunctions.FocusAdminChat,
  183. InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Admin)));
  184. _input.SetInputCommand(ContentKeyFunctions.FocusRadio,
  185. InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Radio)));
  186. _input.SetInputCommand(ContentKeyFunctions.FocusDeadChat,
  187. InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Dead)));
  188. _input.SetInputCommand(ContentKeyFunctions.FocusConsoleChat,
  189. InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Console)));
  190. _input.SetInputCommand(ContentKeyFunctions.CycleChatChannelForward,
  191. InputCmdHandler.FromDelegate(_ => CycleChatChannel(true)));
  192. _input.SetInputCommand(ContentKeyFunctions.CycleChatChannelBackward,
  193. InputCmdHandler.FromDelegate(_ => CycleChatChannel(false)));
  194. var gameplayStateLoad = UIManager.GetUIController<GameplayStateLoadController>();
  195. gameplayStateLoad.OnScreenLoad += OnScreenLoad;
  196. gameplayStateLoad.OnScreenUnload += OnScreenUnload;
  197. var nameColors = _prototypeManager.Index<ColorPalettePrototype>(ChatNamePalette).Colors.Values.ToArray();
  198. _chatNameColors = new string[nameColors.Length];
  199. for (var i = 0; i < nameColors.Length; i++)
  200. {
  201. _chatNameColors[i] = nameColors[i].ToHex();
  202. }
  203. _config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged);
  204. }
  205. public void OnScreenLoad()
  206. {
  207. SetMainChat(true);
  208. var viewportContainer = UIManager.ActiveScreen!.FindControl<LayoutContainer>("ViewportContainer");
  209. SetSpeechBubbleRoot(viewportContainer);
  210. SetChatWindowOpacity(_config.GetCVar(CCVars.ChatWindowOpacity));
  211. }
  212. public void OnScreenUnload()
  213. {
  214. SetMainChat(false);
  215. }
  216. private void OnChatWindowOpacityChanged(float opacity)
  217. {
  218. SetChatWindowOpacity(opacity);
  219. }
  220. private void SetChatWindowOpacity(float opacity)
  221. {
  222. var chatBox = UIManager.ActiveScreen?.GetWidget<ChatBox>() ?? UIManager.ActiveScreen?.GetWidget<ResizableChatBox>();
  223. var panel = chatBox?.ChatWindowPanel;
  224. if (panel is null)
  225. return;
  226. Color color;
  227. if (panel.PanelOverride is StyleBoxFlat styleBoxFlat)
  228. color = styleBoxFlat.BackgroundColor;
  229. else if (panel.TryGetStyleProperty<StyleBox>(PanelContainer.StylePropertyPanel, out var style)
  230. && style is StyleBoxFlat propStyleBoxFlat)
  231. color = propStyleBoxFlat.BackgroundColor;
  232. else
  233. color = StyleNano.ChatBackgroundColor;
  234. panel.PanelOverride = new StyleBoxFlat
  235. {
  236. BackgroundColor = color.WithAlpha(opacity)
  237. };
  238. }
  239. public void SetMainChat(bool setting)
  240. {
  241. if (UIManager.ActiveScreen == null)
  242. {
  243. return;
  244. }
  245. ChatBox chatBox;
  246. string? chatSizeRaw;
  247. switch (UIManager.ActiveScreen)
  248. {
  249. case DefaultGameScreen defaultScreen:
  250. chatBox = defaultScreen.ChatBox;
  251. chatSizeRaw = _config.GetCVar(CCVars.DefaultScreenChatSize);
  252. SetChatSizing(chatSizeRaw, defaultScreen, setting);
  253. break;
  254. case SeparatedChatGameScreen separatedScreen:
  255. chatBox = separatedScreen.ChatBox;
  256. chatSizeRaw = _config.GetCVar(CCVars.SeparatedScreenChatSize);
  257. SetChatSizing(chatSizeRaw, separatedScreen, setting);
  258. break;
  259. default:
  260. // this could be better?
  261. var maybeChat = UIManager.ActiveScreen.GetWidget<ChatBox>();
  262. chatBox = maybeChat ?? throw new Exception("Cannot get chat box in screen!");
  263. break;
  264. }
  265. chatBox.Main = setting;
  266. }
  267. private void SetChatSizing(string sizing, InGameScreen screen, bool setting)
  268. {
  269. if (!setting)
  270. {
  271. screen.OnChatResized -= StoreChatSize;
  272. return;
  273. }
  274. screen.OnChatResized += StoreChatSize;
  275. if (string.IsNullOrEmpty(sizing))
  276. {
  277. return;
  278. }
  279. var split = sizing.Split(",");
  280. var chatSize = new Vector2(
  281. float.Parse(split[0], CultureInfo.InvariantCulture),
  282. float.Parse(split[1], CultureInfo.InvariantCulture));
  283. screen.SetChatSize(chatSize);
  284. }
  285. private void StoreChatSize(Vector2 size)
  286. {
  287. if (UIManager.ActiveScreen == null)
  288. {
  289. throw new Exception("Cannot get active screen!");
  290. }
  291. var stringSize =
  292. $"{size.X.ToString(CultureInfo.InvariantCulture)},{size.Y.ToString(CultureInfo.InvariantCulture)}";
  293. switch (UIManager.ActiveScreen)
  294. {
  295. case DefaultGameScreen _:
  296. _config.SetCVar(CCVars.DefaultScreenChatSize, stringSize);
  297. break;
  298. case SeparatedChatGameScreen _:
  299. _config.SetCVar(CCVars.SeparatedScreenChatSize, stringSize);
  300. break;
  301. default:
  302. // do nothing
  303. return;
  304. }
  305. _config.SaveToFile();
  306. }
  307. private void FocusChat()
  308. {
  309. foreach (var chat in _chats)
  310. {
  311. if (!chat.Main)
  312. continue;
  313. chat.Focus();
  314. break;
  315. }
  316. }
  317. private void FocusChannel(ChatSelectChannel channel)
  318. {
  319. foreach (var chat in _chats)
  320. {
  321. if (!chat.Main)
  322. continue;
  323. chat.Focus(channel);
  324. break;
  325. }
  326. }
  327. private void CycleChatChannel(bool forward)
  328. {
  329. foreach (var chat in _chats)
  330. {
  331. if (!chat.Main)
  332. continue;
  333. chat.CycleChatChannel(forward);
  334. break;
  335. }
  336. }
  337. private void StateChanged(StateChangedEventArgs args)
  338. {
  339. if (args.NewState is GameplayState)
  340. {
  341. PreferredChannel = ChatSelectChannel.Local;
  342. }
  343. UpdateChannelPermissions();
  344. }
  345. public void SetSpeechBubbleRoot(LayoutContainer root)
  346. {
  347. _speechBubbleRoot.Orphan();
  348. root.AddChild(_speechBubbleRoot);
  349. LayoutContainer.SetAnchorPreset(_speechBubbleRoot, LayoutContainer.LayoutPreset.Wide);
  350. _speechBubbleRoot.SetPositionLast();
  351. }
  352. private void OnAttachedChanged(EntityUid uid)
  353. {
  354. UpdateChannelPermissions();
  355. }
  356. private void AddSpeechBubble(ChatMessage msg, SpeechBubble.SpeechType speechType)
  357. {
  358. var ent = EntityManager.GetEntity(msg.SenderEntity);
  359. if (!EntityManager.EntityExists(ent))
  360. {
  361. _sawmill.Debug("Got local chat message with invalid sender entity: {0}", msg.SenderEntity);
  362. return;
  363. }
  364. EnqueueSpeechBubble(ent, msg, speechType);
  365. }
  366. private void CreateSpeechBubble(EntityUid entity, SpeechBubbleData speechData)
  367. {
  368. var bubble =
  369. SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity);
  370. bubble.OnDied += SpeechBubbleDied;
  371. if (_activeSpeechBubbles.TryGetValue(entity, out var existing))
  372. {
  373. // Push up existing bubbles above the mob's head.
  374. foreach (var existingBubble in existing)
  375. {
  376. existingBubble.VerticalOffset += bubble.ContentSize.Y;
  377. }
  378. }
  379. else
  380. {
  381. existing = new List<SpeechBubble>();
  382. _activeSpeechBubbles.Add(entity, existing);
  383. }
  384. existing.Add(bubble);
  385. _speechBubbleRoot.AddChild(bubble);
  386. if (existing.Count > SpeechBubbleCap)
  387. {
  388. // Get the next speech bubble to fade
  389. // Any speech bubbles before it are already fading
  390. var last = existing[^(SpeechBubbleCap + 1)];
  391. last.FadeNow();
  392. }
  393. }
  394. private void SpeechBubbleDied(EntityUid entity, SpeechBubble bubble)
  395. {
  396. RemoveSpeechBubble(entity, bubble);
  397. }
  398. private void EnqueueSpeechBubble(EntityUid entity, ChatMessage message, SpeechBubble.SpeechType speechType)
  399. {
  400. // Don't enqueue speech bubbles for other maps. TODO: Support multiple viewports/maps?
  401. if (EntityManager.GetComponent<TransformComponent>(entity).MapID != _eye.CurrentMap)
  402. return;
  403. if (!_queuedSpeechBubbles.TryGetValue(entity, out var queueData))
  404. {
  405. queueData = new SpeechBubbleQueueData();
  406. _queuedSpeechBubbles.Add(entity, queueData);
  407. }
  408. queueData.MessageQueue.Enqueue(new SpeechBubbleData(message, speechType));
  409. }
  410. public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble)
  411. {
  412. bubble.Dispose();
  413. var list = _activeSpeechBubbles[entityUid];
  414. list.Remove(bubble);
  415. if (list.Count == 0)
  416. {
  417. _activeSpeechBubbles.Remove(entityUid);
  418. }
  419. }
  420. private void UpdateChannelPermissions()
  421. {
  422. CanSendChannels = default;
  423. FilterableChannels = default;
  424. // Can always send console stuff.
  425. CanSendChannels |= ChatSelectChannel.Console;
  426. // can always send/recieve OOC
  427. CanSendChannels |= ChatSelectChannel.OOC;
  428. CanSendChannels |= ChatSelectChannel.LOOC;
  429. FilterableChannels |= ChatChannel.OOC;
  430. FilterableChannels |= ChatChannel.LOOC;
  431. // can always hear server (nobody can actually send server messages).
  432. FilterableChannels |= ChatChannel.Server;
  433. if (_state.CurrentState is GameplayStateBase)
  434. {
  435. // can always hear local / radio / emote / notifications when in the game
  436. FilterableChannels |= ChatChannel.Local;
  437. FilterableChannels |= ChatChannel.Whisper;
  438. FilterableChannels |= ChatChannel.Radio;
  439. FilterableChannels |= ChatChannel.Emotes;
  440. FilterableChannels |= ChatChannel.Notifications;
  441. // Can only send local / radio / emote when attached to a non-ghost entity.
  442. // TODO: this logic is iffy (checking if controlling something that's NOT a ghost), is there a better way to check this?
  443. if (_ghost is not {IsGhost: true})
  444. {
  445. CanSendChannels |= ChatSelectChannel.Local;
  446. CanSendChannels |= ChatSelectChannel.Whisper;
  447. CanSendChannels |= ChatSelectChannel.Radio;
  448. CanSendChannels |= ChatSelectChannel.Emotes;
  449. }
  450. }
  451. // Only ghosts and admins can send / see deadchat.
  452. if (_admin.HasFlag(AdminFlags.Admin) || _ghost is {IsGhost: true})
  453. {
  454. FilterableChannels |= ChatChannel.Dead;
  455. CanSendChannels |= ChatSelectChannel.Dead;
  456. }
  457. // only admins can see / filter asay
  458. if (_admin.HasFlag(AdminFlags.Adminchat))
  459. {
  460. FilterableChannels |= ChatChannel.Admin;
  461. FilterableChannels |= ChatChannel.AdminAlert;
  462. FilterableChannels |= ChatChannel.AdminChat;
  463. CanSendChannels |= ChatSelectChannel.Admin;
  464. }
  465. SelectableChannels = CanSendChannels;
  466. // Necessary so that we always have a channel to fall back to.
  467. DebugTools.Assert((CanSendChannels & ChatSelectChannel.OOC) != 0, "OOC must always be available");
  468. DebugTools.Assert((FilterableChannels & ChatChannel.OOC) != 0, "OOC must always be available");
  469. DebugTools.Assert((SelectableChannels & ChatSelectChannel.OOC) != 0, "OOC must always be available");
  470. // let our chatbox know all the new settings
  471. CanSendChannelsChanged?.Invoke(CanSendChannels);
  472. FilterableChannelsChanged?.Invoke(FilterableChannels);
  473. SelectableChannelsChanged?.Invoke(SelectableChannels);
  474. }
  475. public void ClearUnfilteredUnreads(ChatChannel channels)
  476. {
  477. foreach (var channel in _unreadMessages.Keys.ToArray())
  478. {
  479. if ((channels & channel) == 0)
  480. continue;
  481. _unreadMessages[channel] = 0;
  482. UnreadMessageCountsUpdated?.Invoke(channel, 0);
  483. }
  484. }
  485. public override void FrameUpdate(FrameEventArgs delta)
  486. {
  487. UpdateQueuedSpeechBubbles(delta);
  488. }
  489. private void UpdateQueuedSpeechBubbles(FrameEventArgs delta)
  490. {
  491. // Update queued speech bubbles.
  492. if (_queuedSpeechBubbles.Count == 0 || _examine == null)
  493. {
  494. return;
  495. }
  496. foreach (var (entity, queueData) in _queuedSpeechBubbles.ShallowClone())
  497. {
  498. if (!EntityManager.EntityExists(entity))
  499. {
  500. _queuedSpeechBubbles.Remove(entity);
  501. continue;
  502. }
  503. queueData.TimeLeft -= delta.DeltaSeconds;
  504. if (queueData.TimeLeft > 0)
  505. {
  506. continue;
  507. }
  508. if (queueData.MessageQueue.Count == 0)
  509. {
  510. _queuedSpeechBubbles.Remove(entity);
  511. continue;
  512. }
  513. var msg = queueData.MessageQueue.Dequeue();
  514. queueData.TimeLeft += BubbleDelayBase + msg.Message.Message.Length * BubbleDelayFactor;
  515. // We keep the queue around while it has 0 items. This allows us to keep the timer.
  516. // When the timer hits 0 and there's no messages left, THEN we can clear it up.
  517. CreateSpeechBubble(entity, msg);
  518. }
  519. var player = _player.LocalEntity;
  520. var predicate = static (EntityUid uid, (EntityUid compOwner, EntityUid? attachedEntity) data)
  521. => uid == data.compOwner || uid == data.attachedEntity;
  522. var playerPos = player != null
  523. ? _eye.CurrentEye.Position
  524. : MapCoordinates.Nullspace;
  525. var occluded = player != null && _examine.IsOccluded(player.Value);
  526. foreach (var (ent, bubs) in _activeSpeechBubbles)
  527. {
  528. if (EntityManager.Deleted(ent))
  529. {
  530. SetBubbles(bubs, false);
  531. continue;
  532. }
  533. if (ent == player)
  534. {
  535. SetBubbles(bubs, true);
  536. continue;
  537. }
  538. var otherPos = _transform?.GetMapCoordinates(ent) ?? MapCoordinates.Nullspace;
  539. if (occluded && !_examine.InRangeUnOccluded(
  540. playerPos,
  541. otherPos, 0f,
  542. (ent, player), predicate))
  543. {
  544. SetBubbles(bubs, false);
  545. continue;
  546. }
  547. SetBubbles(bubs, true);
  548. }
  549. }
  550. private void SetBubbles(List<SpeechBubble> bubbles, bool visible)
  551. {
  552. foreach (var bubble in bubbles)
  553. {
  554. bubble.Visible = visible;
  555. }
  556. }
  557. public ChatSelectChannel MapLocalIfGhost(ChatSelectChannel channel)
  558. {
  559. if (channel == ChatSelectChannel.Local && _ghost is {IsGhost: true})
  560. return ChatSelectChannel.Dead;
  561. return channel;
  562. }
  563. private bool TryGetRadioChannel(string text, out RadioChannelPrototype? radioChannel)
  564. {
  565. radioChannel = null;
  566. return _player.LocalEntity is EntityUid { Valid: true } uid
  567. && _chatSys != null
  568. && _chatSys.TryProccessRadioMessage(uid, text, out _, out radioChannel, quiet: true);
  569. }
  570. public void UpdateSelectedChannel(ChatBox box)
  571. {
  572. var (prefixChannel, _, radioChannel) = SplitInputContents(box.ChatInput.Input.Text.ToLower());
  573. if (prefixChannel == ChatSelectChannel.None)
  574. box.ChatInput.ChannelSelector.UpdateChannelSelectButton(box.SelectedChannel, null);
  575. else
  576. box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, radioChannel);
  577. }
  578. public (ChatSelectChannel chatChannel, string text, RadioChannelPrototype? radioChannel) SplitInputContents(string text)
  579. {
  580. text = text.Trim();
  581. if (text.Length == 0)
  582. return (ChatSelectChannel.None, text, null);
  583. // We only cut off prefix only if it is not a radio or local channel, which both map to the same /say command
  584. // because ????????
  585. ChatSelectChannel chatChannel;
  586. if (TryGetRadioChannel(text, out var radioChannel))
  587. chatChannel = ChatSelectChannel.Radio;
  588. else
  589. chatChannel = PrefixToChannel.GetValueOrDefault(text[0]);
  590. if ((CanSendChannels & chatChannel) == 0)
  591. return (ChatSelectChannel.None, text, null);
  592. if (chatChannel == ChatSelectChannel.Radio)
  593. return (chatChannel, text, radioChannel);
  594. if (chatChannel == ChatSelectChannel.Local)
  595. {
  596. if (_ghost?.IsGhost != true)
  597. return (chatChannel, text, null);
  598. else
  599. chatChannel = ChatSelectChannel.Dead;
  600. }
  601. return (chatChannel, text[1..].TrimStart(), null);
  602. }
  603. public void SendMessage(ChatBox box, ChatSelectChannel channel)
  604. {
  605. _typingIndicator?.ClientSubmittedChatText();
  606. var text = box.ChatInput.Input.Text;
  607. box.ChatInput.Input.Clear();
  608. box.ChatInput.Input.ReleaseKeyboardFocus();
  609. UpdateSelectedChannel(box);
  610. if (string.IsNullOrWhiteSpace(text))
  611. return;
  612. (var prefixChannel, text, var _) = SplitInputContents(text);
  613. // Check if message is longer than the character limit
  614. if (text.Length > MaxMessageLength)
  615. {
  616. var locWarning = Loc.GetString("chat-manager-max-message-length",
  617. ("maxMessageLength", MaxMessageLength));
  618. box.AddLine(locWarning, Color.Orange);
  619. return;
  620. }
  621. if (prefixChannel != ChatSelectChannel.None)
  622. channel = prefixChannel;
  623. else if (channel == ChatSelectChannel.Radio)
  624. {
  625. // radio must have prefix as it goes through the say command.
  626. text = $";{text}";
  627. }
  628. _manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
  629. }
  630. private void OnDamageForceSay(DamageForceSayEvent ev, EntitySessionEventArgs _)
  631. {
  632. var chatBox = UIManager.ActiveScreen?.GetWidget<ChatBox>() ?? UIManager.ActiveScreen?.GetWidget<ResizableChatBox>();
  633. if (chatBox == null)
  634. return;
  635. var msg = chatBox.ChatInput.Input.Text.TrimEnd();
  636. // Don't send on OOC/LOOC obviously!
  637. // we need to handle selected channel
  638. // and prefix-channel separately..
  639. var allowedChannels = ChatSelectChannel.Local | ChatSelectChannel.Whisper;
  640. if ((chatBox.SelectedChannel & allowedChannels) == ChatSelectChannel.None)
  641. return;
  642. // none can be returned from this if theres no prefix,
  643. // so we allow it in that case (assuming the previous check will have exited already if its an invalid channel)
  644. var prefixChannel = SplitInputContents(msg).chatChannel;
  645. if (prefixChannel != ChatSelectChannel.None && (prefixChannel & allowedChannels) == ChatSelectChannel.None)
  646. return;
  647. if (_player.LocalSession?.AttachedEntity is not { } ent
  648. || !EntityManager.TryGetComponent<DamageForceSayComponent>(ent, out var forceSay))
  649. return;
  650. if (string.IsNullOrWhiteSpace(msg))
  651. return;
  652. var modifiedText = ev.Suffix != null
  653. ? Loc.GetString(forceSay.ForceSayMessageWrap,
  654. ("message", msg), ("suffix", ev.Suffix))
  655. : Loc.GetString(forceSay.ForceSayMessageWrapNoSuffix,
  656. ("message", msg));
  657. chatBox.ChatInput.Input.SetText(modifiedText);
  658. chatBox.ChatInput.Input.ForceSubmitText();
  659. }
  660. private void OnChatMessage(MsgChatMessage message)
  661. {
  662. var msg = message.Message;
  663. ProcessChatMessage(msg);
  664. if ((msg.Channel & ChatChannel.AdminRelated) == 0 ||
  665. _config.GetCVar(CCVars.ReplayRecordAdminChat))
  666. {
  667. _replayRecording.RecordClientMessage(msg);
  668. }
  669. }
  670. public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true)
  671. {
  672. // color the name unless it's something like "the old man"
  673. if ((msg.Channel == ChatChannel.Local || msg.Channel == ChatChannel.Whisper) && _chatNameColorsEnabled)
  674. {
  675. var grammar = _ent.GetComponentOrNull<GrammarComponent>(_ent.GetEntity(msg.SenderEntity));
  676. if (grammar != null && grammar.ProperNoun == true)
  677. msg.WrappedMessage = SharedChatSystem.InjectTagInsideTag(msg, "Name", "color", GetNameColor(SharedChatSystem.GetStringInsideTag(msg, "Name")));
  678. }
  679. // Color any codewords for minds that have roles that use them
  680. if (_player.LocalUser != null && _mindSystem != null && _roleCodewordSystem != null)
  681. {
  682. if (_mindSystem.TryGetMind(_player.LocalUser.Value, out var mindId) && _ent.TryGetComponent(mindId, out RoleCodewordComponent? codewordComp))
  683. {
  684. foreach (var (_, codewordData) in codewordComp.RoleCodewords)
  685. {
  686. foreach (string codeword in codewordData.Codewords)
  687. msg.WrappedMessage = SharedChatSystem.InjectTagAroundString(msg, codeword, "color", codewordData.Color.ToHex());
  688. }
  689. }
  690. }
  691. // Log all incoming chat to repopulate when filter is un-toggled
  692. if (!msg.HideChat)
  693. {
  694. History.Add((_timing.CurTick, msg));
  695. MessageAdded?.Invoke(msg);
  696. if (!msg.Read)
  697. {
  698. _sawmill.Debug($"Message filtered: {msg.Channel}: {msg.Message}");
  699. if (!_unreadMessages.TryGetValue(msg.Channel, out var count))
  700. count = 0;
  701. count += 1;
  702. _unreadMessages[msg.Channel] = count;
  703. UnreadMessageCountsUpdated?.Invoke(msg.Channel, count);
  704. }
  705. }
  706. // Local messages that have an entity attached get a speech bubble.
  707. if (!speechBubble || msg.SenderEntity == default)
  708. return;
  709. switch (msg.Channel)
  710. {
  711. case ChatChannel.Local:
  712. AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
  713. break;
  714. case ChatChannel.Whisper:
  715. AddSpeechBubble(msg, SpeechBubble.SpeechType.Whisper);
  716. break;
  717. case ChatChannel.Dead:
  718. if (_ghost is not {IsGhost: true})
  719. break;
  720. AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
  721. break;
  722. case ChatChannel.Emotes:
  723. AddSpeechBubble(msg, SpeechBubble.SpeechType.Emote);
  724. break;
  725. case ChatChannel.LOOC:
  726. if (_config.GetCVar(CCVars.LoocAboveHeadShow))
  727. AddSpeechBubble(msg, SpeechBubble.SpeechType.Looc);
  728. break;
  729. }
  730. }
  731. public void OnDeleteChatMessagesBy(MsgDeleteChatMessagesBy msg)
  732. {
  733. // This will delete messages from an entity even if different players were the author.
  734. // Usages of the erase admin verb should be rare enough that this does not matter.
  735. // Otherwise the client would need to know that one entity has multiple author players,
  736. // or the server would need to track when and which entities a player sent messages as.
  737. History.RemoveAll(h => h.Msg.SenderKey == msg.Key || msg.Entities.Contains(h.Msg.SenderEntity));
  738. Repopulate();
  739. }
  740. public void RegisterChat(ChatBox chat)
  741. {
  742. _chats.Add(chat);
  743. }
  744. public void UnregisterChat(ChatBox chat)
  745. {
  746. _chats.Remove(chat);
  747. }
  748. public ChatSelectChannel GetPreferredChannel()
  749. {
  750. return MapLocalIfGhost(PreferredChannel);
  751. }
  752. public void NotifyChatTextChange()
  753. {
  754. _typingIndicator?.ClientChangedChatText();
  755. }
  756. public void Repopulate()
  757. {
  758. foreach (var chat in _chats)
  759. {
  760. chat.Repopulate();
  761. }
  762. }
  763. /// <summary>
  764. /// Returns the chat name color for a mob
  765. /// </summary>
  766. /// <param name="name">Name of the mob</param>
  767. /// <returns>Hex value of the color</returns>
  768. public string GetNameColor(string name)
  769. {
  770. var colorIdx = Math.Abs(name.GetHashCode() % _chatNameColors.Length);
  771. return _chatNameColors[colorIdx];
  772. }
  773. private readonly record struct SpeechBubbleData(ChatMessage Message, SpeechBubble.SpeechType Type);
  774. private sealed class SpeechBubbleQueueData
  775. {
  776. /// <summary>
  777. /// Time left until the next speech bubble can appear.
  778. /// </summary>
  779. public float TimeLeft { get; set; }
  780. public Queue<SpeechBubbleData> MessageQueue { get; } = new();
  781. }
  782. }