Skip to content

Commit 7fd70cc

Browse files
committed
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
1 parent 914c56f commit 7fd70cc

4 files changed

Lines changed: 80 additions & 6 deletions

File tree

MinecraftClient/ClassicConsoleBackend.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Text.RegularExpressions;
3+
using static MinecraftClient.Settings.ConsoleConfigHealper.ConsoleConfig;
24

35
namespace MinecraftClient
46
{
@@ -26,11 +28,29 @@ public void WriteLine(string text)
2628
ConsoleInteractive.ConsoleWriter.WriteLine(text);
2729
}
2830

31+
private static readonly Regex HexColorRegex = new(@"§#([0-9a-fA-F]{6})", RegexOptions.Compiled);
32+
2933
public void WriteLineFormatted(string text)
3034
{
35+
bool hasHex = text.Contains("§#");
36+
if (hasHex)
37+
text = ResolveHexColors(text);
3138
ConsoleInteractive.ConsoleWriter.WriteLineFormatted(text);
3239
}
3340

41+
private static string ResolveHexColors(string text)
42+
{
43+
var mode = Settings.Config.Console.General.ConsoleColorMode;
44+
return HexColorRegex.Replace(text, match =>
45+
{
46+
ReadOnlySpan<char> hex = match.Groups[1].ValueSpan;
47+
byte r = Convert.ToByte(hex[..2].ToString(), 16);
48+
byte g = Convert.ToByte(hex[2..4].ToString(), 16);
49+
byte b = Convert.ToByte(hex[4..6].ToString(), 16);
50+
return ColorHelper.GetColorEscapeCode(r, g, b, foreground: true, mode);
51+
});
52+
}
53+
3454
public void BeginReadThread()
3555
{
3656
ConsoleInteractive.ConsoleReader.MessageReceived += ForwardMessage;

MinecraftClient/Protocol/Message/ChatParser.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,12 @@ public static string ParseSignedChat(ChatMessage message, List<string>? links =
200200
/// <returns>Color code</returns>
201201
private static string Color2tag(string colorname)
202202
{
203-
return colorname.ToLower() switch
203+
string lower = colorname.ToLower();
204+
205+
if (lower.Length == 7 && lower[0] == '#' && IsHexColor(lower))
206+
return "§" + lower;
207+
208+
return lower switch
204209
{
205210
#pragma warning disable format // @formatter:off
206211

@@ -227,6 +232,17 @@ private static string Color2tag(string colorname)
227232
};
228233
}
229234

235+
private static bool IsHexColor(string s)
236+
{
237+
for (int i = 1; i < s.Length; i++)
238+
{
239+
char c = s[i];
240+
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')))
241+
return false;
242+
}
243+
return true;
244+
}
245+
230246
/// <summary>
231247
/// Specify whether translation rules have been loaded
232248
/// </summary>
@@ -443,8 +459,8 @@ private static string TranslateString(string rulename, List<string> using_data)
443459
{ "italic", "o" },
444460
};
445461

446-
/// <summary>Matches a single color code (§0§9, §a–§f). Used to strip color when replacing.</summary>
447-
private static readonly Regex ColorCodeRegex = new(@"§[0-9a-f]", RegexOptions.Compiled);
462+
/// <summary>Matches a single color code (§0-§9, §a-§f) or a hex color (§#rrggbb). Used to strip color when replacing.</summary>
463+
private static readonly Regex ColorCodeRegex = new(@"§(?:[0-9a-f]|#[0-9a-f]{6})", RegexOptions.Compiled);
448464

449465
/// <summary>
450466
/// Use a JSON Object to build the corresponding string

MinecraftClient/Scripting/ChatBot.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
@@ -612,7 +612,7 @@ protected bool PerformInternalCommand(string command, ref CmdResult result, Dict
612612
}
613613

614614
/// <summary>
615-
/// Remove color codes ("§c") from a text message received from the server
615+
/// Remove color codes ("§c" and "§#rrggbb") from a text message received from the server
616616
/// </summary>
617617
public static string GetVerbatim(string? text)
618618
{
@@ -623,10 +623,20 @@ public static string GetVerbatim(string? text)
623623
var data = new char[text.Length];
624624

625625
for (int i = 0; i < text.Length; i++)
626+
{
626627
if (text[i] != '§')
628+
{
627629
data[idx++] = text[i];
630+
}
631+
else if (i + 1 < text.Length && text[i + 1] == '#' && i + 7 < text.Length)
632+
{
633+
i += 7; // skip §#rrggbb (8 chars total, loop increments once)
634+
}
628635
else
629-
i++;
636+
{
637+
i++; // skip §x (2 chars total)
638+
}
639+
}
630640

631641
return new string(data, 0, idx);
632642
}

MinecraftClient/Tui/McColorParser.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using Avalonia.Controls;
45
using Avalonia.Controls.Documents;
56
using Avalonia.Media;
@@ -63,6 +64,19 @@ public static TextBlock CreateColoredTextBlock(string text, TextWrapping wrappin
6364
if (i > start)
6465
AddRun(tb, text[start..i], currentColor, bold, italic, underline, strikethrough);
6566

67+
if (text[i + 1] == '#' && i + 8 <= text.Length
68+
&& TryParseHexColor(text.AsSpan(i + 2, 6), out var hexBrush))
69+
{
70+
currentColor = hexBrush;
71+
bold = false;
72+
italic = false;
73+
underline = false;
74+
strikethrough = false;
75+
i += 7;
76+
start = i + 1;
77+
continue;
78+
}
79+
6680
char code = char.ToLower(text[i + 1]);
6781

6882
if (ColorMap.TryGetValue(code, out var brush))
@@ -108,6 +122,20 @@ public static TextBlock CreateColoredTextBlock(string text, TextWrapping wrappin
108122
return tb;
109123
}
110124

125+
private static bool TryParseHexColor(ReadOnlySpan<char> hex, out IBrush brush)
126+
{
127+
if (hex.Length == 6
128+
&& byte.TryParse(hex[..2], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte r)
129+
&& byte.TryParse(hex[2..4], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte g)
130+
&& byte.TryParse(hex[4..6], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte b))
131+
{
132+
brush = new SolidColorBrush(Color.FromRgb(r, g, b));
133+
return true;
134+
}
135+
brush = Brushes.White;
136+
return false;
137+
}
138+
111139
private static void AddRun(TextBlock tb, string text, IBrush color,
112140
bool bold, bool italic, bool underline, bool strikethrough)
113141
{

0 commit comments

Comments
 (0)