From 2cedb457ce86d9c1a4475072339b0afeea0a0b3f Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sat, 29 Dec 2018 09:28:24 -0500 Subject: [PATCH] [WIP] Inject commands for Minecraft 1.13+ --- build.gradle | 7 + proxy/build.gradle | 2 + .../proxy/command/VelocityCommandManager.java | 33 ++ .../connection/MinecraftSessionHandler.java | 5 + .../backend/BackendPlaySessionHandler.java | 25 ++ .../client/ClientPlaySessionHandler.java | 53 ++- .../proxy/protocol/ProtocolUtils.java | 16 + .../proxy/protocol/StateRegistry.java | 9 +- .../protocol/packet/AvailableCommands.java | 320 ++++++++++++++++++ .../protocol/packet/TabCompleteRequest.java | 65 ++-- .../protocol/packet/TabCompleteResponse.java | 106 +++++- .../brigadier/ArgumentPropertyRegistry.java | 122 +++++++ .../brigadier/ArgumentPropertySerializer.java | 10 + .../ByteArgumentPropertySerializer.java | 24 ++ .../DoubleArgumentPropertySerializer.java | 41 +++ .../packet/brigadier/DummyProperty.java | 37 ++ .../DummyVoidArgumentPropertySerializer.java | 27 ++ .../FloatArgumentPropertySerializer.java | 42 +++ .../IntegerArgumentPropertySerializer.java | 52 +++ .../StringArgumentPropertySerializer.java | 52 +++ .../VoidArgumentPropertySerializer.java | 32 ++ 21 files changed, 1034 insertions(+), 46 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommands.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertySerializer.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ByteArgumentPropertySerializer.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DoubleArgumentPropertySerializer.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DummyProperty.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DummyVoidArgumentPropertySerializer.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/FloatArgumentPropertySerializer.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/IntegerArgumentPropertySerializer.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/StringArgumentPropertySerializer.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/VoidArgumentPropertySerializer.java diff --git a/build.gradle b/build.gradle index de94d0a6f..e2022aa8a 100644 --- a/build.gradle +++ b/build.gradle @@ -33,9 +33,16 @@ allprojects { repositories { mavenLocal() mavenCentral() + + // for kyoripowered dependencies maven { url 'https://oss.sonatype.org/content/groups/public/' } + + // Brigadier + maven { + url "https://libraries.minecraft.net" + } } test { diff --git a/proxy/build.gradle b/proxy/build.gradle index 2e604e862..725b05d5d 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -47,6 +47,8 @@ dependencies { compile 'it.unimi.dsi:fastutil:8.2.1' compile 'net.kyori:event-method-asm:3.0.0' + compile 'com.mojang:brigadier:1.0.15' + testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}" testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java index e5b07c58b..5593352b0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -2,6 +2,7 @@ package com.velocitypowered.proxy.command; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.velocitypowered.api.command.Command; import com.velocitypowered.api.command.CommandManager; import com.velocitypowered.api.command.CommandSource; @@ -11,6 +12,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import org.checkerframework.checker.nullness.qual.NonNull; public class VelocityCommandManager implements CommandManager { @@ -68,6 +70,10 @@ public class VelocityCommandManager implements CommandManager { return commands.containsKey(command); } + public Set getAllRegisteredCommands() { + return ImmutableSet.copyOf(commands.keySet()); + } + /** * Offer suggestions to fill in the command. * @param source the source for the command @@ -116,4 +122,31 @@ public class VelocityCommandManager implements CommandManager { "Unable to invoke suggestions for command " + alias + " for " + source, e); } } + + public boolean hasPermission(CommandSource source, String cmdLine) { + Preconditions.checkNotNull(source, "source"); + Preconditions.checkNotNull(cmdLine, "cmdLine"); + + String[] split = cmdLine.split(" ", -1); + if (split.length == 0) { + // No command available. + return false; + } + + String alias = split[0]; + @SuppressWarnings("nullness") + String[] actualArgs = Arrays.copyOfRange(split, 1, split.length); + Command command = commands.get(alias.toLowerCase(Locale.ENGLISH)); + if (command == null) { + // No such command. + return false; + } + + try { + return command.hasPermission(source, actualArgs); + } catch (Exception e) { + throw new RuntimeException( + "Unable to invoke suggestions for command " + alias + " for " + source, e); + } + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index e9b88e144..13bfcf4a2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -1,6 +1,7 @@ package com.velocitypowered.proxy.connection; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.AvailableCommands; import com.velocitypowered.proxy.protocol.packet.BossBar; import com.velocitypowered.proxy.protocol.packet.Chat; import com.velocitypowered.proxy.protocol.packet.ClientSettings; @@ -67,6 +68,10 @@ public interface MinecraftSessionHandler { } + default boolean handle(AvailableCommands commands) { + return false; + } + default boolean handle(BossBar packet) { return false; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index d83f7989d..d61b0126c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -1,5 +1,9 @@ package com.velocitypowered.proxy.connection.backend; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.network.ProtocolVersion; @@ -10,6 +14,8 @@ import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.packet.AvailableCommands; +import com.velocitypowered.proxy.protocol.packet.AvailableCommands.ProtocolSuggestionProvider; import com.velocitypowered.proxy.protocol.packet.BossBar; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.JoinGame; @@ -128,6 +134,25 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return false; //Forward packet to player } + @Override + public boolean handle(AvailableCommands commands) { + // Inject commands from the proxy. + for (String command : server.getCommandManager().getAllRegisteredCommands()) { + if (!server.getCommandManager().hasPermission(serverConn.getPlayer(), command)) { + continue; + } + + LiteralCommandNode root = LiteralArgumentBuilder.literal(command) + .then(RequiredArgumentBuilder.argument("args", StringArgumentType.greedyString()) + .suggests(new ProtocolSuggestionProvider("minecraft:ask_server")) + .build()) + .executes((ctx) -> 0) + .build(); + commands.getRootNode().addChild(root); + } + return false; + } + @Override public void handleGeneric(MinecraftPacket packet) { serverConn.getPlayer().getConnection().write(packet); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index ad3aa128b..bce84e206 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -1,5 +1,7 @@ package com.velocitypowered.proxy.connection.client; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; + import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; @@ -19,6 +21,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.Respawn; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; +import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse.Offer; import com.velocitypowered.proxy.protocol.packet.TitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.ThrowableUtils; @@ -133,23 +136,39 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(TabCompleteRequest packet) { // Record the request so that the outstanding request can be augmented later. - if (!packet.isAssumeCommand() && packet.getCommand().startsWith("/")) { - int spacePos = packet.getCommand().indexOf(' '); - if (spacePos > 0) { - String cmd = packet.getCommand().substring(1, spacePos); - if (server.getCommandManager().hasCommand(cmd)) { - List suggestions = server.getCommandManager() - .offerSuggestions(player, packet.getCommand().substring(1)); - if (!suggestions.isEmpty()) { - TabCompleteResponse resp = new TabCompleteResponse(); - resp.getOffers().addAll(suggestions); - player.getConnection().write(resp); - return true; + boolean is113 = player.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0; + boolean isCommand = is113 || (!packet.isAssumeCommand() && packet.getCommand().startsWith("/")); + + if (!isCommand) { + // Outstanding tab completes are recorded for use with 1.12 clients and below to provide + // tab list completion support for command names. In 1.13, Brigadier handles everything for + // us. + outstandingTabComplete = packet; + return false; + } + + int spacePos = packet.getCommand().indexOf(' '); + if (spacePos > 0) { + String command = is113 ? packet.getCommand() : packet.getCommand().substring(1); + String commandLabel = command.substring(0, spacePos); + if (server.getCommandManager().hasCommand(commandLabel)) { + List suggestions = server.getCommandManager().offerSuggestions(player, command); + if (!suggestions.isEmpty()) { + List offers = new ArrayList<>(); + for (String suggestion : suggestions) { + offers.add(new Offer(suggestion, null)); } + TabCompleteResponse resp = new TabCompleteResponse(); + resp.setTransactionId(packet.getTransactionId()); + resp.setStart(spacePos); + resp.setLength(command.length() - spacePos); + resp.getOffers().addAll(offers); + player.getConnection().write(resp); + return true; } } } - outstandingTabComplete = packet; + return false; } @@ -323,7 +342,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Tell the server about this client's plugin message channels. ProtocolVersion serverVersion = serverMc.getProtocolVersion(); Collection toRegister = new HashSet<>(knownChannels); - if (serverVersion.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) { + if (serverVersion.compareTo(MINECRAFT_1_13) >= 0) { toRegister.addAll(server.getChannelRegistrar().getModernChannelIds()); } else { toRegister.addAll(server.getChannelRegistrar().getIdsForLegacyConnections()); @@ -362,7 +381,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { && outstandingTabComplete.getCommand().startsWith("/")) { String command = outstandingTabComplete.getCommand().substring(1); try { - response.getOffers().addAll(server.getCommandManager().offerSuggestions(player, command)); + List offers = server.getCommandManager().offerSuggestions(player, command); + for (String offer : offers) { + response.getOffers().add(new Offer(offer, null)); + } + response.getOffers().sort(null); } catch (Exception e) { logger.error("Unable to provide tab list completions for {} for command '{}'", player.getUsername(), diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index 2862e36ab..f30cd6807 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -109,6 +109,22 @@ public enum ProtocolUtils { buf.writeBytes(array); } + public static int[] readIntegerArray(ByteBuf buf) { + int len = readVarInt(buf); + int[] array = new int[len]; + for (int i = 0; i < len; i++) { + array[i] = readVarInt(buf); + } + return array; + } + + public static void writeIntegerArray(ByteBuf buf, int[] array) { + writeVarInt(buf, array.length); + for (int i : array) { + writeVarInt(buf, i); + } + } + public static UUID readUuid(ByteBuf buf) { long msb = buf.readLong(); long lsb = buf.readLong(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 882c9af1d..d44414085 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -19,6 +19,7 @@ import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import com.google.common.collect.ImmutableList; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.packet.AvailableCommands; import com.velocitypowered.proxy.protocol.packet.BossBar; import com.velocitypowered.proxy.protocol.packet.Chat; import com.velocitypowered.proxy.protocol.packet.ClientSettings; @@ -83,7 +84,8 @@ public enum StateRegistry { map(0x14, MINECRAFT_1_8, false), map(0x01, MINECRAFT_1_9, false), map(0x02, MINECRAFT_1_12, false), - map(0x01, MINECRAFT_1_12_1, false)); + map(0x01, MINECRAFT_1_12_1, false), + map(0x05, MINECRAFT_1_13, false)); serverbound.register(Chat.class, Chat::new, map(0x01, MINECRAFT_1_8, false), map(0x02, MINECRAFT_1_9, false), @@ -121,7 +123,10 @@ public enum StateRegistry { clientbound.register(TabCompleteResponse.class, TabCompleteResponse::new, map(0x3A, MINECRAFT_1_8, false), map(0x0E, MINECRAFT_1_9, false), - map(0x0E, MINECRAFT_1_12, false)); + map(0x0E, MINECRAFT_1_12, false), + map(0x10, MINECRAFT_1_13, false)); + clientbound.register(AvailableCommands.class, AvailableCommands::new, + map(0x11, MINECRAFT_1_13, false)); clientbound.register(PluginMessage.class, PluginMessage::new, map(0x3F, MINECRAFT_1_8, false), map(0x18, MINECRAFT_1_9, false), diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommands.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommands.java new file mode 100644 index 000000000..086c4455f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommands.java @@ -0,0 +1,320 @@ +package com.velocitypowered.proxy.protocol.packet; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.RootCommandNode; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; +import com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentPropertyRegistry; +import io.netty.buffer.ByteBuf; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class AvailableCommands implements MinecraftPacket { + private static final byte NODE_TYPE_ROOT = 0x00; + private static final byte NODE_TYPE_LITERAL = 0x01; + private static final byte NODE_TYPE_ARGUMENT = 0x02; + + private static final byte FLAG_NODE_TYPE = 0x03; + private static final byte FLAG_EXECUTABLE = 0x04; + private static final byte FLAG_IS_REDIRECT = 0x08; + private static final byte FLAG_HAS_SUGGESTIONS = 0x10; + + // Note: Velocity doesn't use Brigadier for command handling. This may change in Velocity 2.0.0. + @MonotonicNonNull + private RootCommandNode rootNode; + + public RootCommandNode getRootNode() { + if (rootNode == null) { + throw new IllegalStateException("Packet not yet deserialized"); + } + return rootNode; + } + + @Override + public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + int commands = ProtocolUtils.readVarInt(buf); + WireNode[] wireNodes = new WireNode[commands]; + for (int i = 0; i < commands; i++) { + WireNode node = deserializeNode(buf, i); + wireNodes[i] = node; + } + + // Iterate over the deserialized nodes and attempt to form a graph. We also resolve any cycles + // that exist. + Queue nodeQueue = new ArrayDeque<>(Arrays.asList(wireNodes)); + while (!nodeQueue.isEmpty()) { + boolean cycling = false; + + for (Iterator it = nodeQueue.iterator(); it.hasNext(); ) { + WireNode node = it.next(); + if (node.toNode(wireNodes)) { + cycling = true; + it.remove(); + } + } + + if (!cycling) { + // Uh-oh. We can't cycle. This is bad. + throw new IllegalStateException("Stopped cycling; the root node can't be built."); + } + } + + int rootIdx = ProtocolUtils.readVarInt(buf); + rootNode = (RootCommandNode) wireNodes[rootIdx].built; + } + + @Override + public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + // Assign all the children an index. + Deque> childrenQueue = new ArrayDeque<>(ImmutableList.of(rootNode)); + Object2IntMap> idMappings = new Object2IntLinkedOpenHashMap<>(); + while (!childrenQueue.isEmpty()) { + CommandNode child = childrenQueue.poll(); + if (!idMappings.containsKey(child)) { + idMappings.put(child, idMappings.size()); + childrenQueue.addAll(child.getChildren()); + } + } + + // Now serialize the children + ProtocolUtils.writeVarInt(buf, idMappings.size()); + for (CommandNode child : idMappings.keySet()) { + serializeNode(child, buf, idMappings); + } + ProtocolUtils.writeVarInt(buf, idMappings.getInt(rootNode)); + } + + private static void serializeNode(CommandNode node, ByteBuf buf, + Object2IntMap> idMappings) { + byte flags = 0; + if (node.getRedirect() != null) { + flags |= FLAG_IS_REDIRECT; + } + if (node.getCommand() != null) { + flags |= FLAG_EXECUTABLE; + } + + if (node instanceof RootCommandNode) { + flags |= NODE_TYPE_ROOT; + } else if (node instanceof LiteralCommandNode) { + flags |= NODE_TYPE_LITERAL; + } else if (node instanceof ArgumentCommandNode) { + flags |= NODE_TYPE_ARGUMENT; + if (((ArgumentCommandNode) node).getCustomSuggestions() != null) { + flags |= FLAG_HAS_SUGGESTIONS; + } + } else { + throw new IllegalArgumentException("Unknown node type " + node.getClass().getName()); + } + + buf.writeByte(flags); + ProtocolUtils.writeVarInt(buf, node.getChildren().size()); + for (CommandNode child : node.getChildren()) { + ProtocolUtils.writeVarInt(buf, idMappings.getInt(child)); + } + if (node.getRedirect() != null) { + ProtocolUtils.writeVarInt(buf, idMappings.getInt(node.getRedirect())); + } + + if (node instanceof ArgumentCommandNode) { + ProtocolUtils.writeString(buf, node.getName()); + ArgumentPropertyRegistry.serialize(buf, ((ArgumentCommandNode) node).getType()); + + if (((ArgumentCommandNode) node).getCustomSuggestions() != null) { + // The unchecked cast is required, but it is not particularly relevant because we check for + // a more specific type later. (Even then, we only pull out one field.) + @SuppressWarnings("unchecked") + SuggestionProvider provider = ((ArgumentCommandNode) node).getCustomSuggestions(); + + if (!(provider instanceof ProtocolSuggestionProvider)) { + throw new IllegalArgumentException("Suggestion provider " + provider.getClass().getName() + + " is not valid."); + } + + ProtocolUtils.writeString(buf, ((ProtocolSuggestionProvider) provider).name); + } + } else if (node instanceof LiteralCommandNode) { + ProtocolUtils.writeString(buf, node.getName()); + } + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + private static WireNode deserializeNode(ByteBuf buf, int idx) { + byte flags = buf.readByte(); + int[] children = ProtocolUtils.readIntegerArray(buf); + int redirectTo = -1; + if ((flags & FLAG_IS_REDIRECT) > 0) { + redirectTo = ProtocolUtils.readVarInt(buf); + } + + switch (flags & FLAG_NODE_TYPE) { + case NODE_TYPE_ROOT: + return new WireNode(idx, flags, children, redirectTo, null); + case NODE_TYPE_LITERAL: + return new WireNode(idx, flags, children, redirectTo, LiteralArgumentBuilder + .literal(ProtocolUtils.readString(buf))); + case NODE_TYPE_ARGUMENT: + String name = ProtocolUtils.readString(buf); + ArgumentType argumentType = ArgumentPropertyRegistry.deserialize(buf); + + RequiredArgumentBuilder argumentBuilder = RequiredArgumentBuilder + .argument(name, argumentType); + if ((flags & FLAG_HAS_SUGGESTIONS) != 0) { + argumentBuilder.suggests(new ProtocolSuggestionProvider(ProtocolUtils.readString(buf))); + } + + return new WireNode(idx, flags, children, redirectTo, argumentBuilder); + default: + throw new IllegalArgumentException("Unknown node type " + (flags & FLAG_NODE_TYPE)); + } + } + + private static class WireNode { + private final int idx; + private final byte flags; + private final int[] children; + private final int redirectTo; + @Nullable + private final ArgumentBuilder args; + @MonotonicNonNull + private CommandNode built; + + private WireNode(int idx, byte flags, int[] children, int redirectTo, + @Nullable ArgumentBuilder args) { + this.idx = idx; + this.flags = flags; + this.children = children; + this.redirectTo = redirectTo; + this.args = args; + } + + public boolean toNode(WireNode[] wireNodes) { + if (this.built == null) { + // Ensure all children exist. Note that we delay checking if the node has been built yet; + // that needs to come after this node is built. + for (int child : children) { + if (child >= wireNodes.length) { + throw new IllegalStateException("Node points to non-existent index " + redirectTo); + } + } + + int type = flags & FLAG_NODE_TYPE; + if (type == NODE_TYPE_ROOT) { + this.built = new RootCommandNode<>(); + } else { + if (args == null) { + throw new IllegalStateException("Non-root node without args builder!"); + } + + // Add any redirects + if (redirectTo != -1) { + if (redirectTo >= wireNodes.length) { + throw new IllegalStateException("Node points to non-existent index " + redirectTo); + } + + if (wireNodes[redirectTo].built != null) { + args.redirect(wireNodes[redirectTo].built); + } else { + // Redirect node does not yet exist + return false; + } + } + + // If executable, add a dummy command + if ((flags & FLAG_EXECUTABLE) != 0) { + args.executes((Command) context -> 0); + } + + this.built = args.build(); + } + } + + for (int child : children) { + if (wireNodes[child].built == null) { + // The child is not yet deserialized. The node can't be built now. + return false; + } + } + + // Associate children with nodes + for (int child : children) { + CommandNode childNode = wireNodes[child].built; + if (!(childNode instanceof RootCommandNode)) { + built.addChild(childNode); + } + } + + return true; + } + + @Override + public String toString() { + MoreObjects.ToStringHelper helper = MoreObjects.toStringHelper(this) + .add("idx", idx) + .add("flags", flags) + .add("children", children) + .add("redirectTo", redirectTo); + + if (args != null) { + if (args instanceof LiteralArgumentBuilder) { + helper.add("argsLabel", ((LiteralArgumentBuilder) args).getLiteral()); + } else if (args instanceof RequiredArgumentBuilder) { + helper.add("argsName", ((RequiredArgumentBuilder) args).getName()); + } + } + + return helper.toString(); + } + } + + /** + * A placeholder {@link SuggestionProvider} used internally to preserve the suggestion provider + * name. + */ + public static class ProtocolSuggestionProvider implements SuggestionProvider { + + private final String name; + + public ProtocolSuggestionProvider(String name) { + this.name = name; + } + + @Override + public CompletableFuture getSuggestions(CommandContext context, + SuggestionsBuilder builder) throws CommandSyntaxException { + return builder.buildFuture(); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java index e0ce9a7a5..d334e8db3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java @@ -1,5 +1,9 @@ package com.velocitypowered.proxy.protocol.packet; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; + +import com.google.common.base.MoreObjects; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; @@ -10,6 +14,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; public class TabCompleteRequest implements MinecraftPacket { private @Nullable String command; + private int transactionId; private boolean assumeCommand; private boolean hasPosition; private long position; @@ -49,25 +54,39 @@ public class TabCompleteRequest implements MinecraftPacket { this.position = position; } + public int getTransactionId() { + return transactionId; + } + + public void setTransactionId(int transactionId) { + this.transactionId = transactionId; + } + @Override public String toString() { - return "TabCompleteRequest{" - + "command='" + command + '\'' - + ", assumeCommand=" + assumeCommand - + ", hasPosition=" + hasPosition - + ", position=" + position - + '}'; + return MoreObjects.toStringHelper(this) + .add("command", command) + .add("transactionId", transactionId) + .add("assumeCommand", assumeCommand) + .add("hasPosition", hasPosition) + .add("position", position) + .toString(); } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - this.command = ProtocolUtils.readString(buf); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { - this.assumeCommand = buf.readBoolean(); - } - this.hasPosition = buf.readBoolean(); - if (hasPosition) { - this.position = buf.readLong(); + if (version.compareTo(MINECRAFT_1_13) >= 0) { + this.transactionId = ProtocolUtils.readVarInt(buf); + this.command = ProtocolUtils.readString(buf); + } else { + this.command = ProtocolUtils.readString(buf); + if (version.compareTo(MINECRAFT_1_9) >= 0) { + this.assumeCommand = buf.readBoolean(); + } + this.hasPosition = buf.readBoolean(); + if (hasPosition) { + this.position = buf.readLong(); + } } } @@ -76,13 +95,19 @@ public class TabCompleteRequest implements MinecraftPacket { if (command == null) { throw new IllegalStateException("Command is not specified"); } - ProtocolUtils.writeString(buf, command); - if (version.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0) { - buf.writeBoolean(assumeCommand); - } - buf.writeBoolean(hasPosition); - if (hasPosition) { - buf.writeLong(position); + + if (version.compareTo(MINECRAFT_1_13) >= 0) { + ProtocolUtils.writeVarInt(buf, transactionId); + ProtocolUtils.writeString(buf, command); + } else { + ProtocolUtils.writeString(buf, command); + if (version.compareTo(MINECRAFT_1_9) >= 0) { + buf.writeBoolean(assumeCommand); + } + buf.writeBoolean(hasPosition); + if (hasPosition) { + buf.writeLong(position); + } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java index 3b208bab0..b6e274bb8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java @@ -1,5 +1,8 @@ package com.velocitypowered.proxy.protocol.packet; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; + +import com.google.common.base.MoreObjects; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; @@ -7,35 +10,95 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import java.util.ArrayList; import java.util.List; +import net.kyori.text.Component; +import net.kyori.text.serializer.ComponentSerializers; +import org.checkerframework.checker.nullness.qual.Nullable; public class TabCompleteResponse implements MinecraftPacket { - private final List offers = new ArrayList<>(); + private int transactionId; + private int start; + private int length; + private final List offers = new ArrayList<>(); - public List getOffers() { + public int getTransactionId() { + return transactionId; + } + + public void setTransactionId(int transactionId) { + this.transactionId = transactionId; + } + + public int getStart() { + return start; + } + + public void setStart(int start) { + this.start = start; + } + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } + + public List getOffers() { return offers; } @Override public String toString() { return "TabCompleteResponse{" - + "offers=" + offers + + "transactionId=" + transactionId + + ", start=" + start + + ", length=" + length + + ", offers=" + offers + '}'; } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - int offersAvailable = ProtocolUtils.readVarInt(buf); - for (int i = 0; i < offersAvailable; i++) { - offers.add(ProtocolUtils.readString(buf)); + if (version.compareTo(MINECRAFT_1_13) >= 0) { + this.transactionId = ProtocolUtils.readVarInt(buf); + this.start = ProtocolUtils.readVarInt(buf); + this.length = ProtocolUtils.readVarInt(buf); + int offersAvailable = ProtocolUtils.readVarInt(buf); + for (int i = 0; i < offersAvailable; i++) { + String offer = ProtocolUtils.readString(buf); + Component tooltip = buf.readBoolean() ? ComponentSerializers.JSON.deserialize( + ProtocolUtils.readString(buf)) : null; + offers.add(new Offer(offer, tooltip)); + } + } else { + int offersAvailable = ProtocolUtils.readVarInt(buf); + for (int i = 0; i < offersAvailable; i++) { + offers.add(new Offer(ProtocolUtils.readString(buf), null)); + } } } @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - ProtocolUtils.writeVarInt(buf, offers.size()); - for (String offer : offers) { - ProtocolUtils.writeString(buf, offer); + if (version.compareTo(MINECRAFT_1_13) >= 0) { + ProtocolUtils.writeVarInt(buf, this.transactionId); + ProtocolUtils.writeVarInt(buf, this.start); + ProtocolUtils.writeVarInt(buf, this.length); + ProtocolUtils.writeVarInt(buf, offers.size()); + for (Offer offer : offers) { + ProtocolUtils.writeString(buf, offer.text); + buf.writeBoolean(offer.tooltip != null); + if (offer.tooltip != null) { + ProtocolUtils.writeString(buf, ComponentSerializers.JSON.serialize(offer.tooltip)); + } + } + } else { + ProtocolUtils.writeVarInt(buf, offers.size()); + for (Offer offer : offers) { + ProtocolUtils.writeString(buf, offer.text); + } } } @@ -43,4 +106,29 @@ public class TabCompleteResponse implements MinecraftPacket { public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); } + + public static class Offer implements Comparable { + private final String text; + @Nullable + private final Component tooltip; + + public Offer(String text, + @Nullable Component tooltip) { + this.text = text; + this.tooltip = tooltip; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("text", text) + .add("tooltip", tooltip) + .toString(); + } + + @Override + public int compareTo(Offer o) { + return this.text.compareTo(o.text); + } + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java new file mode 100644 index 000000000..1b68b13bb --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertyRegistry.java @@ -0,0 +1,122 @@ +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE; +import static com.velocitypowered.proxy.protocol.packet.brigadier.DummyVoidArgumentPropertySerializer.DUMMY; +import static com.velocitypowered.proxy.protocol.packet.brigadier.FloatArgumentPropertySerializer.FLOAT; +import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.INTEGER; +import static com.velocitypowered.proxy.protocol.packet.brigadier.StringArgumentPropertySerializer.STRING; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.FloatArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import java.util.HashMap; +import java.util.Map; + +public class ArgumentPropertyRegistry { + private ArgumentPropertyRegistry() { + throw new AssertionError(); + } + + private static final Map> byId = new HashMap<>(); + private static final Map, + ArgumentPropertySerializer> byClass = new HashMap<>(); + private static final Map, String> classToId = new HashMap<>(); + + private static > void register(String identifier, Class klazz, + ArgumentPropertySerializer serializer) { + byId.put(identifier, serializer); + byClass.put(klazz, serializer); + classToId.put(klazz, identifier); + } + + private static void dummy(String identifier, ArgumentPropertySerializer serializer) { + byId.put(identifier, serializer); + } + + public static ArgumentType deserialize(ByteBuf buf) { + String identifier = ProtocolUtils.readString(buf); + ArgumentPropertySerializer serializer = byId.get(identifier); + if (serializer == null) { + throw new IllegalArgumentException("Argument type identifier " + identifier + " unknown."); + } + Object result = serializer.deserialize(buf); + + if (result instanceof ArgumentType) { + return (ArgumentType) result; + } else { + return new DummyProperty(identifier, serializer, result); + } + } + + public static void serialize(ByteBuf buf, ArgumentType type) { + if (type instanceof DummyProperty) { + DummyProperty property = (DummyProperty) type; + ProtocolUtils.writeString(buf, property.getIdentifier()); + if (property.getResult() != null) { + property.getSerializer().serialize(property.getResult(), buf); + } + } else { + ArgumentPropertySerializer serializer = byClass.get(type.getClass()); + String id = classToId.get(type.getClass()); + if (serializer == null || id == null) { + throw new IllegalArgumentException("Don't know how to serialize " + + type.getClass().getName()); + } + ProtocolUtils.writeString(buf, id); + serializer.serialize(type, buf); + } + } + + static { + // Base Brigadier argument types + register("brigadier:string", StringArgumentType.class, STRING); + register("brigadier:integer", IntegerArgumentType.class, INTEGER); + register("brigadier:float", FloatArgumentType.class, FLOAT); + register("brigadier:double", DoubleArgumentType.class, DOUBLE); + register("brigadier:bool", BoolArgumentType.class, + VoidArgumentPropertySerializer.create(BoolArgumentType::bool)); + + // Minecraft argument types with extra properties + dummy("minecraft:entity", ByteArgumentPropertySerializer.BYTE); + dummy("minecraft:score_holder", ByteArgumentPropertySerializer.BYTE); + + // Minecraft argument types + dummy("minecraft:game_profile", DUMMY); + dummy("minecraft:block_pos", DUMMY); + dummy("minecraft:column_pos", DUMMY); + dummy("minecraft:vec3", DUMMY); + dummy("minecraft:vec2", DUMMY); + dummy("minecraft:block_state", DUMMY); + dummy("minecraft:block_predicate", DUMMY); + dummy("minecraft:item_stack", DUMMY); + dummy("minecraft:item_predicate", DUMMY); + dummy("minecraft:color", DUMMY); + dummy("minecraft:component", DUMMY); + dummy("minecraft:message", DUMMY); + dummy("minecraft:nbt", DUMMY); + dummy("minecraft:nbt_path", DUMMY); + dummy("minecraft:objective", DUMMY); + dummy("minecraft:objective_criteria", DUMMY); + dummy("minecraft:operation", DUMMY); + dummy("minecraft:particle", DUMMY); + dummy("minecraft:rotation", DUMMY); + dummy("minecraft:scoreboard_slot", DUMMY); + dummy("minecraft:swizzle", DUMMY); + dummy("minecraft:team", DUMMY); + dummy("minecraft:item_slot", DUMMY); + dummy("minecraft:resource_location", DUMMY); + dummy("minecraft:mob_effect", DUMMY); + dummy("minecraft:function", DUMMY); + dummy("minecraft:entity_anchor", DUMMY); + dummy("minecraft:item_enchantment", DUMMY); + dummy("minecraft:entity_summon", DUMMY); + dummy("minecraft:dimension", DUMMY); + dummy("minecraft:int_range", DUMMY); + dummy("minecraft:float_range", DUMMY); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertySerializer.java new file mode 100644 index 000000000..077474a63 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ArgumentPropertySerializer.java @@ -0,0 +1,10 @@ +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + +public interface ArgumentPropertySerializer { + @Nullable T deserialize(ByteBuf buf); + + void serialize(T object, ByteBuf buf); +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ByteArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ByteArgumentPropertySerializer.java new file mode 100644 index 000000000..d62c19c8b --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/ByteArgumentPropertySerializer.java @@ -0,0 +1,24 @@ +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + +class ByteArgumentPropertySerializer implements ArgumentPropertySerializer { + + static final ByteArgumentPropertySerializer BYTE = new ByteArgumentPropertySerializer(); + + private ByteArgumentPropertySerializer() { + + } + + @Nullable + @Override + public Byte deserialize(ByteBuf buf) { + return buf.readByte(); + } + + @Override + public void serialize(Byte object, ByteBuf buf) { + buf.writeByte(object); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DoubleArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DoubleArgumentPropertySerializer.java new file mode 100644 index 000000000..b36e5869c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DoubleArgumentPropertySerializer.java @@ -0,0 +1,41 @@ +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.HAS_MAXIMUM; +import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.HAS_MINIMUM; +import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.getFlags; + +import com.mojang.brigadier.arguments.DoubleArgumentType; +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + +class DoubleArgumentPropertySerializer implements ArgumentPropertySerializer { + + static final DoubleArgumentPropertySerializer DOUBLE = new DoubleArgumentPropertySerializer(); + + private DoubleArgumentPropertySerializer() { + } + + @Nullable + @Override + public DoubleArgumentType deserialize(ByteBuf buf) { + byte flags = buf.readByte(); + double minimum = (flags & HAS_MINIMUM) != 0 ? buf.readDouble() : Double.MIN_VALUE; + double maximum = (flags & HAS_MAXIMUM) != 0 ? buf.readDouble() : Double.MAX_VALUE; + return DoubleArgumentType.doubleArg(minimum, maximum); + } + + @Override + public void serialize(DoubleArgumentType object, ByteBuf buf) { + boolean hasMinimum = object.getMinimum() != Double.MIN_VALUE; + boolean hasMaximum = object.getMaximum() != Double.MAX_VALUE; + byte flag = getFlags(hasMinimum, hasMaximum); + + buf.writeByte(flag); + if (hasMinimum) { + buf.writeDouble(object.getMinimum()); + } + if (hasMaximum) { + buf.writeDouble(object.getMaximum()); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DummyProperty.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DummyProperty.java new file mode 100644 index 000000000..2ea410d55 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DummyProperty.java @@ -0,0 +1,37 @@ +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import org.checkerframework.checker.nullness.qual.Nullable; + +class DummyProperty implements ArgumentType { + + private final String identifier; + private final ArgumentPropertySerializer serializer; + @Nullable + private final T result; + + DummyProperty(String identifier, ArgumentPropertySerializer serializer, @Nullable T result) { + this.identifier = identifier; + this.serializer = serializer; + this.result = result; + } + + @Override + public T parse(StringReader reader) throws CommandSyntaxException { + throw new UnsupportedOperationException(); + } + + public String getIdentifier() { + return identifier; + } + + public ArgumentPropertySerializer getSerializer() { + return serializer; + } + + public @Nullable T getResult() { + return result; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DummyVoidArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DummyVoidArgumentPropertySerializer.java new file mode 100644 index 000000000..075d0435f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/DummyVoidArgumentPropertySerializer.java @@ -0,0 +1,27 @@ +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * An argument property serializer that will serialize and deserialize nothing. + */ +class DummyVoidArgumentPropertySerializer implements ArgumentPropertySerializer { + + static final ArgumentPropertySerializer DUMMY = + new DummyVoidArgumentPropertySerializer(); + + private DummyVoidArgumentPropertySerializer() { + } + + @Nullable + @Override + public Void deserialize(ByteBuf buf) { + return null; + } + + @Override + public void serialize(Void object, ByteBuf buf) { + + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/FloatArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/FloatArgumentPropertySerializer.java new file mode 100644 index 000000000..ec7da4d17 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/FloatArgumentPropertySerializer.java @@ -0,0 +1,42 @@ +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.HAS_MAXIMUM; +import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.HAS_MINIMUM; +import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.getFlags; + +import com.mojang.brigadier.arguments.FloatArgumentType; +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + +class FloatArgumentPropertySerializer implements ArgumentPropertySerializer { + + static FloatArgumentPropertySerializer FLOAT = new FloatArgumentPropertySerializer(); + + private FloatArgumentPropertySerializer() { + + } + + @Nullable + @Override + public FloatArgumentType deserialize(ByteBuf buf) { + byte flags = buf.readByte(); + float minimum = (flags & HAS_MINIMUM) != 0 ? buf.readFloat() : Float.MIN_VALUE; + float maximum = (flags & HAS_MAXIMUM) != 0 ? buf.readFloat() : Float.MAX_VALUE; + return FloatArgumentType.floatArg(minimum, maximum); + } + + @Override + public void serialize(FloatArgumentType object, ByteBuf buf) { + boolean hasMinimum = object.getMinimum() != Float.MIN_VALUE; + boolean hasMaximum = object.getMaximum() != Float.MAX_VALUE; + byte flag = getFlags(hasMinimum, hasMaximum); + + buf.writeByte(flag); + if (hasMinimum) { + buf.writeFloat(object.getMinimum()); + } + if (hasMaximum) { + buf.writeFloat(object.getMaximum()); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/IntegerArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/IntegerArgumentPropertySerializer.java new file mode 100644 index 000000000..c8ca10549 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/IntegerArgumentPropertySerializer.java @@ -0,0 +1,52 @@ +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import com.mojang.brigadier.arguments.IntegerArgumentType; +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + +class IntegerArgumentPropertySerializer implements ArgumentPropertySerializer { + + static final IntegerArgumentPropertySerializer INTEGER = new IntegerArgumentPropertySerializer(); + + static final byte HAS_MINIMUM = 0x01; + static final byte HAS_MAXIMUM = 0x02; + + private IntegerArgumentPropertySerializer() { + + } + + @Nullable + @Override + public IntegerArgumentType deserialize(ByteBuf buf) { + byte flags = buf.readByte(); + int minimum = (flags & HAS_MINIMUM) != 0 ? buf.readInt() : Integer.MIN_VALUE; + int maximum = (flags & HAS_MAXIMUM) != 0 ? buf.readInt() : Integer.MAX_VALUE; + return IntegerArgumentType.integer(minimum, maximum); + } + + @Override + public void serialize(IntegerArgumentType object, ByteBuf buf) { + boolean hasMinimum = object.getMinimum() != Integer.MIN_VALUE; + boolean hasMaximum = object.getMaximum() != Integer.MAX_VALUE; + byte flag = getFlags(hasMinimum, hasMaximum); + + buf.writeByte(flag); + if (hasMinimum) { + buf.writeInt(object.getMinimum()); + } + if (hasMaximum) { + buf.writeInt(object.getMaximum()); + } + } + + static byte getFlags(boolean hasMinimum, boolean hasMaximum) { + byte flags = 0; + if (hasMinimum) { + flags |= HAS_MINIMUM; + } + if (hasMaximum) { + flags |= HAS_MAXIMUM; + } + return flags; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/StringArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/StringArgumentPropertySerializer.java new file mode 100644 index 000000000..e93818deb --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/StringArgumentPropertySerializer.java @@ -0,0 +1,52 @@ +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Serializes properties for {@link StringArgumentType}. + */ +class StringArgumentPropertySerializer implements ArgumentPropertySerializer { + + public static final ArgumentPropertySerializer STRING = + new StringArgumentPropertySerializer(); + + private StringArgumentPropertySerializer() { + + } + + @Nullable + @Override + public StringArgumentType deserialize(ByteBuf buf) { + int type = ProtocolUtils.readVarInt(buf); + switch (type) { + case 0: + return StringArgumentType.word(); + case 1: + return StringArgumentType.string(); + case 2: + return StringArgumentType.greedyString(); + default: + throw new IllegalArgumentException("Invalid string argument type " + type); + } + } + + @Override + public void serialize(StringArgumentType object, ByteBuf buf) { + switch (object.getType()) { + case SINGLE_WORD: + ProtocolUtils.writeVarInt(buf, 0); + break; + case QUOTABLE_PHRASE: + ProtocolUtils.writeVarInt(buf, 1); + break; + case GREEDY_PHRASE: + ProtocolUtils.writeVarInt(buf, 2); + break; + default: + throw new IllegalArgumentException("Invalid string argument type " + object.getType()); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/VoidArgumentPropertySerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/VoidArgumentPropertySerializer.java new file mode 100644 index 000000000..797cbc360 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/brigadier/VoidArgumentPropertySerializer.java @@ -0,0 +1,32 @@ +package com.velocitypowered.proxy.protocol.packet.brigadier; + +import com.mojang.brigadier.arguments.ArgumentType; +import io.netty.buffer.ByteBuf; +import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; + +class VoidArgumentPropertySerializer> + implements ArgumentPropertySerializer { + + private final Supplier argumentSupplier; + + public VoidArgumentPropertySerializer(Supplier argumentSupplier) { + this.argumentSupplier = argumentSupplier; + } + + public static > ArgumentPropertySerializer create( + Supplier supplier) { + return new VoidArgumentPropertySerializer(supplier); + } + + @Nullable + @Override + public T deserialize(ByteBuf buf) { + return argumentSupplier.get(); + } + + @Override + public void serialize(T object, ByteBuf buf) { + + } +}