fix: decode modern chat type holders correctly 1.21.1+

fix: decode modern chat type holders correctly
This commit is contained in:
Anon 2026-07-27 22:25:07 +02:00 committed by GitHub
commit 503652a760
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 339 additions and 73 deletions

View file

@ -0,0 +1,159 @@
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Tests;
public sealed class ChatTypeHolderTests
{
[Fact]
public void ReferenceHolderUsesOneBasedWireIdIn121AndNewer()
{
var dataTypes = new DataTypes(Protocol18Handler.MC_1_21_Version);
var packetData = new Queue<byte>(DataTypes.GetVarInt(2));
int chatTypeId = ChatParser.ReadChatTypeHolder(
dataTypes,
packetData,
Protocol18Handler.MC_1_21_Version,
out var directDecoration);
Assert.Equal(1, chatTypeId);
Assert.Null(directDecoration);
Assert.Empty(packetData);
}
[Fact]
public void RegistryIdRemainsUnchangedBefore121()
{
var dataTypes = new DataTypes(Protocol18Handler.MC_1_20_6_Version);
var packetData = new Queue<byte>(DataTypes.GetVarInt(2));
int chatTypeId = ChatParser.ReadChatTypeHolder(
dataTypes,
packetData,
Protocol18Handler.MC_1_20_6_Version,
out var directDecoration);
Assert.Equal(2, chatTypeId);
Assert.Null(directDecoration);
Assert.Empty(packetData);
}
[Fact]
public void DirectHolderConsumesChatAndNarrationDecorations()
{
var dataTypes = new DataTypes(Protocol18Handler.MC_1_21_Version);
var packetBytes = new List<byte>();
packetBytes.AddRange(DataTypes.GetVarInt(0));
AddDecoration(packetBytes, dataTypes, "chat.type.text", 0, 2);
AddDecoration(packetBytes, dataTypes, "chat.type.text.narrate", 0, 2);
var packetData = new Queue<byte>(packetBytes);
int chatTypeId = ChatParser.ReadChatTypeHolder(
dataTypes,
packetData,
Protocol18Handler.MC_1_21_Version,
out var directDecoration);
Assert.Equal(-1, chatTypeId);
Assert.NotNull(directDecoration);
Assert.Equal("chat.type.text", directDecoration.TranslationKey);
Assert.Equal(
[ChatParser.ChatTypeParameter.Sender, ChatParser.ChatTypeParameter.Content],
directDecoration.Parameters);
Assert.Empty(packetData);
}
[Fact]
public void RegistryDecorationControlsParameterSelectionAndOrdering()
{
Dictionary<int, ChatParser.MessageType>? originalChatTypes = ChatParser.ChatId2Type;
try
{
ChatParser.ClearChatTypeDecorations();
ChatParser.ChatId2Type = [];
var chatTypeData = new Dictionary<string, object>
{
["chat"] = new Dictionary<string, object>
{
["translation_key"] = "commands.message.display.outgoing",
["parameters"] = new object[] { "target", "content" }
}
};
ChatParser.ReadChatType(42, "example:custom_chat", chatTypeData);
var message = new ChatMessage(
"hello",
false,
42,
Guid.Empty,
null,
"Alice",
"Bob",
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
null,
false);
string rendered = ChatParser.ParseSignedChat(message);
Assert.Equal("You whisper to Bob: hello", rendered);
}
finally
{
ChatParser.ClearChatTypeDecorations();
ChatParser.ChatId2Type = originalChatTypes;
}
}
[Fact]
public void UnknownTranslationKeyIsUsedAsVanillaFormatPattern()
{
Dictionary<int, ChatParser.MessageType>? originalChatTypes = ChatParser.ChatId2Type;
try
{
ChatParser.ClearChatTypeDecorations();
ChatParser.ChatId2Type = [];
var chatTypeData = new Dictionary<string, object>
{
["chat"] = new Dictionary<string, object>
{
["translation_key"] = "%s",
["parameters"] = new object[] { "sender", "content" }
}
};
ChatParser.ReadChatType(42, "ordinary:custom_chat", chatTypeData);
var message = new ChatMessage(
"hello",
false,
42,
Guid.Empty,
null,
"Alice » hello",
null,
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
null,
false);
string rendered = ChatParser.ParseSignedChat(message);
Assert.Equal("Alice » hello", rendered);
}
finally
{
ChatParser.ClearChatTypeDecorations();
ChatParser.ChatId2Type = originalChatTypes;
}
}
private static void AddDecoration(
List<byte> packetBytes,
DataTypes dataTypes,
string translationKey,
params int[] parameters)
{
packetBytes.AddRange(dataTypes.GetString(translationKey));
packetBytes.AddRange(DataTypes.GetVarInt(parameters.Length));
foreach (int parameter in parameters)
packetBytes.AddRange(DataTypes.GetVarInt(parameter));
packetBytes.AddRange(dataTypes.GetNbtTag(new Dictionary<string, object>()));
}
}

