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;
{
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,
/// </summary>
private const float EntityVerticalOffset = 0.5f;
- private readonly IEyeManager _eyeManager;
+ /// <summary>
+ /// The default maximum width for speech bubbles.
+ /// </summary>
+ public const float SpeechMaxWidth = 256;
+
private readonly EntityUid _senderEntity;
- private readonly IChatManager _chatManager;
- private readonly IEntityManager _entityManager;
private float _timeLeft = TotalTime;
// man down
public event Action<EntityUid, SpeechBubble>? 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);
_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)
{
_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
{
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;
+ }
+ }
}
ShowHeldItemCheckBox.OnToggled += OnCheckBoxToggled;
ShowCombatModeIndicatorsCheckBox.OnToggled += OnCheckBoxToggled;
ShowLoocAboveHeadCheckBox.OnToggled += OnCheckBoxToggled;
+ FancySpeechBubblesCheckBox.OnToggled += OnCheckBoxToggled;
+ FancyNameBackgroundsCheckBox.OnToggled += OnCheckBoxToggled;
IntegerScalingCheckBox.OnToggled += OnCheckBoxToggled;
ViewportLowResCheckBox.OnToggled += OnCheckBoxToggled;
ParallaxLowQualityCheckBox.OnToggled += OnCheckBoxToggled;
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());
_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);
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);
isShowHeldItemSame &&
isCombatModeIndicatorsSame &&
isLoocShowSame &&
+ isFancyChatSame &&
+ isFancyBackgroundSame &&
isFpsCounterVisibleSame &&
isWidthSame &&
isLayoutSame;
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);
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;
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<TransformComponent>(entity).MapID != _eye.CurrentMap)
_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)
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.
}
}
- private List<string> SplitMessage(string msg)
- {
- // Split message into words separated by spaces.
- var words = msg.Split(' ');
- var messages = new List<string>();
- var currentBuffer = new List<string>();
-
- // 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})
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;
}
}
}
}
- private readonly record struct SpeechBubbleData(string Message, SpeechBubble.SpeechType Type);
+ private readonly record struct SpeechBubbleData(ChatMessage Message, SpeechBubble.SpeechType Type);
private sealed class SpeechBubbleQueueData
{