From e32ede5882337bdfc129cb23e328f484e4f4e521 Mon Sep 17 00:00:00 2001 From: deathride58 Date: Mon, 4 Dec 2023 18:10:49 -0500 Subject: [PATCH] Fancy speech bubbles - Names over speech bubbles, and 1:1 chat parity for emotes and LOOC! (#21999) --- Content.Client/Chat/UI/SpeechBubble.cs | 151 ++++++++++++++---- .../Options/UI/Tabs/GraphicsTab.xaml | 2 + .../Options/UI/Tabs/GraphicsTab.xaml.cs | 10 ++ .../Systems/Chat/ChatUIController.cs | 75 +-------- Content.Shared/CCVar/CCVars.cs | 6 + .../en-US/chat/managers/chat-manager.ftl | 8 +- .../en-US/escape-menu/ui/options-menu.ftl | 2 + 7 files changed, 154 insertions(+), 100 deletions(-) diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index 46a9f05392..4c8b449994 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -1,8 +1,11 @@ using System.Numerics; using Content.Client.Chat.Managers; +using Content.Shared.CCVar; +using Content.Shared.Chat; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.Configuration; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -10,6 +13,10 @@ namespace Content.Client.Chat.UI { public abstract class SpeechBubble : Control { + [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] protected readonly IConfigurationManager ConfigManager = default!; + public enum SpeechType : byte { Emote, @@ -34,10 +41,12 @@ namespace Content.Client.Chat.UI /// private const float EntityVerticalOffset = 0.5f; - private readonly IEyeManager _eyeManager; + /// + /// The default maximum width for speech bubbles. + /// + public const float SpeechMaxWidth = 256; + private readonly EntityUid _senderEntity; - private readonly IChatManager _chatManager; - private readonly IEntityManager _entityManager; private float _timeLeft = TotalTime; @@ -49,38 +58,36 @@ namespace Content.Client.Chat.UI // man down public event Action? OnDied; - public static SpeechBubble CreateSpeechBubble(SpeechType type, string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager) + public static SpeechBubble CreateSpeechBubble(SpeechType type, ChatMessage message, EntityUid senderEntity) { switch (type) { case SpeechType.Emote: - return new TextSpeechBubble(text, senderEntity, eyeManager, chatManager, entityManager, "emoteBox"); + return new TextSpeechBubble(message, senderEntity, "emoteBox"); case SpeechType.Say: - return new TextSpeechBubble(text, senderEntity, eyeManager, chatManager, entityManager, "sayBox"); + return new FancyTextSpeechBubble(message, senderEntity, "sayBox"); case SpeechType.Whisper: - return new TextSpeechBubble(text, senderEntity, eyeManager, chatManager, entityManager, "whisperBox"); + return new FancyTextSpeechBubble(message, senderEntity, "whisperBox"); case SpeechType.Looc: - return new TextSpeechBubble(text, senderEntity, eyeManager, chatManager, entityManager, "emoteBox", Color.FromHex("#48d1cc")); + return new TextSpeechBubble(message, senderEntity, "emoteBox", Color.FromHex("#48d1cc")); default: throw new ArgumentOutOfRangeException(); } } - public SpeechBubble(string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager, string speechStyleClass, Color? fontColor = null) + public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) { - _chatManager = chatManager; + IoCManager.InjectDependencies(this); _senderEntity = senderEntity; - _eyeManager = eyeManager; - _entityManager = entityManager; // Use text clipping so new messages don't overlap old ones being pushed up. RectClipContent = true; - var bubble = BuildBubble(text, speechStyleClass, fontColor); + var bubble = BuildBubble(message, speechStyleClass, fontColor); AddChild(bubble); @@ -91,7 +98,7 @@ namespace Content.Client.Chat.UI _verticalOffsetAchieved = -ContentSize.Y; } - protected abstract Control BuildBubble(string text, string speechStyleClass, Color? fontColor = null); + protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null); protected override void FrameUpdate(FrameEventArgs args) { @@ -165,33 +172,47 @@ namespace Content.Client.Chat.UI _timeLeft = FadeTime; } } + + protected FormattedMessage FormatSpeech(string message, Color? fontColor = null) + { + var msg = new FormattedMessage(); + if (fontColor != null) + msg.PushColor(fontColor.Value); + msg.AddMarkup(message); + return msg; + } + + protected string ExtractSpeechSubstring(ChatMessage message, string tag) + { + var rawmsg = message.WrappedMessage; + var tagStart = rawmsg.IndexOf($"[{tag}]"); + var tagEnd = rawmsg.IndexOf($"[/{tag}]"); + tagStart += tag.Length + 2; + return rawmsg.Substring(tagStart, tagEnd - tagStart); + } + + protected FormattedMessage ExtractAndFormatSpeechSubstring(ChatMessage message, string tag, Color? fontColor = null) + { + return FormatSpeech(ExtractSpeechSubstring(message, tag), fontColor); + } + } public sealed class TextSpeechBubble : SpeechBubble { - public TextSpeechBubble(string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager, string speechStyleClass, Color? fontColor = null) - : base(text, senderEntity, eyeManager, chatManager, entityManager, speechStyleClass, fontColor) + public TextSpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) + : base(message, senderEntity, speechStyleClass, fontColor) { } - protected override Control BuildBubble(string text, string speechStyleClass, Color? fontColor = null) + protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null) { var label = new RichTextLabel { - MaxWidth = 256, + MaxWidth = SpeechMaxWidth, }; - if (fontColor != null) - { - var msg = new FormattedMessage(); - msg.PushColor(fontColor.Value); - msg.AddMarkup(text); - label.SetMessage(msg); - } - else - { - label.SetMessage(text); - } + label.SetMessage(FormatSpeech(message.WrappedMessage, fontColor)); var panel = new PanelContainer { @@ -203,4 +224,76 @@ namespace Content.Client.Chat.UI return panel; } } + + public sealed class FancyTextSpeechBubble : SpeechBubble + { + + public FancyTextSpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) + : base(message, senderEntity, speechStyleClass, fontColor) + { + } + + protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null) + { + if (!ConfigManager.GetCVar(CCVars.ChatEnableFancyBubbles)) + { + var label = new RichTextLabel + { + MaxWidth = SpeechMaxWidth + }; + + label.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); + + var unfanciedPanel = new PanelContainer + { + StyleClasses = { "speechBox", speechStyleClass }, + Children = { label }, + ModulateSelfOverride = Color.White.WithAlpha(0.75f) + }; + return unfanciedPanel; + } + + var bubbleHeader = new RichTextLabel + { + Margin = new Thickness(1, 1, 1, 1) + }; + + var bubbleContent = new RichTextLabel + { + MaxWidth = SpeechMaxWidth, + Margin = new Thickness(2, 6, 2, 2) + }; + + //We'll be honest. *Yes* this is hacky. Doing this in a cleaner way would require a bottom-up refactor of how saycode handles sending chat messages. -Myr + bubbleHeader.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleHeader", fontColor)); + bubbleContent.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); + + //As for below: Some day this could probably be converted to xaml. But that is not today. -Myr + var mainPanel = new PanelContainer + { + StyleClasses = { "speechBox", speechStyleClass }, + Children = { bubbleContent }, + ModulateSelfOverride = Color.White.WithAlpha(0.75f), + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Bottom, + Margin = new Thickness(4, 14, 4, 2) + }; + + var headerPanel = new PanelContainer + { + StyleClasses = { "speechBox", speechStyleClass }, + Children = { bubbleHeader }, + ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.ChatFancyNameBackground) ? 0.75f : 0f), + HorizontalAlignment = HAlignment.Center, + VerticalAlignment = VAlignment.Top + }; + + var panel = new PanelContainer + { + Children = { mainPanel, headerPanel } + }; + + return panel; + } + } } diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml index f759c78eca..3de59cf5dc 100644 --- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml +++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml @@ -23,6 +23,8 @@ + + diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs index e64838ba75..9d7e500651 100644 --- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs @@ -104,6 +104,8 @@ namespace Content.Client.Options.UI.Tabs ShowHeldItemCheckBox.OnToggled += OnCheckBoxToggled; ShowCombatModeIndicatorsCheckBox.OnToggled += OnCheckBoxToggled; ShowLoocAboveHeadCheckBox.OnToggled += OnCheckBoxToggled; + FancySpeechBubblesCheckBox.OnToggled += OnCheckBoxToggled; + FancyNameBackgroundsCheckBox.OnToggled += OnCheckBoxToggled; IntegerScalingCheckBox.OnToggled += OnCheckBoxToggled; ViewportLowResCheckBox.OnToggled += OnCheckBoxToggled; ParallaxLowQualityCheckBox.OnToggled += OnCheckBoxToggled; @@ -123,6 +125,8 @@ namespace Content.Client.Options.UI.Tabs ShowHeldItemCheckBox.Pressed = _cfg.GetCVar(CCVars.HudHeldItemShow); ShowCombatModeIndicatorsCheckBox.Pressed = _cfg.GetCVar(CCVars.CombatModeIndicatorsPointShow); ShowLoocAboveHeadCheckBox.Pressed = _cfg.GetCVar(CCVars.LoocAboveHeadShow); + FancySpeechBubblesCheckBox.Pressed = _cfg.GetCVar(CCVars.ChatEnableFancyBubbles); + FancyNameBackgroundsCheckBox.Pressed = _cfg.GetCVar(CCVars.ChatFancyNameBackground); ViewportWidthSlider.Value = _cfg.GetCVar(CCVars.ViewportWidth); _cfg.OnValueChanged(CCVars.ViewportMinimumWidth, _ => UpdateViewportWidthRange()); @@ -171,6 +175,8 @@ namespace Content.Client.Options.UI.Tabs _cfg.SetCVar(CCVars.HudHeldItemShow, ShowHeldItemCheckBox.Pressed); _cfg.SetCVar(CCVars.CombatModeIndicatorsPointShow, ShowCombatModeIndicatorsCheckBox.Pressed); _cfg.SetCVar(CCVars.LoocAboveHeadShow, ShowLoocAboveHeadCheckBox.Pressed); + _cfg.SetCVar(CCVars.ChatEnableFancyBubbles, FancySpeechBubblesCheckBox.Pressed); + _cfg.SetCVar(CCVars.ChatFancyNameBackground, FancyNameBackgroundsCheckBox.Pressed); _cfg.SetCVar(CCVars.HudFpsCounterVisible, FpsCounterCheckBox.Pressed); _cfg.SetCVar(CCVars.ViewportWidth, (int) ViewportWidthSlider.Value); @@ -209,6 +215,8 @@ namespace Content.Client.Options.UI.Tabs var isShowHeldItemSame = ShowHeldItemCheckBox.Pressed == _cfg.GetCVar(CCVars.HudHeldItemShow); var isCombatModeIndicatorsSame = ShowCombatModeIndicatorsCheckBox.Pressed == _cfg.GetCVar(CCVars.CombatModeIndicatorsPointShow); var isLoocShowSame = ShowLoocAboveHeadCheckBox.Pressed == _cfg.GetCVar(CCVars.LoocAboveHeadShow); + var isFancyChatSame = FancySpeechBubblesCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatEnableFancyBubbles); + var isFancyBackgroundSame = FancyNameBackgroundsCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatFancyNameBackground); var isFpsCounterVisibleSame = FpsCounterCheckBox.Pressed == _cfg.GetCVar(CCVars.HudFpsCounterVisible); var isWidthSame = (int) ViewportWidthSlider.Value == _cfg.GetCVar(CCVars.ViewportWidth); var isLayoutSame = HudLayoutOption.SelectedMetadata is string opt && opt == _cfg.GetCVar(CCVars.UILayout); @@ -226,6 +234,8 @@ namespace Content.Client.Options.UI.Tabs isShowHeldItemSame && isCombatModeIndicatorsSame && isLoocShowSame && + isFancyChatSame && + isFancyBackgroundSame && isFpsCounterVisibleSame && isWidthSame && isLayoutSame; diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs index 2d90b371c3..e539394416 100644 --- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs +++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs @@ -369,7 +369,7 @@ public sealed class ChatUIController : UIController UpdateChannelPermissions(); } - private void AddSpeechBubble(ChatMessage msg, SpeechBubble.SpeechType speechType, string? prefixText = null, string? prefixEndText = null) + private void AddSpeechBubble(ChatMessage msg, SpeechBubble.SpeechType speechType) { var ent = EntityManager.GetEntity(msg.SenderEntity); @@ -379,22 +379,13 @@ public sealed class ChatUIController : UIController return; } - // Kind of shitty way to add prefixes but hey it works! - string Message = prefixText + msg.Message + prefixEndText; - - // msg.Message should be the string that a user sent over text, without any added markup. - var messages = SplitMessage(Message); - - foreach (var message in messages) - { - EnqueueSpeechBubble(ent, message, speechType); - } + EnqueueSpeechBubble(ent, msg, speechType); } private void CreateSpeechBubble(EntityUid entity, SpeechBubbleData speechData) { var bubble = - SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eye, _manager, EntityManager); + SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity); bubble.OnDied += SpeechBubbleDied; @@ -428,7 +419,7 @@ public sealed class ChatUIController : UIController RemoveSpeechBubble(entity, bubble); } - private void EnqueueSpeechBubble(EntityUid entity, string contents, SpeechBubble.SpeechType speechType) + private void EnqueueSpeechBubble(EntityUid entity, ChatMessage message, SpeechBubble.SpeechType speechType) { // Don't enqueue speech bubbles for other maps. TODO: Support multiple viewports/maps? if (EntityManager.GetComponent(entity).MapID != _eye.CurrentMap) @@ -440,7 +431,7 @@ public sealed class ChatUIController : UIController _queuedSpeechBubbles.Add(entity, queueData); } - queueData.MessageQueue.Enqueue(new SpeechBubbleData(contents, speechType)); + queueData.MessageQueue.Enqueue(new SpeechBubbleData(message, speechType)); } public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble) @@ -568,7 +559,7 @@ public sealed class ChatUIController : UIController var msg = queueData.MessageQueue.Dequeue(); - queueData.TimeLeft += BubbleDelayBase + msg.Message.Length * BubbleDelayFactor; + queueData.TimeLeft += BubbleDelayBase + msg.Message.Message.Length * BubbleDelayFactor; // We keep the queue around while it has 0 items. This allows us to keep the timer. // When the timer hits 0 and there's no messages left, THEN we can clear it up. @@ -621,52 +612,6 @@ public sealed class ChatUIController : UIController } } - private List SplitMessage(string msg) - { - // Split message into words separated by spaces. - var words = msg.Split(' '); - var messages = new List(); - var currentBuffer = new List(); - - // Really shoddy way to approximate word length. - // Yes, I am aware of all the crimes here. - // TODO: Improve this to use actual glyph width etc.. - var currentWordLength = 0; - foreach (var word in words) - { - // +1 for the space. - currentWordLength += word.Length + 1; - - if (currentWordLength > SingleBubbleCharLimit) - { - // Too long for the current speech bubble, flush it. - messages.Add(string.Join(" ", currentBuffer)); - currentBuffer.Clear(); - - currentWordLength = word.Length; - - if (currentWordLength > SingleBubbleCharLimit) - { - // Word is STILL too long. - // Truncate it with an ellipse. - messages.Add($"{word.Substring(0, SingleBubbleCharLimit - 3)}..."); - currentWordLength = 0; - continue; - } - } - - currentBuffer.Add(word); - } - - if (currentBuffer.Count != 0) - { - // Don't forget the last bubble. - messages.Add(string.Join(" ", currentBuffer)); - } - - return messages; - } - public ChatSelectChannel MapLocalIfGhost(ChatSelectChannel channel) { if (channel == ChatSelectChannel.Local && _ghost is {IsGhost: true}) @@ -849,11 +794,7 @@ public sealed class ChatUIController : UIController case ChatChannel.LOOC: if (_cfg.GetCVar(CCVars.LoocAboveHeadShow)) - { - const string prefixText = "(LOOC: "; - const string prefixEndText = ")"; - AddSpeechBubble(msg, SpeechBubble.SpeechType.Looc, prefixText, prefixEndText); - } + AddSpeechBubble(msg, SpeechBubble.SpeechType.Looc); break; } } @@ -896,7 +837,7 @@ public sealed class ChatUIController : UIController } } - private readonly record struct SpeechBubbleData(string Message, SpeechBubble.SpeechType Type); + private readonly record struct SpeechBubbleData(ChatMessage Message, SpeechBubble.SpeechType Type); private sealed class SpeechBubbleQueueData { diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 999ef21e25..5b03816f26 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -1476,6 +1476,12 @@ namespace Content.Shared.CCVar public static readonly CVarDef ChatShowTypingIndicator = CVarDef.Create("chat.show_typing_indicator", true, CVar.CLIENTONLY); + public static readonly CVarDef ChatEnableFancyBubbles = + CVarDef.Create("chat.enable_fancy_bubbles", true, CVar.CLIENTONLY | CVar.ARCHIVE, "Toggles displaying fancy speech bubbles, which display the speaking character's name."); + + public static readonly CVarDef ChatFancyNameBackground = + CVarDef.Create("chat.fancy_name_background", false, CVar.CLIENTONLY | CVar.ARCHIVE, "Toggles displaying a background under the speaking character's name."); + /// /// A message broadcast to each player that joins the lobby. /// May be changed by admins ingame through use of the "set-motd" command. diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl index 2690a6dfdb..0c817479a2 100644 --- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl +++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl @@ -21,11 +21,11 @@ chat-manager-whisper-headset-on-message = You can't whisper on the radio! chat-manager-server-wrap-message = [bold]{$message}[/bold] chat-manager-sender-announcement-wrap-message = [font size=14][bold]{$sender} Announcement:[/font][font size=12] {$message}[/bold][/font] -chat-manager-entity-say-wrap-message = [bold]{$entityName}[/bold] {$verb}, [font={$fontType} size={$fontSize}]"{$message}"[/font] -chat-manager-entity-say-bold-wrap-message = [bold]{$entityName}[/bold] {$verb}, [font={$fontType} size={$fontSize}][bold]"{$message}"[/bold][/font] +chat-manager-entity-say-wrap-message = [BubbleHeader][bold]{$entityName}[/bold][/BubbleHeader] {$verb}, [font={$fontType} size={$fontSize}]"[BubbleContent]{$message}[/BubbleContent]"[/font] +chat-manager-entity-say-bold-wrap-message = [BubbleHeader][bold]{$entityName}[/bold][/BubbleHeader] {$verb}, [font={$fontType} size={$fontSize}]"[BubbleContent][bold]{$message}[/bold][/BubbleContent]"[/font] -chat-manager-entity-whisper-wrap-message = [font size=11][italic]{$entityName} whispers, "{$message}"[/italic][/font] -chat-manager-entity-whisper-unknown-wrap-message = [font size=11][italic]Someone whispers, "{$message}"[/italic][/font] +chat-manager-entity-whisper-wrap-message = [font size=11][italic][BubbleHeader]{$entityName}[/BubbleHeader] whispers,"[BubbleContent]{$message}[/BubbleContent]"[/italic][/font] +chat-manager-entity-whisper-unknown-wrap-message = [font size=11][italic][BubbleHeader]Someone[/BubbleHeader] whispers, "[BubbleContent]{$message}[/BubbleContent]"[/italic][/font] # THE() is not used here because the entity and its name can technically be disconnected if a nameOverride is passed... chat-manager-entity-me-wrap-message = [italic]{ PROPER($entity) -> diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index 7c541e0257..da9edc6f75 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -30,6 +30,8 @@ ui-options-volume-percent = { TOSTRING($volume, "P0") } ui-options-show-held-item = Show held item next to cursor? ui-options-show-combat-mode-indicators = Show combat mode indicators with cursor? ui-options-show-looc-on-head = Show LOOC chat above characters head? +ui-options-fancy-speech = Show names in speech bubbles? +ui-options-fancy-name-background = Add background to speech bubble names? ui-options-vsync = VSync ui-options-fullscreen = Fullscreen ui-options-lighting-label = Lighting Quality: -- 2.51.2