diff --git a/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs b/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs index 9a2a8e4f..6883b6cf 100644 --- a/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs +++ b/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs @@ -5,9 +5,57 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c { internal static class DeclareCommands { - private static int RootIdx; + private const byte NodeTypeMask = 0x03; + private const byte NodeExecutableFlag = 0x04; + private const byte NodeRedirectFlag = 0x08; + private const byte NodeCustomSuggestionsFlag = 0x10; + private const byte NodeRestrictedFlag = 0x20; + + private static readonly Dictionary s_argumentTypeCatalog = CreateArgumentTypeCatalog(); + private static readonly ArgumentTypeLayout s_unknownLegacyArgumentType = new("minecraft:unknown"); + + // Generated from tools/gen_command_argument_registry.py with IDE-only registrations excluded. + private static readonly string[] s_modernArgumentTypes1206 = + [ + "brigadier:bool", "brigadier:float", "brigadier:double", "brigadier:integer", "brigadier:long", "brigadier:string", + "entity", "game_profile", "block_pos", "column_pos", "vec3", "vec2", "block_state", "block_predicate", + "item_stack", "item_predicate", "color", "component", "style", "message", "nbt_compound_tag", "nbt_tag", + "nbt_path", "objective", "objective_criteria", "operation", "particle", "angle", "rotation", + "scoreboard_slot", "score_holder", "swizzle", "team", "item_slot", "item_slots", "resource_location", + "function", "entity_anchor", "int_range", "float_range", "dimension", "gamemode", "time", + "resource_or_tag", "resource_or_tag_key", "resource", "resource_key", "template_mirror", + "template_rotation", "heightmap", "loot_table", "loot_predicate", "loot_modifier", "uuid" + ]; + + private static readonly string[] s_modernArgumentTypes1215 = + [ + "brigadier:bool", "brigadier:float", "brigadier:double", "brigadier:integer", "brigadier:long", "brigadier:string", + "entity", "game_profile", "block_pos", "column_pos", "vec3", "vec2", "block_state", "block_predicate", + "item_stack", "item_predicate", "color", "component", "style", "message", "nbt_compound_tag", "nbt_tag", + "nbt_path", "objective", "objective_criteria", "operation", "particle", "angle", "rotation", + "scoreboard_slot", "score_holder", "swizzle", "team", "item_slot", "item_slots", "resource_location", + "function", "entity_anchor", "int_range", "float_range", "dimension", "gamemode", "time", + "resource_or_tag", "resource_or_tag_key", "resource", "resource_key", "resource_selector", + "template_mirror", "template_rotation", "heightmap", "loot_table", "loot_predicate", "loot_modifier", "uuid" + ]; + + private static readonly string[] s_modernArgumentTypes1216 = + [ + "brigadier:bool", "brigadier:float", "brigadier:double", "brigadier:integer", "brigadier:long", "brigadier:string", + "entity", "game_profile", "block_pos", "column_pos", "vec3", "vec2", "block_state", "block_predicate", + "item_stack", "item_predicate", "color", "hex_color", "component", "style", "message", + "nbt_compound_tag", "nbt_tag", "nbt_path", "objective", "objective_criteria", "operation", "particle", + "angle", "rotation", "scoreboard_slot", "score_holder", "swizzle", "team", "item_slot", "item_slots", + "resource_location", "function", "entity_anchor", "int_range", "float_range", "dimension", "gamemode", + "time", "resource_or_tag", "resource_or_tag_key", "resource", "resource_key", "resource_selector", + "template_mirror", "template_rotation", "heightmap", "loot_table", "loot_predicate", "loot_modifier", + "dialog", "uuid" + ]; + + private static int RootIdx = -1; private static CommandNode[] Nodes = Array.Empty(); private static bool HasLoadedTree; + internal static string? LastReadError { get; private set; } public static bool IsCommandTreeAvailable => HasValidCommandTree(); @@ -16,160 +64,138 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c Reset(); ConsoleIO.OnDeclareMinecraftCommand(Array.Empty()); - // TODO: Fix this - // It crashes in 1.20.6+ , could not figure out why - // it's hard to debug, so I'll just disable it for now - if (protocolVersion > Protocol18Handler.MC_1_20_4_Version) + try { - return; + ReadCommandTree(dataTypes, packetData, protocolVersion); + } + catch (Exception ex) + { + LastReadError = ex.ToString(); + Reset(); } + ConsoleIO.OnDeclareMinecraftCommand(HasLoadedTree ? ExtractRootCommand() : Array.Empty()); + } + + public static List> CollectSignArguments(string command) + { + List> needSigned = new(); + if (!HasValidCommandTree() || string.IsNullOrEmpty(command)) + return needSigned; + + return TryMatchNode(RootIdx, command, 0, needSigned, out List> matchedArguments) + ? matchedArguments + : []; + } + + private static void ReadCommandTree(DataTypes dataTypes, Queue packetData, int protocolVersion) + { int count = dataTypes.ReadNextVarInt(packetData); Nodes = new CommandNode[count]; + for (int i = 0; i < count; ++i) { byte flags = dataTypes.ReadNextByte(packetData); + int[] children = ReadChildIndices(dataTypes, packetData); + int redirectNode = (flags & NodeRedirectFlag) != 0 ? dataTypes.ReadNextVarInt(packetData) : -1; - int childCount = dataTypes.ReadNextVarInt(packetData); - int[] childs = new int[childCount]; - for (int j = 0; j < childCount; ++j) - childs[j] = dataTypes.ReadNextVarInt(packetData); - - int redirectNode = ((flags & 0x08) == 0x08) ? dataTypes.ReadNextVarInt(packetData) : -1; - - string? name = ((flags & 0x03) == 1 || (flags & 0x03) == 2) ? dataTypes.ReadNextString(packetData) : null; - - int parserId = ((flags & 0x03) == 2) ? dataTypes.ReadNextVarInt(packetData) : -1; - Parser? parser = null; - if ((flags & 0x03) == 2) + CommandNodeKind nodeKind = (CommandNodeKind)(flags & NodeTypeMask); + CommandNode node = nodeKind switch { - if (protocolVersion <= Protocol18Handler.MC_1_19_2_Version) - parser = parserId switch - { - 1 => new ParserFloat(dataTypes, packetData), - 2 => new ParserDouble(dataTypes, packetData), - 3 => new ParserInteger(dataTypes, packetData), - 4 => new ParserLong(dataTypes, packetData), - 5 => new ParserString(dataTypes, packetData), - 6 => new ParserEntity(dataTypes, packetData), - 8 => new ParserBlockPos(dataTypes, packetData), - 9 => new ParserColumnPos(dataTypes, packetData), - 10 => new ParserVec3(dataTypes, packetData), - 11 => new ParserVec2(dataTypes, packetData), - 18 => new ParserMessage(dataTypes, packetData), - 27 => new ParserRotation(dataTypes, packetData), - 29 => new ParserScoreHolder(dataTypes, packetData), - 43 => new ParserResourceOrTag(dataTypes, packetData), - 44 => new ParserResource(dataTypes, packetData), - 50 => new ParserForgeEnum(dataTypes, packetData), - _ => new ParserEmpty(dataTypes, packetData), - }; - else if (protocolVersion <= Protocol18Handler.MC_1_19_3_Version) // 1.19.3 - parser = parserId switch - { - 1 => new ParserFloat(dataTypes, packetData), - 2 => new ParserDouble(dataTypes, packetData), - 3 => new ParserInteger(dataTypes, packetData), - 4 => new ParserLong(dataTypes, packetData), - 5 => new ParserString(dataTypes, packetData), - 6 => new ParserEntity(dataTypes, packetData), - 8 => new ParserBlockPos(dataTypes, packetData), - 9 => new ParserColumnPos(dataTypes, packetData), - 10 => new ParserVec3(dataTypes, packetData), - 11 => new ParserVec2(dataTypes, packetData), - 18 => new ParserMessage(dataTypes, packetData), - 27 => new ParserRotation(dataTypes, packetData), - 29 => new ParserScoreHolder(dataTypes, packetData), - 41 => new ParserResourceOrTag(dataTypes, packetData), - 42 => new ParserResourceOrTag(dataTypes, packetData), - 43 => new ParserResource(dataTypes, packetData), - 44 => new ParserResource(dataTypes, packetData), - 50 => new ParserForgeEnum(dataTypes, packetData), - _ => new ParserEmpty(dataTypes, packetData), - }; - else if (protocolVersion <= Protocol18Handler.MC_1_20_2_Version)// 1.19.4 - 1.20.2 - parser = parserId switch - { - 1 => new ParserFloat(dataTypes, packetData), - 2 => new ParserDouble(dataTypes, packetData), - 3 => new ParserInteger(dataTypes, packetData), - 4 => new ParserLong(dataTypes, packetData), - 5 => new ParserString(dataTypes, packetData), - 6 => new ParserEntity(dataTypes, packetData), - 8 => new ParserBlockPos(dataTypes, packetData), - 9 => new ParserColumnPos(dataTypes, packetData), - 10 => new ParserVec3(dataTypes, packetData), - 11 => new ParserVec2(dataTypes, packetData), - 18 => new ParserMessage(dataTypes, packetData), - 27 => new ParserRotation(dataTypes, packetData), - 29 => new ParserScoreHolder(dataTypes, packetData), - 40 => new ParserTime(dataTypes, packetData), - 41 => new ParserResourceOrTag(dataTypes, packetData), - 42 => new ParserResourceOrTag(dataTypes, packetData), - 43 => new ParserResource(dataTypes, packetData), - 44 => new ParserResource(dataTypes, packetData), - 50 => protocolVersion == Protocol18Handler.MC_1_19_4_Version ? - new ParserForgeEnum(dataTypes, packetData) : - new ParserEmpty(dataTypes, packetData), - 51 => (protocolVersion >= Protocol18Handler.MC_1_20_Version && - protocolVersion <= Protocol18Handler.MC_1_20_2_Version) ? // 1.20 - 1.20.2 - new ParserForgeEnum(dataTypes, packetData) : - new ParserEmpty(dataTypes, packetData), - _ => new ParserEmpty(dataTypes, packetData), - }; - else if (protocolVersion is > Protocol18Handler.MC_1_20_2_Version and < Protocol18Handler.MC_1_20_6_Version) - // 1.20.3 - 1.20.4 - parser = parserId switch - { - 1 => new ParserFloat(dataTypes, packetData), - 2 => new ParserDouble(dataTypes, packetData), - 3 => new ParserInteger(dataTypes, packetData), - 4 => new ParserLong(dataTypes, packetData), - 5 => new ParserString(dataTypes, packetData), - 6 => new ParserEntity(dataTypes, packetData), - 8 => new ParserBlockPos(dataTypes, packetData), - 9 => new ParserColumnPos(dataTypes, packetData), - 10 => new ParserVec3(dataTypes, packetData), - 11 => new ParserVec2(dataTypes, packetData), - 18 => new ParserMessage(dataTypes, packetData), - 27 => new ParserRotation(dataTypes, packetData), - 30 => new ParserScoreHolder(dataTypes, packetData), - 41 => new ParserTime(dataTypes, packetData), - 42 => new ParserResourceOrTag(dataTypes, packetData), - 43 => new ParserResourceOrTag(dataTypes, packetData), - 44 => new ParserResource(dataTypes, packetData), - 45 => new ParserResource(dataTypes, packetData), - 52 => new ParserForgeEnum(dataTypes, packetData), - _ => new ParserEmpty(dataTypes, packetData), - }; - else // 1.20.6+ - parser = parserId switch - { - 1 => new ParserFloat(dataTypes, packetData), - 2 => new ParserDouble(dataTypes, packetData), - 3 => new ParserInteger(dataTypes, packetData), - 4 => new ParserLong(dataTypes, packetData), - 5 => new ParserString(dataTypes, packetData), - 6 => new ParserEntity(dataTypes, packetData), - 30 => new ParserScoreHolder(dataTypes, packetData), - 41 => new ParserTime(dataTypes, packetData), - 42 => new ParserResourceOrTag(dataTypes, packetData), - 43 => new ParserResourceOrTag(dataTypes, packetData), - 44 => new ParserResource(dataTypes, packetData), - 45 => new ParserResource(dataTypes, packetData), - 52 => new ParserForgeEnum(dataTypes, packetData), - _ => new ParserEmpty(dataTypes, packetData), - }; - } + CommandNodeKind.Root => new(flags, children, redirectNode), + CommandNodeKind.Literal => new(flags, children, redirectNode, dataTypes.ReadNextString(packetData)), + CommandNodeKind.Argument => ReadArgumentNode(dataTypes, packetData, protocolVersion, flags, children, redirectNode), + _ => throw new InvalidOperationException($"Unsupported DeclareCommands node type {(byte)nodeKind}.") + }; - string? suggestionsType = ((flags & 0x10) == 0x10) ? dataTypes.ReadNextString(packetData) : null; - - Nodes[i] = new(flags, childs, redirectNode, name, parser, suggestionsType, parserId); + Nodes[i] = node; } + RootIdx = dataTypes.ReadNextVarInt(packetData); HasLoadedTree = IsValidNodeIndex(RootIdx); + } - ConsoleIO.OnDeclareMinecraftCommand(HasLoadedTree ? ExtractRootCommand() : Array.Empty()); + private static CommandNode ReadArgumentNode( + DataTypes dataTypes, + Queue packetData, + int protocolVersion, + byte flags, + int[] children, + int redirectNode) + { + string name = dataTypes.ReadNextString(packetData); + int parserId = dataTypes.ReadNextVarInt(packetData); + + if (!TryResolveArgumentTypeLayout(protocolVersion, parserId, out ArgumentTypeLayout layout)) + throw new InvalidOperationException($"Unsupported DeclareCommands argument type id {parserId} for protocol {protocolVersion}."); + + CommandArgumentDescriptor descriptor = ReadArgumentDescriptor(dataTypes, packetData, layout); + string? suggestionsType = (flags & NodeCustomSuggestionsFlag) != 0 ? dataTypes.ReadNextString(packetData) : null; + + return new(flags, children, redirectNode, name, descriptor, suggestionsType, parserId); + } + + private static int[] ReadChildIndices(DataTypes dataTypes, Queue packetData) + { + int childCount = dataTypes.ReadNextVarInt(packetData); + int[] children = new int[childCount]; + + for (int i = 0; i < childCount; ++i) + children[i] = dataTypes.ReadNextVarInt(packetData); + + return children; + } + + private static CommandArgumentDescriptor ReadArgumentDescriptor(DataTypes dataTypes, Queue packetData, ArgumentTypeLayout layout) + { + switch (layout.PayloadKind) + { + case ArgumentPayloadKind.None: + return layout.CreateDescriptor(); + case ArgumentPayloadKind.BrigadierFloat: + ReadNumberBounds(dataTypes, packetData, static (types, data) => types.ReadNextFloat(data)); + return layout.CreateDescriptor(); + case ArgumentPayloadKind.BrigadierDouble: + ReadNumberBounds(dataTypes, packetData, static (types, data) => types.ReadNextDouble(data)); + return layout.CreateDescriptor(); + case ArgumentPayloadKind.BrigadierInteger: + ReadNumberBounds(dataTypes, packetData, static (types, data) => types.ReadNextInt(data)); + return layout.CreateDescriptor(); + case ArgumentPayloadKind.BrigadierLong: + ReadNumberBounds(dataTypes, packetData, static (types, data) => types.ReadNextLong(data)); + return layout.CreateDescriptor(); + case ArgumentPayloadKind.BrigadierString: + ArgumentConsumption consumption = dataTypes.ReadNextVarInt(packetData) switch + { + 0 => ArgumentConsumption.SingleToken, + 1 => ArgumentConsumption.QuotedStringOrWord, + 2 => ArgumentConsumption.GreedyTail, + int stringType => throw new InvalidOperationException($"Unsupported brigadier:string type {stringType}.") + }; + return layout.CreateDescriptor(consumption); + case ArgumentPayloadKind.Entity: + case ArgumentPayloadKind.ScoreHolder: + dataTypes.ReadNextByte(packetData); + return layout.CreateDescriptor(); + case ArgumentPayloadKind.Time: + dataTypes.ReadNextInt(packetData); + return layout.CreateDescriptor(); + case ArgumentPayloadKind.RegistryKey: + case ArgumentPayloadKind.ForgeEnum: + dataTypes.ReadNextString(packetData); + return layout.CreateDescriptor(); + default: + throw new InvalidOperationException($"Unsupported DeclareCommands payload kind {layout.PayloadKind}."); + } + } + + private static void ReadNumberBounds(DataTypes dataTypes, Queue packetData, Func, TValue> readValue) + { + byte flags = dataTypes.ReadNextByte(packetData); + if ((flags & 0x01) != 0) + _ = readValue(dataTypes, packetData); + if ((flags & 0x02) != 0) + _ = readValue(dataTypes, packetData); } private static string[] ExtractRootCommand() @@ -179,74 +205,398 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c List commands = new(); CommandNode root = Nodes[RootIdx]; - foreach (var child in root.Clildren) + + foreach (int child in root.Children) { if (!IsValidNodeIndex(child)) continue; string? childName = Nodes[child].Name; - if (childName != null) + if (!string.IsNullOrEmpty(childName)) commands.Add(childName); } + return commands.ToArray(); } - public static List> CollectSignArguments(string command) + private static bool TryMatchNode( + int nodeIdx, + string command, + int position, + List> signedArguments, + out List> matchedArguments) { - List> needSigned = new(); - if (!HasValidCommandTree()) - return needSigned; + matchedArguments = signedArguments; + if (!IsValidNodeIndex(nodeIdx)) + return false; - CollectSignArguments(RootIdx, command, needSigned); - return needSigned; + CommandNode node = Nodes[nodeIdx]; + if (!TryConsumeNode(node, command, position, out int nextPosition, out Tuple? signedCapture)) + return false; + + List> currentArguments = signedArguments; + if (signedCapture != null) + { + currentArguments = new List>(signedArguments.Count + 1); + currentArguments.AddRange(signedArguments); + currentArguments.Add(signedCapture); + } + + int traversalNodeIdx = ResolveRedirect(nodeIdx); + bool canStopHere = node.IsExecutable || + (traversalNodeIdx != nodeIdx && IsValidNodeIndex(traversalNodeIdx) && Nodes[traversalNodeIdx].IsExecutable); + + if (nextPosition == command.Length) + { + if (canStopHere) + { + matchedArguments = currentArguments; + return true; + } + + return false; + } + + int childPosition = nextPosition; + if (node.Kind != CommandNodeKind.Root) + { + if (command[childPosition] != ' ') + return false; + childPosition++; + } + + return TryMatchChildren(traversalNodeIdx, command, childPosition, currentArguments, out matchedArguments); } - private static void CollectSignArguments(int NodeIdx, string command, List> arguments) + private static bool TryMatchChildren( + int nodeIdx, + string command, + int position, + List> signedArguments, + out List> matchedArguments) { - if (!IsValidNodeIndex(NodeIdx)) - return; + matchedArguments = signedArguments; + if (!IsValidNodeIndex(nodeIdx)) + return false; - CommandNode node = Nodes[NodeIdx]; - string last_arg = command; - switch (node.Flags & 0x03) + int[] children = Nodes[nodeIdx].Children; + + for (int pass = 0; pass < 2; ++pass) { - case 0: // root - break; - case 1: // literal - { - string[] arg = command.Split(' ', 2, StringSplitOptions.None); - if (!(arg.Length == 2 && node.Name! == arg[0])) - return; - last_arg = arg[1]; - } - break; - case 2: // argument - { - int argCnt = (node.Paser == null) ? 1 : node.Paser.GetArgCnt(); - string[] arg = command.Split(' ', argCnt + 1, StringSplitOptions.None); - if ((node.Flags & 0x04) > 0) - { - if (node.Paser != null && node.Paser.GetName() == "minecraft:message") - arguments.Add(new(node.Name!, command)); - } - if (!(arg.Length == argCnt + 1)) - return; - last_arg = arg[^1]; - } - break; + foreach (int childIdx in children) + { + if (!IsValidNodeIndex(childIdx)) + continue; + + bool isLiteral = Nodes[childIdx].Kind == CommandNodeKind.Literal; + if ((pass == 0 && !isLiteral) || (pass == 1 && isLiteral)) + continue; + + if (TryMatchNode(childIdx, command, position, signedArguments, out matchedArguments)) + return true; + } + } + + return false; + } + + private static bool TryConsumeNode( + CommandNode node, + string command, + int position, + out int nextPosition, + out Tuple? signedCapture) + { + nextPosition = position; + signedCapture = null; + + switch (node.Kind) + { + case CommandNodeKind.Root: + return true; + case CommandNodeKind.Literal: + return TryConsumeLiteral(command, position, node.Name!, out nextPosition); + case CommandNodeKind.Argument: + if (node.Argument == null || !TryConsumeArgument(command, position, node.Argument.Value, out nextPosition)) + return false; + + if (node.Argument.Value.IsSigned) + signedCapture = new Tuple(node.Name!, command[position..nextPosition]); + + return true; default: - break; + return false; } + } - while (Nodes[NodeIdx].RedirectNode >= 0) + private static bool TryConsumeLiteral(string command, int position, string literal, out int nextPosition) + { + nextPosition = position; + if (position + literal.Length > command.Length) + return false; + + if (string.CompareOrdinal(command, position, literal, 0, literal.Length) != 0) + return false; + + nextPosition = position + literal.Length; + return nextPosition == command.Length || command[nextPosition] == ' '; + } + + private static bool TryConsumeArgument(string command, int position, CommandArgumentDescriptor descriptor, out int nextPosition) + { + nextPosition = position; + + return descriptor.Consumption switch { - NodeIdx = Nodes[NodeIdx].RedirectNode; - if (!IsValidNodeIndex(NodeIdx)) - return; + ArgumentConsumption.SingleToken => TryConsumeSingleToken(command, position, out nextPosition), + ArgumentConsumption.QuotedStringOrWord => TryConsumeQuotedStringOrWord(command, position, out nextPosition), + ArgumentConsumption.GreedyTail => TryConsumeGreedyTail(command, position, out nextPosition), + ArgumentConsumption.FixedTokenCount => TryConsumeFixedTokenCount(command, position, descriptor.TokenCount, out nextPosition), + _ => false + }; + } + + private static bool TryConsumeSingleToken(string command, int position, out int nextPosition) + { + nextPosition = position; + if (position >= command.Length) + return false; + + int cursor = position; + while (cursor < command.Length && command[cursor] != ' ') + cursor++; + + nextPosition = cursor; + return cursor > position; + } + + private static bool TryConsumeQuotedStringOrWord(string command, int position, out int nextPosition) + { + nextPosition = position; + if (position >= command.Length) + return false; + + if (command[position] != '"') + return TryConsumeSingleToken(command, position, out nextPosition); + + bool escaped = false; + for (int cursor = position + 1; cursor < command.Length; ++cursor) + { + char current = command[cursor]; + if (escaped) + { + escaped = false; + continue; + } + + if (current == '\\') + { + escaped = true; + continue; + } + + if (current == '"') + { + nextPosition = cursor + 1; + return nextPosition == command.Length || command[nextPosition] == ' '; + } } - foreach (int childIdx in Nodes[NodeIdx].Clildren) - CollectSignArguments(childIdx, last_arg, arguments); + return false; + } + + private static bool TryConsumeGreedyTail(string command, int position, out int nextPosition) + { + nextPosition = command.Length; + return position < command.Length; + } + + private static bool TryConsumeFixedTokenCount(string command, int position, int tokenCount, out int nextPosition) + { + nextPosition = position; + int cursor = position; + + for (int i = 0; i < tokenCount; ++i) + { + if (!TryConsumeSingleToken(command, cursor, out int tokenEnd)) + return false; + + cursor = tokenEnd; + if (i < tokenCount - 1) + { + if (cursor >= command.Length || command[cursor] != ' ') + return false; + + cursor++; + } + } + + nextPosition = cursor; + return true; + } + + private static int ResolveRedirect(int nodeIdx) + { + if (!IsValidNodeIndex(nodeIdx)) + return -1; + + HashSet visited = new(); + int current = nodeIdx; + + while (IsValidNodeIndex(current) && Nodes[current].RedirectNode >= 0) + { + if (!visited.Add(current)) + return current; + + current = Nodes[current].RedirectNode; + } + + return IsValidNodeIndex(current) ? current : -1; + } + + private static bool TryResolveArgumentTypeLayout(int protocolVersion, int parserId, out ArgumentTypeLayout layout) + { + return protocolVersion >= Protocol18Handler.MC_1_20_6_Version + ? TryResolveModernArgumentTypeLayout(protocolVersion, parserId, out layout) + : TryResolveLegacyArgumentTypeLayout(protocolVersion, parserId, out layout); + } + + private static bool TryResolveModernArgumentTypeLayout(int protocolVersion, int parserId, out ArgumentTypeLayout layout) + { + string[] registry = protocolVersion switch + { + >= Protocol18Handler.MC_1_21_6_Version => s_modernArgumentTypes1216, + >= Protocol18Handler.MC_1_21_5_Version => s_modernArgumentTypes1215, + _ => s_modernArgumentTypes1206 + }; + + if (parserId < 0 || parserId >= registry.Length) + { + layout = default; + return false; + } + + return s_argumentTypeCatalog.TryGetValue(ToCanonicalArgumentTypeName(registry[parserId]), out layout); + } + + private static bool TryResolveLegacyArgumentTypeLayout(int protocolVersion, int parserId, out ArgumentTypeLayout layout) + { + string? name; + + if (protocolVersion <= Protocol18Handler.MC_1_19_2_Version) + { + name = parserId switch + { + 1 => "brigadier:float", + 2 => "brigadier:double", + 3 => "brigadier:integer", + 4 => "brigadier:long", + 5 => "brigadier:string", + 6 => "minecraft:entity", + 8 => "minecraft:block_pos", + 9 => "minecraft:column_pos", + 10 => "minecraft:vec3", + 11 => "minecraft:vec2", + 18 => "minecraft:message", + 27 => "minecraft:rotation", + 29 => "minecraft:score_holder", + 43 => "minecraft:resource_or_tag", + 44 => "minecraft:resource", + 50 => "forge:enum", + _ => null + }; + } + else if (protocolVersion <= Protocol18Handler.MC_1_19_3_Version) + { + name = parserId switch + { + 1 => "brigadier:float", + 2 => "brigadier:double", + 3 => "brigadier:integer", + 4 => "brigadier:long", + 5 => "brigadier:string", + 6 => "minecraft:entity", + 8 => "minecraft:block_pos", + 9 => "minecraft:column_pos", + 10 => "minecraft:vec3", + 11 => "minecraft:vec2", + 18 => "minecraft:message", + 27 => "minecraft:rotation", + 29 => "minecraft:score_holder", + 41 => "minecraft:resource_or_tag", + 42 => "minecraft:resource_or_tag_key", + 43 => "minecraft:resource", + 44 => "minecraft:resource_key", + 50 => "forge:enum", + _ => null + }; + } + else if (protocolVersion <= Protocol18Handler.MC_1_20_2_Version) + { + name = parserId switch + { + 1 => "brigadier:float", + 2 => "brigadier:double", + 3 => "brigadier:integer", + 4 => "brigadier:long", + 5 => "brigadier:string", + 6 => "minecraft:entity", + 8 => "minecraft:block_pos", + 9 => "minecraft:column_pos", + 10 => "minecraft:vec3", + 11 => "minecraft:vec2", + 18 => "minecraft:message", + 27 => "minecraft:rotation", + 29 => "minecraft:score_holder", + 40 => "minecraft:time", + 41 => "minecraft:resource_or_tag", + 42 => "minecraft:resource_or_tag_key", + 43 => "minecraft:resource", + 44 => "minecraft:resource_key", + 50 when protocolVersion == Protocol18Handler.MC_1_19_4_Version => "forge:enum", + 51 when protocolVersion is >= Protocol18Handler.MC_1_20_Version and <= Protocol18Handler.MC_1_20_2_Version => "forge:enum", + _ => null + }; + } + else + { + name = parserId switch + { + 1 => "brigadier:float", + 2 => "brigadier:double", + 3 => "brigadier:integer", + 4 => "brigadier:long", + 5 => "brigadier:string", + 6 => "minecraft:entity", + 8 => "minecraft:block_pos", + 9 => "minecraft:column_pos", + 10 => "minecraft:vec3", + 11 => "minecraft:vec2", + 18 or 19 => "minecraft:message", + 27 => "minecraft:rotation", + 30 => "minecraft:score_holder", + 41 => "minecraft:time", + 42 => "minecraft:resource_or_tag", + 43 => "minecraft:resource_or_tag_key", + 44 => "minecraft:resource", + 45 => "minecraft:resource_key", + 52 => "forge:enum", + _ => null + }; + } + + if (name == null) + { + layout = s_unknownLegacyArgumentType; + return true; + } + + return s_argumentTypeCatalog.TryGetValue(name, out layout); + } + + private static string ToCanonicalArgumentTypeName(string rawName) + { + return rawName.Contains(':', StringComparison.Ordinal) ? rawName : "minecraft:" + rawName; } private static void Reset() @@ -254,6 +604,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c RootIdx = -1; Nodes = Array.Empty(); HasLoadedTree = false; + LastReadError = null; } private static bool HasValidCommandTree() @@ -266,508 +617,197 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c return nodeIdx >= 0 && nodeIdx < Nodes.Length; } - internal class CommandNode + private static Dictionary CreateArgumentTypeCatalog() { - public byte Flags; - public int[] Clildren; - public int RedirectNode; - public string? Name; - public Parser? Paser; - public string? SuggestionsType; - public int ParserId; // Added for easy debug + Dictionary catalog = new(StringComparer.Ordinal); - - public CommandNode(byte Flags, - int[] Clildren, - int RedirectNode = -1, - string? Name = null, - Parser? Paser = null, - string? SuggestionsType = null, - int parserId = -1) + static void Add( + Dictionary items, + string name, + ArgumentPayloadKind payloadKind = ArgumentPayloadKind.None, + ArgumentConsumption consumption = ArgumentConsumption.SingleToken, + int tokenCount = 1, + bool isSigned = false) { - this.Flags = Flags; - this.Clildren = Clildren; - this.RedirectNode = RedirectNode; - this.Name = Name; - this.Paser = Paser; - this.SuggestionsType = SuggestionsType; + items[name] = new ArgumentTypeLayout(name, payloadKind, consumption, tokenCount, isSigned); + } + + static void AddFixedTokens(Dictionary items, string name, int tokenCount) + { + Add(items, name, consumption: ArgumentConsumption.FixedTokenCount, tokenCount: tokenCount); + } + + Add(catalog, "brigadier:bool"); + Add(catalog, "brigadier:float", ArgumentPayloadKind.BrigadierFloat); + Add(catalog, "brigadier:double", ArgumentPayloadKind.BrigadierDouble); + Add(catalog, "brigadier:integer", ArgumentPayloadKind.BrigadierInteger); + Add(catalog, "brigadier:long", ArgumentPayloadKind.BrigadierLong); + Add(catalog, "brigadier:string", ArgumentPayloadKind.BrigadierString); + Add(catalog, "minecraft:entity", ArgumentPayloadKind.Entity); + Add(catalog, "minecraft:game_profile"); + AddFixedTokens(catalog, "minecraft:block_pos", 3); + AddFixedTokens(catalog, "minecraft:column_pos", 2); + AddFixedTokens(catalog, "minecraft:vec3", 3); + AddFixedTokens(catalog, "minecraft:vec2", 2); + Add(catalog, "minecraft:block_state"); + Add(catalog, "minecraft:block_predicate"); + Add(catalog, "minecraft:item_stack"); + Add(catalog, "minecraft:item_predicate"); + Add(catalog, "minecraft:color"); + Add(catalog, "minecraft:hex_color"); + Add(catalog, "minecraft:component"); + Add(catalog, "minecraft:style"); + Add(catalog, "minecraft:message", consumption: ArgumentConsumption.GreedyTail, isSigned: true); + Add(catalog, "minecraft:nbt_compound_tag"); + Add(catalog, "minecraft:nbt_tag"); + Add(catalog, "minecraft:nbt_path"); + Add(catalog, "minecraft:objective"); + Add(catalog, "minecraft:objective_criteria"); + Add(catalog, "minecraft:operation"); + Add(catalog, "minecraft:particle"); + Add(catalog, "minecraft:angle"); + AddFixedTokens(catalog, "minecraft:rotation", 2); + Add(catalog, "minecraft:scoreboard_slot"); + Add(catalog, "minecraft:score_holder", ArgumentPayloadKind.ScoreHolder); + Add(catalog, "minecraft:swizzle"); + Add(catalog, "minecraft:team"); + Add(catalog, "minecraft:item_slot"); + Add(catalog, "minecraft:item_slots"); + Add(catalog, "minecraft:resource_location"); + Add(catalog, "minecraft:function"); + Add(catalog, "minecraft:entity_anchor"); + Add(catalog, "minecraft:int_range"); + Add(catalog, "minecraft:float_range"); + Add(catalog, "minecraft:dimension"); + Add(catalog, "minecraft:gamemode"); + Add(catalog, "minecraft:time", ArgumentPayloadKind.Time); + Add(catalog, "minecraft:resource_or_tag", ArgumentPayloadKind.RegistryKey); + Add(catalog, "minecraft:resource_or_tag_key", ArgumentPayloadKind.RegistryKey); + Add(catalog, "minecraft:resource", ArgumentPayloadKind.RegistryKey); + Add(catalog, "minecraft:resource_key", ArgumentPayloadKind.RegistryKey); + Add(catalog, "minecraft:resource_selector", ArgumentPayloadKind.RegistryKey); + Add(catalog, "minecraft:template_mirror"); + Add(catalog, "minecraft:template_rotation"); + Add(catalog, "minecraft:heightmap"); + Add(catalog, "minecraft:loot_table"); + Add(catalog, "minecraft:loot_predicate"); + Add(catalog, "minecraft:loot_modifier"); + Add(catalog, "minecraft:dialog"); + Add(catalog, "minecraft:uuid"); + Add(catalog, "forge:enum", ArgumentPayloadKind.ForgeEnum); + + return catalog; + } + + private enum CommandNodeKind : byte + { + Root = 0, + Literal = 1, + Argument = 2 + } + + private enum ArgumentConsumption + { + SingleToken, + QuotedStringOrWord, + GreedyTail, + FixedTokenCount + } + + private enum ArgumentPayloadKind + { + None, + BrigadierFloat, + BrigadierDouble, + BrigadierInteger, + BrigadierLong, + BrigadierString, + Entity, + ScoreHolder, + Time, + RegistryKey, + ForgeEnum + } + + private sealed class CommandNode + { + public byte Flags { get; } + public int[] Children { get; } + public int RedirectNode { get; } + public string? Name { get; } + public CommandArgumentDescriptor? Argument { get; } + public string? SuggestionsType { get; } + public int ParserId { get; } + + public CommandNodeKind Kind => (CommandNodeKind)(Flags & NodeTypeMask); + public bool IsExecutable => (Flags & NodeExecutableFlag) != 0; + public bool IsRestricted => (Flags & NodeRestrictedFlag) != 0; + + public CommandNode( + byte flags, + int[] children, + int redirectNode = -1, + string? name = null, + CommandArgumentDescriptor? argument = null, + string? suggestionsType = null, + int parserId = -1) + { + Flags = flags; + Children = children; + RedirectNode = redirectNode; + Name = name; + Argument = argument; + SuggestionsType = suggestionsType; ParserId = parserId; } } - internal abstract class Parser + private readonly struct CommandArgumentDescriptor { - public abstract string GetName(); + public string Name { get; } + public ArgumentConsumption Consumption { get; } + public int TokenCount { get; } + public bool IsSigned { get; } - public abstract int GetArgCnt(); - - public abstract bool Check(string text); - } - - internal class ParserEmpty : Parser - { - - public ParserEmpty(DataTypes dataTypes, Queue packetData) { } - - public override bool Check(string text) + public CommandArgumentDescriptor(string name, ArgumentConsumption consumption, int tokenCount = 1, bool isSigned = false) { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return ""; + Name = name; + Consumption = consumption; + TokenCount = tokenCount; + IsSigned = isSigned; } } - internal class ParserFloat : Parser + private readonly struct ArgumentTypeLayout { - private byte Flags; - private float Min = float.MinValue, Max = float.MaxValue; + public string Name { get; } + public ArgumentPayloadKind PayloadKind { get; } + public ArgumentConsumption Consumption { get; } + public int TokenCount { get; } + public bool IsSigned { get; } - public ParserFloat(DataTypes dataTypes, Queue packetData) + public ArgumentTypeLayout( + string name, + ArgumentPayloadKind payloadKind = ArgumentPayloadKind.None, + ArgumentConsumption consumption = ArgumentConsumption.SingleToken, + int tokenCount = 1, + bool isSigned = false) { - Flags = dataTypes.ReadNextByte(packetData); - if ((Flags & 0x01) > 0) - Min = dataTypes.ReadNextFloat(packetData); - if ((Flags & 0x02) > 0) - Max = dataTypes.ReadNextFloat(packetData); + Name = name; + PayloadKind = payloadKind; + Consumption = consumption; + TokenCount = tokenCount; + IsSigned = isSigned; } - public override bool Check(string text) + public CommandArgumentDescriptor CreateDescriptor() { - return true; + return new CommandArgumentDescriptor(Name, Consumption, TokenCount, IsSigned); } - public override int GetArgCnt() + public CommandArgumentDescriptor CreateDescriptor(ArgumentConsumption consumption) { - return 1; - } - - public override string GetName() - { - return "brigadier:float"; - } - } - - internal class ParserDouble : Parser - { - private byte Flags; - private double Min = double.MinValue, Max = double.MaxValue; - - public ParserDouble(DataTypes dataTypes, Queue packetData) - { - Flags = dataTypes.ReadNextByte(packetData); - if ((Flags & 0x01) > 0) - Min = dataTypes.ReadNextDouble(packetData); - if ((Flags & 0x02) > 0) - Max = dataTypes.ReadNextDouble(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "brigadier:double"; - } - } - - internal class ParserInteger : Parser - { - private byte Flags; - private int Min = int.MinValue, Max = int.MaxValue; - - public ParserInteger(DataTypes dataTypes, Queue packetData) - { - Flags = dataTypes.ReadNextByte(packetData); - if ((Flags & 0x01) > 0) - Min = dataTypes.ReadNextInt(packetData); - if ((Flags & 0x02) > 0) - Max = dataTypes.ReadNextInt(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "brigadier:integer"; - } - } - - internal class ParserLong : Parser - { - private byte Flags; - private long Min = long.MinValue, Max = long.MaxValue; - - public ParserLong(DataTypes dataTypes, Queue packetData) - { - Flags = dataTypes.ReadNextByte(packetData); - if ((Flags & 0x01) > 0) - Min = dataTypes.ReadNextLong(packetData); - if ((Flags & 0x02) > 0) - Max = dataTypes.ReadNextLong(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "brigadier:long"; - } - } - - internal class ParserString : Parser - { - private StringType Type; - - private enum StringType { SINGLE_WORD, QUOTABLE_PHRASE, GREEDY_PHRASE }; - - public ParserString(DataTypes dataTypes, Queue packetData) - { - Type = (StringType)dataTypes.ReadNextVarInt(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "brigadier:string"; - } - } - - internal class ParserEntity : Parser - { - private byte Flags; - - public ParserEntity(DataTypes dataTypes, Queue packetData) - { - Flags = dataTypes.ReadNextByte(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "minecraft:entity"; - } - } - - internal class ParserBlockPos : Parser - { - - public ParserBlockPos(DataTypes dataTypes, Queue packetData) { } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 3; - } - - public override string GetName() - { - return "minecraft:block_pos"; - } - } - - internal class ParserColumnPos : Parser - { - - public ParserColumnPos(DataTypes dataTypes, Queue packetData) { } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 3; - } - - public override string GetName() - { - return "minecraft:column_pos"; - } - } - - internal class ParserVec3 : Parser - { - - public ParserVec3(DataTypes dataTypes, Queue packetData) { } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 3; - } - - public override string GetName() - { - return "minecraft:vec3"; - } - } - - internal class ParserVec2 : Parser - { - - public ParserVec2(DataTypes dataTypes, Queue packetData) { } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 2; - } - - public override string GetName() - { - return "minecraft:vec2"; - } - } - - internal class ParserRotation : Parser - { - - public ParserRotation(DataTypes dataTypes, Queue packetData) { } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 2; - } - - public override string GetName() - { - return "minecraft:rotation"; - } - } - - internal class ParserMessage : Parser - { - public ParserMessage(DataTypes dataTypes, Queue packetData) { } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "minecraft:message"; - } - } - - internal class ParserScoreHolder : Parser - { - private byte Flags; - - public ParserScoreHolder(DataTypes dataTypes, Queue packetData) - { - Flags = dataTypes.ReadNextByte(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "minecraft:score_holder"; - } - } - - internal class ParserRange : Parser - { - private bool Decimals; - - public ParserRange(DataTypes dataTypes, Queue packetData) - { - Decimals = dataTypes.ReadNextBool(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "minecraft:range"; - } - } - - internal class ParserResourceOrTag : Parser - { - private string Registry; - - public ParserResourceOrTag(DataTypes dataTypes, Queue packetData) - { - Registry = dataTypes.ReadNextString(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "minecraft:resource_or_tag"; - } - } - - internal class ParserResource : Parser - { - private string Registry; - - public ParserResource(DataTypes dataTypes, Queue packetData) - { - Registry = dataTypes.ReadNextString(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "minecraft:resource"; - } - } - - /// - /// Undocumented parser type for 1.19.4+ - /// - internal class ParserTime : Parser - { - public ParserTime(DataTypes dataTypes, Queue packetData) - { - dataTypes.ReadNextInt(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "minecraft:time"; - } - } - - internal class ParserForgeEnum : Parser - { - public ParserForgeEnum(DataTypes dataTypes, Queue packetData) - { - dataTypes.ReadNextString(packetData); - } - - public override bool Check(string text) - { - return true; - } - - public override int GetArgCnt() - { - return 1; - } - - public override string GetName() - { - return "forge:enum"; + return new CommandArgumentDescriptor(Name, consumption, TokenCount, IsSigned); } } } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 98d7aec7..09d4f6bc 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -3702,8 +3702,13 @@ namespace MinecraftClient.Protocol.Handlers try { List>? needSigned = null; + bool canSignCommand = protocolVersion >= MC_1_19_Version && + isOnlineMode && + playerKeyPair != null && + Config.Signature.LoginWithSecureProfile && + Config.Signature.SignMessageInCommand; - if (protocolVersion >= MC_1_19_Version && Config.Signature is { LoginWithSecureProfile: true, SignMessageInCommand: true }) + if (canSignCommand) { if (DeclareCommands.IsCommandTreeAvailable) { diff --git a/tools/README.md b/tools/README.md index 17df9676..f8ba7226 100644 --- a/tools/README.md +++ b/tools/README.md @@ -102,6 +102,14 @@ Reads `EntityDataSerializers.java` static block registration order. Maps Java fi 2. MCC's `EntityMetaDataType.cs` enum 3. `DataTypes.cs` ReadNextMetadata() read logic +## gen_command_argument_registry.py — Generate DeclareCommands registry arrays + +```bash +python3 tools/gen_command_argument_registry.py 1.20.6 1.21.5 1.21.6 +``` + +Reads `ArgumentTypeInfos.java`, skips the `SharedConstants.IS_RUNNING_IN_IDE` block, and prints C# array initializers for the runtime `COMMAND_ARGUMENT_TYPE` registry order. Use this when Mojang inserts new command argument types and the modern `DeclareCommands` parser needs updated ID routing. + ## Recommended workflow 1. Generate server reports (Step 0) diff --git a/tools/gen_command_argument_registry.py b/tools/gen_command_argument_registry.py new file mode 100644 index 00000000..6e6b70a2 --- /dev/null +++ b/tools/gen_command_argument_registry.py @@ -0,0 +1,85 @@ + +#!/usr/bin/env python3 +""" +Generate ordered DeclareCommands argument-type arrays from decompiled ArgumentTypeInfos.java. + +The runtime server registry excludes registrations guarded by SharedConstants.IS_RUNNING_IN_IDE, +so this script skips that block before emitting the final order. +""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + + +REGISTER_RE = re.compile(r'register\(\$\$0, "([^"]+)"') + + +def extract_runtime_argument_types(path: Path) -> list[str]: + names: list[str] = [] + skipping_ide_block = False + brace_depth = 0 + + for line in path.read_text(encoding="utf-8").splitlines(): + if "if (SharedConstants.IS_RUNNING_IN_IDE)" in line: + skipping_ide_block = True + brace_depth += line.count("{") - line.count("}") + continue + + if skipping_ide_block: + brace_depth += line.count("{") - line.count("}") + if brace_depth <= 0: + skipping_ide_block = False + brace_depth = 0 + continue + + match = REGISTER_RE.search(line) + if match: + names.append(match.group(1)) + + return names + + +def emit_csharp_array(version: str, names: list[str]) -> str: + lines = [ + f"// {version} ({len(names)})", + f"private static readonly string[] s_modernArgumentTypes{version.replace('.', '')} =", + "[", + ] + + for index, name in enumerate(names): + suffix = "," if index < len(names) - 1 else "" + lines.append(f' "{name}"{suffix}') + + lines.append("];") + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "versions", + nargs="+", + help="Minecraft version folders under MinecraftOfficial/, for example: 1.20.6 1.21.5 1.21.6", + ) + parser.add_argument( + "--repo-root", + default=Path(__file__).resolve().parents[1], + type=Path, + help="Repository root. Defaults to the current repo.", + ) + args = parser.parse_args() + + for version in args.versions: + source = args.repo_root / "MinecraftOfficial" / f"{version}-decompiled" / "net" / "minecraft" / "commands" / "synchronization" / "ArgumentTypeInfos.java" + names = extract_runtime_argument_types(source) + print(emit_csharp_array(version, names)) + print() + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())