View file

@ -3796,6 +3796,8 @@ namespace MinecraftClient
{
UpdateKeepAlive();
Log.Debug(string.Format(Translations.protocol_chat_raw_message, message.content));
List<string> links = new();
string messageText;

View file

@ -287,6 +287,7 @@ namespace MinecraftClient.Protocol.Handlers
},
_ => ChatParser.ChatId2Type
};
ChatParser.ClearChatTypeDecorations();
}
/// <summary>
@ -576,7 +577,6 @@ namespace MinecraftClient.Protocol.Handlers
var isEnchantment = registryId == "minecraft:enchantment";
var isDialog = registryId == "minecraft:dialog";
var availableChats = isChat ? new Dictionary<int, string>() : null;
var dimensionIdMap = isDimension ? new Dictionary<int, string>() : null;
var attributeIdMap = isAttribute ? new Dictionary<int, string>() : null;
var enchantmentIdMap = isEnchantment ? new Dictionary<int, string>() : null;
@ -591,7 +591,7 @@ namespace MinecraftClient.Protocol.Handlers
nbtData = dataTypes.ReadNextNbt(packetData);
if (isChat)
availableChats!.Add(i, entryId);
ChatParser.ReadChatType(i, entryId, nbtData);
else if (isDimension)
{
dimensionIdMap!.Add(i, entryId);
@ -611,9 +611,7 @@ namespace MinecraftClient.Protocol.Handlers
handler.OnDialogRegistryData(i, entryId, dialogNbtParser.Parse(nbtData));
}
if (isChat)
ChatParser.ReadChatType(availableChats!);
else if (isDimension)
if (isDimension)
{
World.SetDimensionIdMap(dimensionIdMap!);
if (!handler.GetTerrainEnabled() || !World.HasAnyDimension())
@ -1232,7 +1230,8 @@ namespace MinecraftClient.Protocol.Handlers
// Network Target
// net.minecraft.network.message.MessageType.Serialized#write
var chatTypeId = dataTypes.ReadNextVarInt(packetData);
var chatTypeId = ChatParser.ReadChatTypeHolder(
dataTypes, packetData, protocolVersion, out var directChatTypeDecoration);
var chatName = dataTypes.ReadNextChat(packetData);
var targetName = dataTypes.ReadNextBool(packetData)
? dataTypes.ReadNextChat(packetData)
@ -1281,7 +1280,10 @@ namespace MinecraftClient.Protocol.Handlers
}
ChatMessage chat = new(message, false, chatTypeId, senderUuid, unsignedChatContent,
senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult);
senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult)
{
chatTypeDecoration = directChatTypeDecoration
};
lock (MessageSigningLock)
Acknowledge(chat);
handler.OnTextReceived(chat);
@ -1345,14 +1347,17 @@ namespace MinecraftClient.Protocol.Handlers
break;
case PacketTypesIn.ProfilelessChatMessage:
var message_ = dataTypes.ReadNextChat(packetData);
var messageType_ = dataTypes.ReadNextVarInt(packetData);
var messageType_ = ChatParser.ReadChatTypeHolder(
dataTypes, packetData, protocolVersion, out var directProfilelessChatTypeDecoration);
var messageName = dataTypes.ReadNextChat(packetData);
var targetName_ = dataTypes.ReadNextBool(packetData)
? dataTypes.ReadNextChat(packetData)
: null;
ChatMessage profilelessChat = new(message_, targetName_ ?? messageName, false, messageType_,
ChatMessage profilelessChat = new(message_, messageName, false, messageType_,
Guid.Empty, true);
profilelessChat.isSenderJson = false;
profilelessChat.teamName = targetName_;
profilelessChat.chatTypeDecoration = directProfilelessChatTypeDecoration;
handler.OnTextReceived(profilelessChat);
break;
case PacketTypesIn.CombatEvent:

View file

@ -35,6 +35,8 @@ namespace MinecraftClient.Protocol.Message
public bool? isSignatureLegal;
internal ChatParser.ChatTypeDecoration? chatTypeDecoration;
public ChatMessage(string content, bool isJson, int chatType, Guid senderUUID, string? unsignedContent, string displayName, string? teamName, long timestamp, byte[]? signature, bool isSignatureLegal)
{
isSignedChat = true;

View file

@ -11,6 +11,7 @@ using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using MinecraftClient.Protocol.Handlers;
using Tomlet;
using Tomlet.Models;
using static MinecraftClient.Settings;
@ -36,33 +37,49 @@ namespace MinecraftClient.Protocol.Message
public static Dictionary<int, MessageType>? ChatId2Type;
internal enum ChatTypeParameter
{
Sender,
Target,
Content
}
internal sealed record ChatTypeDecoration(string TranslationKey, ChatTypeParameter[] Parameters);
private static readonly Dictionary<int, ChatTypeDecoration> ChatId2Decoration = new();
internal static void ClearChatTypeDecorations()
{
ChatId2Decoration.Clear();
}
// Used to store Chat Types in 1.20.6+
public static void ReadChatType(Dictionary<int, string> data)
public static void ReadChatType(int chatId, string chatName, Dictionary<string, object>? chatTypeData)
{
var chatTypeDictionary = ChatId2Type ?? new Dictionary<int, MessageType>();
foreach (var (chatId, chatName) in data)
chatTypeDictionary[chatId] = chatName switch
{
chatTypeDictionary[chatId] = chatName switch
{
"minecraft:chat" => MessageType.CHAT,
"minecraft:emote_command" => MessageType.EMOTE_COMMAND,
"minecraft:msg_command_incoming" => MessageType.MSG_COMMAND_INCOMING,
"minecraft:msg_command_outgoing" => MessageType.MSG_COMMAND_OUTGOING,
"minecraft:say_command" => MessageType.SAY_COMMAND,
"minecraft:team_msg_command_incoming" => MessageType.TEAM_MSG_COMMAND_INCOMING,
"minecraft:team_msg_command_outgoing" => MessageType.TEAM_MSG_COMMAND_OUTGOING,
_ => MessageType.CHAT,
};
}
"minecraft:chat" => MessageType.CHAT,
"minecraft:emote_command" => MessageType.EMOTE_COMMAND,
"minecraft:msg_command_incoming" => MessageType.MSG_COMMAND_INCOMING,
"minecraft:msg_command_outgoing" => MessageType.MSG_COMMAND_OUTGOING,
"minecraft:say_command" => MessageType.SAY_COMMAND,
"minecraft:team_msg_command_incoming" => MessageType.TEAM_MSG_COMMAND_INCOMING,
"minecraft:team_msg_command_outgoing" => MessageType.TEAM_MSG_COMMAND_OUTGOING,
_ => MessageType.CHAT,
};
ChatId2Type = chatTypeDictionary;
if (TryReadChatTypeDecoration(chatTypeData, out var decoration))
ChatId2Decoration[chatId] = decoration;
else
ChatId2Decoration.Remove(chatId);
}
public static void ReadChatType(Dictionary<string, object> registryCodec)
{
Dictionary<int, MessageType> chatTypeDictionary = ChatId2Type ?? new();
// Check if the chat type registry is in the correct format
if (!registryCodec.ContainsKey("minecraft:chat_type"))
{
@ -84,25 +101,80 @@ namespace MinecraftClient.Protocol.Message
}
var chatTypeListNbt = (object[])(((Dictionary<string, object>)registryCodec["minecraft:chat_type"])["value"]);
foreach (var (chatName, chatId) in from Dictionary<string, object> chatTypeNbt in chatTypeListNbt
let chatName = (string)chatTypeNbt["name"]
let chatId = (int)chatTypeNbt["id"]
select (chatName, chatId))
foreach (Dictionary<string, object> chatTypeNbt in chatTypeListNbt)
{
chatTypeDictionary[chatId] = chatName switch
string chatName = (string)chatTypeNbt["name"];
int chatId = (int)chatTypeNbt["id"];
var chatTypeData = chatTypeNbt.TryGetValue("element", out var element)
? element as Dictionary<string, object>
: null;
ReadChatType(chatId, chatName, chatTypeData);
}
}
internal static int ReadChatTypeHolder(
DataTypes dataTypes,
Queue<byte> packetData,
int protocolVersion,
out ChatTypeDecoration? directDecoration)
{
int encodedId = dataTypes.ReadNextVarInt(packetData);
directDecoration = null;
if (protocolVersion < Protocol18Handler.MC_1_21_Version)
return encodedId;
if (encodedId > 0)
return encodedId - 1;
directDecoration = ReadNetworkChatTypeDecoration(dataTypes, packetData);
_ = ReadNetworkChatTypeDecoration(dataTypes, packetData); // Narration decoration
return -1;
}
private static ChatTypeDecoration ReadNetworkChatTypeDecoration(
DataTypes dataTypes,
Queue<byte> packetData)
{
string translationKey = dataTypes.ReadNextString(packetData);
int parameterCount = dataTypes.ReadNextVarInt(packetData);
var parameters = new ChatTypeParameter[parameterCount];
for (int i = 0; i < parameterCount; i++)
parameters[i] = (ChatTypeParameter)dataTypes.ReadNextVarInt(packetData);
_ = dataTypes.ReadNextNbtTag(packetData); // Style
return new ChatTypeDecoration(translationKey, parameters);
}
private static bool TryReadChatTypeDecoration(
Dictionary<string, object>? chatTypeData,
[NotNullWhen(true)] out ChatTypeDecoration? decoration)
{
decoration = null;
if (chatTypeData is null
|| !chatTypeData.TryGetValue("chat", out var chat)
|| chat is not Dictionary<string, object> chatDecoration
|| !chatDecoration.TryGetValue("translation_key", out var translationKey)
|| translationKey is not string translationKeyText
|| !chatDecoration.TryGetValue("parameters", out var parameters)
|| parameters is not object[] parameterList)
return false;
var parsedParameters = new ChatTypeParameter[parameterList.Length];
for (int i = 0; i < parameterList.Length; i++)
{
parsedParameters[i] = parameterList[i] switch
{
"minecraft:chat" => MessageType.CHAT,
"minecraft:emote_command" => MessageType.EMOTE_COMMAND,
"minecraft:msg_command_incoming" => MessageType.MSG_COMMAND_INCOMING,
"minecraft:msg_command_outgoing" => MessageType.MSG_COMMAND_OUTGOING,
"minecraft:say_command" => MessageType.SAY_COMMAND,
"minecraft:team_msg_command_incoming" => MessageType.TEAM_MSG_COMMAND_INCOMING,
"minecraft:team_msg_command_outgoing" => MessageType.TEAM_MSG_COMMAND_OUTGOING,
_ => MessageType.CHAT,
"sender" => ChatTypeParameter.Sender,
"target" => ChatTypeParameter.Target,
"content" => ChatTypeParameter.Content,
_ => ChatTypeParameter.Sender
};
}
ChatId2Type = chatTypeDictionary;
decoration = new ChatTypeDecoration(translationKeyText, parsedParameters);
return true;
}
/// <summary>
@ -147,6 +219,26 @@ namespace MinecraftClient.Protocol.Message
string text;
List<string> usingData = new();
ChatTypeDecoration? decoration = message.chatTypeDecoration;
if (decoration is null)
ChatId2Decoration.TryGetValue(message.chatTypeId, out decoration);
if (decoration is not null)
{
foreach (var parameter in decoration.Parameters)
{
usingData.Add(parameter switch
{
ChatTypeParameter.Sender => sender,
ChatTypeParameter.Target => message.teamName ?? string.Empty,
ChatTypeParameter.Content => content,
_ => string.Empty
});
}
return TranslateString(decoration.TranslationKey, usingData);
}
MessageType chatType;
if (message.chatTypeId == -1)
chatType = MessageType.RAW_MSG;
@ -557,48 +649,47 @@ namespace MinecraftClient.Protocol.Message
RulesInitialized = true;
}
if (TryGetTranslationRule(rulename, out string? rule))
{
int using_idx = 0;
StringBuilder result = new();
for (int i = 0; i < rule.Length; i++)
{
if (rule[i] == '%' && i + 1 < rule.Length)
{
//Using string or int with %s or %d
if (rule[i + 1] == 's' || rule[i + 1] == 'd')
{
if (using_data.Count > using_idx)
{
result.Append(using_data[using_idx]);
using_idx++;
i += 1;
continue;
}
}
if (!TryGetTranslationRule(rulename, out string? rule))
rule = rulename;
//Using specified string or int with %1$s, %2$s...
else if (char.IsDigit(rule[i + 1])
&& i + 3 < rule.Length && rule[i + 2] == '$'
&& (rule[i + 3] == 's' || rule[i + 3] == 'd'))
int using_idx = 0;
StringBuilder result = new();
for (int i = 0; i < rule.Length; i++)
{
if (rule[i] == '%' && i + 1 < rule.Length)
{
//Using string or int with %s or %d
if (rule[i + 1] == 's' || rule[i + 1] == 'd')
{
if (using_data.Count > using_idx)
{
int specified_idx = rule[i + 1] - '1';
if (using_data.Count > specified_idx)
{
result.Append(using_data[specified_idx]);
using_idx++;
i += 3;
continue;
}
result.Append(using_data[using_idx]);
using_idx++;
i += 1;
continue;
}
}
result.Append(rule[i]);
//Using specified string or int with %1$s, %2$s...
else if (char.IsDigit(rule[i + 1])
&& i + 3 < rule.Length && rule[i + 2] == '$'
&& (rule[i + 3] == 's' || rule[i + 3] == 'd'))
{
int specified_idx = rule[i + 1] - '1';
if (using_data.Count > specified_idx)
{
result.Append(using_data[specified_idx]);
using_idx++;
i += 3;
continue;
}
}
}
return result.ToString();
result.Append(rule[i]);
}
else return "[" + rulename + "] " + string.Join(" ", using_data);
return result.ToString();
}
private static bool TryGetTranslationRule(string rulename, [NotNullWhen(true)] out string? result)

View file

@ -8296,5 +8296,9 @@ namespace MinecraftClient {
get { return ResourceManager.GetString("dialog.render.help_hint", resourceCulture); }
}
internal static string protocol_chat_raw_message {
get { return ResourceManager.GetString("protocol.chat.raw_message", resourceCulture); }
}
}
}

View file

@ -3127,4 +3127,7 @@ see item details.</value>
<data name="dialog.render.help_hint" xml:space="preserve">
<value>Use /dialog help for a list of commands.</value>
</data>
<data name="protocol.chat.raw_message" xml:space="preserve">
<value>Raw server chat message: {0}</value>
</data>
</root>