diff --git a/Framework/Intersect.Framework.Core/Color.cs b/Framework/Intersect.Framework.Core/Color.cs index c9ec7faf70..9ccb061353 100644 --- a/Framework/Intersect.Framework.Core/Color.cs +++ b/Framework/Intersect.Framework.Core/Color.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Reflection; using Intersect.Framework; using Intersect.Localization; using MessagePack; @@ -9,7 +10,17 @@ namespace Intersect; [MessagePackObject] public partial class Color : IEquatable { - + private static readonly Dictionary> KnownColors; + static Color() + { + KnownColors = typeof(Color) + .GetProperties(BindingFlags.Public | BindingFlags.Static) + .Where(p => p.PropertyType == typeof(Color) && p.GetMethod != null) + .ToDictionary( + p => p.Name.ToLowerInvariant(), + p => (Func)(() => p.GetMethod?.Invoke(null, null) as Color ?? Color.White) + ); + } public enum ChatColor { @@ -289,7 +300,7 @@ public static Color FromRgba(int rgba) return new Color(a: a, r: r, g: g, b: b); } - public static Color? FromString(string val, Color? defaultColor = default) + public static Color? FromCsv(string val, Color? defaultColor = default) { if (string.IsNullOrEmpty(val)) { @@ -306,6 +317,27 @@ public static Color FromRgba(int rgba) return new Color(parts[0], parts[1], parts[2], parts[3]); } + public static Color? FromString(string val, Color? defaultColor = default) + { + if (string.IsNullOrWhiteSpace(val)) + { + return defaultColor; + } + + val = val.Trim().ToLowerInvariant(); + + if (KnownColors.TryGetValue(val, out Func? colorFunc)) + { + return colorFunc(); + } + + Color? parsedColor = Color.FromHex(val); + parsedColor ??= Color.FromCsv(val); + + return parsedColor ?? defaultColor; + } + + public static implicit operator Color(string colorString) => FromString(colorString); public static Color operator *(Color left, Color right) => new Color( diff --git a/Intersect.Client.Core/Interface/Game/EventWindow.cs b/Intersect.Client.Core/Interface/Game/EventWindow.cs index 279131057b..2ffebbe123 100644 --- a/Intersect.Client.Core/Interface/Game/EventWindow.cs +++ b/Intersect.Client.Core/Interface/Game/EventWindow.cs @@ -10,6 +10,7 @@ using Intersect.Client.Interface.Game.Typewriting; using Intersect.Client.Localization; using Intersect.Client.Networking; +using Intersect.Client.Utilities; using Intersect.Configuration; using Intersect.Enums; using Intersect.Utilities; @@ -160,8 +161,13 @@ private EventWindow(Canvas gameCanvas, Dialog dialog) : base(gameCanvas, nameof( SkipRender(); _promptLabel.ClearText(); - _promptLabel.AddText(_dialog.Prompt ?? string.Empty, _promptTemplateLabel); - _promptLabel.ForceImmediateRebuild(); + var parsedText = TextColorParser.Parse(_dialog.Prompt ?? string.Empty, Color.White); + + foreach (var segment in parsedText) + { + _promptLabel.AddText(segment.Text, segment.Color, Alignments.Left, _promptTemplateLabel.Font); + } + _ = _promptLabel.SizeToChildren(); _typewriting = ClientConfiguration.Instance.TypewriterEnabled && @@ -169,9 +175,11 @@ private EventWindow(Canvas gameCanvas, Dialog dialog) : base(gameCanvas, nameof( if (_typewriting) { _promptLabel.ClearText(); - _writer = new Typewriter(_dialog.Prompt ?? string.Empty, text => _promptLabel.AppendText(text, _promptTemplateLabel)); + _writer = new Typewriter(parsedText.ToArray(), (text, color) => + { + _promptLabel.AppendText(text, color, Alignments.Left, _promptTemplateLabel.Font); + }); } - Defer( () => { diff --git a/Intersect.Client.Core/Interface/Game/Typewriting/Typewriter.cs b/Intersect.Client.Core/Interface/Game/Typewriting/Typewriter.cs index 502c53e55c..8d746e740a 100644 --- a/Intersect.Client.Core/Interface/Game/Typewriting/Typewriter.cs +++ b/Intersect.Client.Core/Interface/Game/Typewriting/Typewriter.cs @@ -1,4 +1,5 @@ using Intersect.Client.Core; +using Intersect.Client.Utilities; using Intersect.Configuration; using Intersect.Utilities; @@ -6,8 +7,7 @@ namespace Intersect.Client.Interface.Game.Typewriting; internal sealed class Typewriter { - public delegate void TextWrittenHandler(string text); - + public delegate void TextWrittenHandler(string text, Color color); private static HashSet FullStops => ClientConfiguration.Instance.TypewriterFullStops; private static long FullStopSpeed => ClientConfiguration.Instance.TypewriterFullStopDelay; private static HashSet PartialStops => ClientConfiguration.Instance.TypewriterPauses; @@ -15,24 +15,24 @@ internal sealed class Typewriter private static long TypingSpeed => ClientConfiguration.Instance.TypewriterPartDelay; private int _offset; + private int _segmentIndex; private string? _lastText; private long _nextUpdateTime; private readonly TextWrittenHandler _textWrittenHandler; - private readonly string _text; + private readonly ColorizedText[] _segments; public bool IsDone { get; private set; } public long DoneAtMilliseconds { get; private set; } - public Typewriter(string text, TextWrittenHandler textWrittenHandler) + public Typewriter(ColorizedText[] segments, TextWrittenHandler textWrittenHandler) { - _text = text.ReplaceLineEndings("\n"); + _segments = segments; _textWrittenHandler = textWrittenHandler; _nextUpdateTime = Timing.Global.MillisecondsUtc; - + _segmentIndex = 0; _offset = 0; _lastText = null; - IsDone = false; } @@ -43,7 +43,7 @@ public void Write(string? soundName) return; } - if (_offset >= _text.Length) + if (_segmentIndex >= _segments.Length) { End(); return; @@ -57,28 +57,44 @@ public void Write(string? soundName) var emitSound = false; while (_nextUpdateTime <= Timing.Global.MillisecondsUtc) { - if (_offset >= _text.Length) + if (_segmentIndex >= _segments.Length) { End(); - break; + return; } emitSound |= _offset % ClientConfiguration.Instance.TypewriterSoundFrequency == 0; + var segment = _segments[_segmentIndex]; + + if (_offset >= segment.Text.Length) + { + _offset = 0; + _segmentIndex++; + + if (_segmentIndex >= _segments.Length) + { + End(); + continue; + } + + segment = _segments[_segmentIndex]; + } + string nextText; - if (char.IsSurrogatePair(_text, _offset)) + if (char.IsSurrogatePair(segment.Text, _offset)) { - nextText = _text[_offset..(_offset + 2)]; + nextText = segment.Text[_offset..(_offset + 2)]; _offset += 2; } else { - nextText = _text[_offset..(_offset + 1)]; + nextText = segment.Text[_offset..(_offset + 1)]; ++_offset; } _nextUpdateTime += GetTypingDelayFor(nextText, _lastText); - _textWrittenHandler(nextText); + _textWrittenHandler(nextText, segment.Color); _lastText = nextText; } @@ -118,14 +134,20 @@ private static long GetTypingDelayFor(string next, string? last) public void End() { - if (IsDone || _text.Length < 1) + if (IsDone) { return; } - if (_offset < _text.Length) + while (_segmentIndex < _segments.Length) { - _textWrittenHandler(_text[_offset..]); + var segment = _segments[_segmentIndex]; + if (_offset < segment.Text.Length) + { + _textWrittenHandler(segment.Text[_offset..], segment.Color); + } + _segmentIndex++; + _offset = 0; } IsDone = true; diff --git a/Intersect.Client.Core/Utilities/TextColorParser.cs b/Intersect.Client.Core/Utilities/TextColorParser.cs new file mode 100644 index 0000000000..385940b4b0 --- /dev/null +++ b/Intersect.Client.Core/Utilities/TextColorParser.cs @@ -0,0 +1,47 @@ +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Intersect.Client.Utilities +{ + public static class TextColorParser + { + private static readonly Regex ColorTagRegex = new(@"(\\c{[^}]*})", RegexOptions.Compiled); + + public static List Parse(string text, Color defaultColor) + { + var segments = ColorTagRegex.Split(text) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToArray(); + + var output = new List(segments.Length); + var currentColor = defaultColor; + + foreach (var segment in segments) + { + if (segment.StartsWith("\\c{") && segment.EndsWith("}")) + { + string colorCode = segment[3..^1].ToLowerInvariant(); + currentColor = Color.FromString(colorCode, defaultColor); + } + else + { + output.Add(new ColorizedText(segment, currentColor)); + } + } + + return output; + } + } + + public class ColorizedText + { + public string Text { get; } + public Color Color { get; } + + public ColorizedText(string text, Color color) + { + Text = text; + Color = color; + } + } +} diff --git a/Intersect.Client.Framework/Gwen/Control/RichLabel.cs b/Intersect.Client.Framework/Gwen/Control/RichLabel.cs index 9410c6bf9a..8b538da582 100644 --- a/Intersect.Client.Framework/Gwen/Control/RichLabel.cs +++ b/Intersect.Client.Framework/Gwen/Control/RichLabel.cs @@ -183,8 +183,8 @@ public void AppendText(string text, Color? color, Alignments alignment, GameFont var appendAlignment = alignment; var appendFont = font ?? lastTextBlock.Font; - if (appendAlignment != lastTextBlock.Alignment && - appendColor != lastTextBlock.Color && + if (appendAlignment != lastTextBlock.Alignment || + appendColor != lastTextBlock.Color || appendFont != lastTextBlock.Font) { AddText(text, color, alignment, font);