fix: Support hex RGB colors (#RRGGBB) in chat messages (fixes #2054)

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
This commit is contained in:
BruceChen 2026-04-06 01:18:05 +08:00
parent 914c56fc6e
commit 7fd70ccf38
4 changed files with 80 additions and 6 deletions

View file

@ -1,4 +1,6 @@
using System; using System;
using System.Text.RegularExpressions;
using static MinecraftClient.Settings.ConsoleConfigHealper.ConsoleConfig;
namespace MinecraftClient namespace MinecraftClient
{ {
@ -26,11 +28,29 @@ namespace MinecraftClient
ConsoleInteractive.ConsoleWriter.WriteLine(text); ConsoleInteractive.ConsoleWriter.WriteLine(text);
} }
private static readonly Regex HexColorRegex = new(@"§#([0-9a-fA-F]{6})", RegexOptions.Compiled);
public void WriteLineFormatted(string text) public void WriteLineFormatted(string text)
{ {
bool hasHex = text.Contains("§#");
if (hasHex)
text = ResolveHexColors(text);
ConsoleInteractive.ConsoleWriter.WriteLineFormatted(text); ConsoleInteractive.ConsoleWriter.WriteLineFormatted(text);
} }
private static string ResolveHexColors(string text)
{
var mode = Settings.Config.Console.General.ConsoleColorMode;
return HexColorRegex.Replace(text, match =>
{
ReadOnlySpan<char> 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() public void BeginReadThread()
{ {
ConsoleInteractive.ConsoleReader.MessageReceived += ForwardMessage; ConsoleInteractive.ConsoleReader.MessageReceived += ForwardMessage;

View file

@ -200,7 +200,12 @@ namespace MinecraftClient.Protocol.Message
/// <returns>Color code</returns> /// <returns>Color code</returns>
private static string Color2tag(string colorname) 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 #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;
}
/// <summary> /// <summary>
/// Specify whether translation rules have been loaded /// Specify whether translation rules have been loaded
/// </summary> /// </summary>
@ -443,8 +459,8 @@ namespace MinecraftClient.Protocol.Message
{ "italic", "o" }, { "italic", "o" },
}; };
/// <summary>Matches a single color code (§0§9, §a§f). Used to strip color when replacing.</summary> /// <summary>Matches a single color code (§0-§9, §a-§f) or a hex color (§#rrggbb). Used to strip color when replacing.</summary>
private static readonly Regex ColorCodeRegex = new(@"§[0-9a-f]", RegexOptions.Compiled); private static readonly Regex ColorCodeRegex = new(@"§(?:[0-9a-f]|#[0-9a-f]{6})", RegexOptions.Compiled);
/// <summary> /// <summary>
/// Use a JSON Object to build the corresponding string /// Use a JSON Object to build the corresponding string

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -612,7 +612,7 @@ namespace MinecraftClient.Scripting
} }
/// <summary> /// <summary>
/// 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
/// </summary> /// </summary>
public static string GetVerbatim(string? text) public static string GetVerbatim(string? text)
{ {
@ -623,10 +623,20 @@ namespace MinecraftClient.Scripting
var data = new char[text.Length]; var data = new char[text.Length];
for (int i = 0; i < text.Length; i++) for (int i = 0; i < text.Length; i++)
{
if (text[i] != '§') if (text[i] != '§')
{
data[idx++] = 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 else
i++; {
i++; // skip §x (2 chars total)
}
}
return new string(data, 0, idx); return new string(data, 0, idx);
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Media; using Avalonia.Media;
@ -63,6 +64,19 @@ namespace MinecraftClient.Tui
if (i > start) if (i > start)
AddRun(tb, text[start..i], currentColor, bold, italic, underline, strikethrough); 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]); char code = char.ToLower(text[i + 1]);
if (ColorMap.TryGetValue(code, out var brush)) if (ColorMap.TryGetValue(code, out var brush))
@ -108,6 +122,20 @@ namespace MinecraftClient.Tui
return tb; return tb;
} }
private static bool TryParseHexColor(ReadOnlySpan<char> 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, private static void AddRun(TextBlock tb, string text, IBrush color,
bool bold, bool italic, bool underline, bool strikethrough) bool bold, bool italic, bool underline, bool strikethrough)
{ {