From 7fd70ccf383f01e534c729e16bec27977849ab3b Mon Sep 17 00:00:00 2001 From: BruceChen Date: Mon, 6 Apr 2026 01:18:05 +0800 Subject: [PATCH] fix: Support hex RGB colors (#RRGGBB) in chat messages (fixes #2054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minecraft 1.16+ servers can send custom hex colors in JSON text components ("color": "#RRGGBB"). MCC's ChatParser.Color2tag() only recognized the 16 named colors, silently dropping hex values and leaving gradient/custom-colored chat as uncolored plain text. - ChatParser: recognize hex color values and emit internal §#rrggbb encoding; extend ColorCodeRegex to match the new format - ClassicConsoleBackend: resolve §#rrggbb to ANSI escape codes via ColorHelper before passing to ConsoleInteractive (adapts to the configured ConsoleColorMode: 24-bit, 8-bit, 4-bit, or disable) - McColorParser (TUI): parse §#rrggbb into exact SolidColorBrush for full RGB fidelity in Avalonia - ChatBot.GetVerbatim(): skip the full 8-char §#rrggbb sequence instead of only 2 chars, preventing hex digits from leaking into stripped text Made-with: Cursor --- MinecraftClient/ClassicConsoleBackend.cs | 20 +++++++++++++ .../Protocol/Message/ChatParser.cs | 22 +++++++++++++-- MinecraftClient/Scripting/ChatBot.cs | 16 +++++++++-- MinecraftClient/Tui/McColorParser.cs | 28 +++++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/MinecraftClient/ClassicConsoleBackend.cs b/MinecraftClient/ClassicConsoleBackend.cs index ac4912c9..099447b2 100644 --- a/MinecraftClient/ClassicConsoleBackend.cs +++ b/MinecraftClient/ClassicConsoleBackend.cs @@ -1,4 +1,6 @@ using System; +using System.Text.RegularExpressions; +using static MinecraftClient.Settings.ConsoleConfigHealper.ConsoleConfig; namespace MinecraftClient { @@ -26,11 +28,29 @@ namespace MinecraftClient ConsoleInteractive.ConsoleWriter.WriteLine(text); } + private static readonly Regex HexColorRegex = new(@"§#([0-9a-fA-F]{6})", RegexOptions.Compiled); + public void WriteLineFormatted(string text) { + bool hasHex = text.Contains("§#"); + if (hasHex) + text = ResolveHexColors(text); ConsoleInteractive.ConsoleWriter.WriteLineFormatted(text); } + private static string ResolveHexColors(string text) + { + var mode = Settings.Config.Console.General.ConsoleColorMode; + return HexColorRegex.Replace(text, match => + { + ReadOnlySpan hex = match.Groups[1].ValueSpan; + byte r = Convert.ToByte(hex[..2].ToString(), 16); + byte g = Convert.ToByte(hex[2..4].ToString(), 16); + byte b = Convert.ToByte(hex[4..6].ToString(), 16); + return ColorHelper.GetColorEscapeCode(r, g, b, foreground: true, mode); + }); + } + public void BeginReadThread() { ConsoleInteractive.ConsoleReader.MessageReceived += ForwardMessage; diff --git a/MinecraftClient/Protocol/Message/ChatParser.cs b/MinecraftClient/Protocol/Message/ChatParser.cs index e0954ee2..1ca981d6 100644 --- a/MinecraftClient/Protocol/Message/ChatParser.cs +++ b/MinecraftClient/Protocol/Message/ChatParser.cs @@ -200,7 +200,12 @@ namespace MinecraftClient.Protocol.Message /// Color code private static string Color2tag(string colorname) { - return colorname.ToLower() switch + string lower = colorname.ToLower(); + + if (lower.Length == 7 && lower[0] == '#' && IsHexColor(lower)) + return "§" + lower; + + return lower switch { #pragma warning disable format // @formatter:off @@ -227,6 +232,17 @@ namespace MinecraftClient.Protocol.Message }; } + private static bool IsHexColor(string s) + { + for (int i = 1; i < s.Length; i++) + { + char c = s[i]; + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) + return false; + } + return true; + } + /// /// Specify whether translation rules have been loaded /// @@ -443,8 +459,8 @@ namespace MinecraftClient.Protocol.Message { "italic", "o" }, }; - /// Matches a single color code (§0–§9, §a–§f). Used to strip color when replacing. - private static readonly Regex ColorCodeRegex = new(@"§[0-9a-f]", RegexOptions.Compiled); + /// Matches a single color code (§0-§9, §a-§f) or a hex color (§#rrggbb). Used to strip color when replacing. + private static readonly Regex ColorCodeRegex = new(@"§(?:[0-9a-f]|#[0-9a-f]{6})", RegexOptions.Compiled); /// /// Use a JSON Object to build the corresponding string diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index f639edfa..45236fda 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -612,7 +612,7 @@ namespace MinecraftClient.Scripting } /// - /// Remove color codes ("§c") from a text message received from the server + /// Remove color codes ("§c" and "§#rrggbb") from a text message received from the server /// public static string GetVerbatim(string? text) { @@ -623,10 +623,20 @@ namespace MinecraftClient.Scripting var data = new char[text.Length]; for (int i = 0; i < text.Length; i++) + { if (text[i] != '§') + { data[idx++] = text[i]; + } + else if (i + 1 < text.Length && text[i + 1] == '#' && i + 7 < text.Length) + { + i += 7; // skip §#rrggbb (8 chars total, loop increments once) + } else - i++; + { + i++; // skip §x (2 chars total) + } + } return new string(data, 0, idx); } diff --git a/MinecraftClient/Tui/McColorParser.cs b/MinecraftClient/Tui/McColorParser.cs index 30f8f280..8f3ac5f2 100644 --- a/MinecraftClient/Tui/McColorParser.cs +++ b/MinecraftClient/Tui/McColorParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Media; @@ -63,6 +64,19 @@ namespace MinecraftClient.Tui if (i > start) AddRun(tb, text[start..i], currentColor, bold, italic, underline, strikethrough); + if (text[i + 1] == '#' && i + 8 <= text.Length + && TryParseHexColor(text.AsSpan(i + 2, 6), out var hexBrush)) + { + currentColor = hexBrush; + bold = false; + italic = false; + underline = false; + strikethrough = false; + i += 7; + start = i + 1; + continue; + } + char code = char.ToLower(text[i + 1]); if (ColorMap.TryGetValue(code, out var brush)) @@ -108,6 +122,20 @@ namespace MinecraftClient.Tui return tb; } + private static bool TryParseHexColor(ReadOnlySpan hex, out IBrush brush) + { + if (hex.Length == 6 + && byte.TryParse(hex[..2], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte r) + && byte.TryParse(hex[2..4], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte g) + && byte.TryParse(hex[4..6], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte b)) + { + brush = new SolidColorBrush(Color.FromRgb(r, g, b)); + return true; + } + brush = Brushes.White; + return false; + } + private static void AddRun(TextBlock tb, string text, IBrush color, bool bold, bool italic, bool underline, bool strikethrough) {