From 02a65e34cffd1bfb4777298d6e9334676c24edf6 Mon Sep 17 00:00:00 2001 From: md_5 Date: Thu, 20 Dec 2018 10:33:36 +1100 Subject: [PATCH] #2479: Allow injection of BungeeCord commands to 1.13 with inject_commands option --- .../md_5/bungee/api/plugin/PluginManager.java | 11 + .../main/java/net/md_5/bungee/Bootstrap.java | 4 +- pom.xml | 2 +- protocol/pom.xml | 6 + .../protocol/AbstractPacketHandler.java | 5 + .../md_5/bungee/protocol/DefinedPacket.java | 13 + .../net/md_5/bungee/protocol/Protocol.java | 8 + .../md_5/bungee/protocol/packet/Commands.java | 638 ++++++++++++++++++ .../protocol/packet/TabCompleteResponse.java | 54 +- .../net/md_5/bungee/conf/Configuration.java | 6 + .../bungee/connection/DownstreamBridge.java | 36 + .../bungee/connection/UpstreamBridge.java | 17 + 12 files changed, 768 insertions(+), 32 deletions(-) create mode 100644 protocol/src/main/java/net/md_5/bungee/protocol/packet/Commands.java diff --git a/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java b/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java index afa98ed9..479dfb5c 100644 --- a/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java +++ b/api/src/main/java/net/md_5/bungee/api/plugin/PluginManager.java @@ -11,6 +11,7 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -428,4 +429,14 @@ public class PluginManager it.remove(); } } + + /** + * Get an unmodifiable collection of all registered commands. + * + * @return commands + */ + public Collection getCommands() + { + return Collections.unmodifiableCollection( commandMap.keySet() ); + } } diff --git a/bootstrap/src/main/java/net/md_5/bungee/Bootstrap.java b/bootstrap/src/main/java/net/md_5/bungee/Bootstrap.java index b7cb81e2..6be22739 100644 --- a/bootstrap/src/main/java/net/md_5/bungee/Bootstrap.java +++ b/bootstrap/src/main/java/net/md_5/bungee/Bootstrap.java @@ -5,9 +5,9 @@ public class Bootstrap public static void main(String[] args) throws Exception { - if ( Float.parseFloat( System.getProperty( "java.class.version" ) ) < 51.0 ) + if ( Float.parseFloat( System.getProperty( "java.class.version" ) ) < 52.0 ) { - System.err.println( "*** ERROR *** BungeeCord requires Java 7 or above to function! Please download and install it!" ); + System.err.println( "*** ERROR *** BungeeCord requires Java 8 or above to function! Please download and install it!" ); System.out.println( "You can check your Java version with the command: java -version" ); return; } diff --git a/pom.xml b/pom.xml index 8587db46..4f8786a5 100644 --- a/pom.xml +++ b/pom.xml @@ -133,7 +133,7 @@ org.codehaus.mojo.signature - java17 + java18 1.0 diff --git a/protocol/pom.xml b/protocol/pom.xml index 34e38ff7..d6d29d19 100644 --- a/protocol/pom.xml +++ b/protocol/pom.xml @@ -19,6 +19,12 @@ Minimal implementation of the Minecraft protocol for use in BungeeCord + + net.md-5 + brigadier + 1.0.16-SNAPSHOT + compile + net.md-5 bungeecord-chat diff --git a/protocol/src/main/java/net/md_5/bungee/protocol/AbstractPacketHandler.java b/protocol/src/main/java/net/md_5/bungee/protocol/AbstractPacketHandler.java index adf189c3..219488dd 100644 --- a/protocol/src/main/java/net/md_5/bungee/protocol/AbstractPacketHandler.java +++ b/protocol/src/main/java/net/md_5/bungee/protocol/AbstractPacketHandler.java @@ -6,6 +6,7 @@ import net.md_5.bungee.protocol.packet.ClientSettings; import net.md_5.bungee.protocol.packet.ClientStatus; import net.md_5.bungee.protocol.packet.Login; import net.md_5.bungee.protocol.packet.Chat; +import net.md_5.bungee.protocol.packet.Commands; import net.md_5.bungee.protocol.packet.EncryptionRequest; import net.md_5.bungee.protocol.packet.PlayerListHeaderFooter; import net.md_5.bungee.protocol.packet.PlayerListItem; @@ -163,4 +164,8 @@ public abstract class AbstractPacketHandler public void handle(EntityStatus status) throws Exception { } + + public void handle(Commands commands) throws Exception + { + } } diff --git a/protocol/src/main/java/net/md_5/bungee/protocol/DefinedPacket.java b/protocol/src/main/java/net/md_5/bungee/protocol/DefinedPacket.java index 10e16d79..28a3efde 100644 --- a/protocol/src/main/java/net/md_5/bungee/protocol/DefinedPacket.java +++ b/protocol/src/main/java/net/md_5/bungee/protocol/DefinedPacket.java @@ -73,6 +73,19 @@ public abstract class DefinedPacket return ret; } + public static int[] readVarIntArray(ByteBuf buf) + { + int len = readVarInt( buf ); + int[] ret = new int[ len ]; + + for ( int i = 0; i < len; i++ ) + { + ret[i] = readVarInt( buf ); + } + + return ret; + } + public static void writeStringArray(List s, ByteBuf buf) { writeVarInt( s.size(), buf ); diff --git a/protocol/src/main/java/net/md_5/bungee/protocol/Protocol.java b/protocol/src/main/java/net/md_5/bungee/protocol/Protocol.java index 557144ca..6e2abdc5 100644 --- a/protocol/src/main/java/net/md_5/bungee/protocol/Protocol.java +++ b/protocol/src/main/java/net/md_5/bungee/protocol/Protocol.java @@ -14,6 +14,7 @@ import lombok.RequiredArgsConstructor; import net.md_5.bungee.protocol.packet.BossBar; import net.md_5.bungee.protocol.packet.Chat; import net.md_5.bungee.protocol.packet.ClientSettings; +import net.md_5.bungee.protocol.packet.Commands; import net.md_5.bungee.protocol.packet.EncryptionRequest; import net.md_5.bungee.protocol.packet.EncryptionResponse; import net.md_5.bungee.protocol.packet.EntityStatus; @@ -179,6 +180,13 @@ public enum Protocol map( ProtocolConstants.MINECRAFT_1_12, 0x1B ), map( ProtocolConstants.MINECRAFT_1_13, 0x1C ) ); + if ( Boolean.getBoolean( "net.md-5.bungee.protocol.register_commands" ) ) + { + TO_CLIENT.registerPacket( + Commands.class, + map( ProtocolConstants.MINECRAFT_1_13, 0x11 ) + ); + } TO_SERVER.registerPacket( KeepAlive.class, diff --git a/protocol/src/main/java/net/md_5/bungee/protocol/packet/Commands.java b/protocol/src/main/java/net/md_5/bungee/protocol/packet/Commands.java new file mode 100644 index 00000000..80485a4d --- /dev/null +++ b/protocol/src/main/java/net/md_5/bungee/protocol/packet/Commands.java @@ -0,0 +1,638 @@ +package net.md_5.bungee.protocol.packet; + +import com.google.common.base.Preconditions; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +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.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 io.netty.buffer.ByteBuf; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import net.md_5.bungee.protocol.AbstractPacketHandler; +import net.md_5.bungee.protocol.DefinedPacket; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class Commands extends DefinedPacket +{ + + private static final int FLAG_TYPE = 0x3; + private static final int FLAG_EXECUTABLE = 0x4; + private static final int FLAG_REDIRECT = 0x8; + private static final int FLAG_SUGGESTIONS = 0x10; + // + private static final int NODE_ROOT = 0; + private static final int NODE_LITERAL = 1; + private static final int NODE_ARGUMENT = 2; + // + private RootCommandNode root; + + @Override + public void read(ByteBuf buf) + { + int nodeCount = readVarInt( buf ); + NetworkNode[] nodes = new NetworkNode[ nodeCount ]; + Deque nodeQueue = new ArrayDeque<>( nodes.length ); + + for ( int i = 0; i < nodeCount; i++ ) + { + byte flags = buf.readByte(); + int[] children = readVarIntArray( buf ); + int redirectNode = ( ( flags & FLAG_REDIRECT ) != 0 ) ? readVarInt( buf ) : 0; + ArgumentBuilder argumentBuilder; + + switch ( flags & FLAG_TYPE ) + { + case NODE_ROOT: + argumentBuilder = null; + break; + case NODE_LITERAL: + argumentBuilder = LiteralArgumentBuilder.literal( readString( buf ) ); + break; + case NODE_ARGUMENT: + String name = readString( buf ); + String parser = readString( buf ); + + argumentBuilder = RequiredArgumentBuilder.argument( name, ArgumentRegistry.read( parser, buf ) ); + + if ( ( flags & FLAG_SUGGESTIONS ) != 0 ) + { + String suggster = readString( buf ); + ( (RequiredArgumentBuilder) argumentBuilder ).suggests( SuggestionRegistry.getProvider( suggster ) ); + } + break; + default: + throw new IllegalArgumentException( "Unhandled node type " + flags ); + } + + NetworkNode node = new NetworkNode( argumentBuilder, flags, redirectNode, children ); + + nodes[i] = node; + nodeQueue.add( node ); + } + + boolean mustCycle; + do + { + if ( nodeQueue.isEmpty() ) + { + int rootIndex = readVarInt( buf ); + root = (RootCommandNode) nodes[rootIndex].command; + return; + } + + mustCycle = false; + + for ( Iterator iter = nodeQueue.iterator(); iter.hasNext(); ) + { + NetworkNode node = iter.next(); + if ( node.buildSelf( nodes ) ) + { + iter.remove(); + mustCycle = true; + } + } + } while ( mustCycle ); + + throw new IllegalStateException( "Did not finish building root node" ); + } + + @Override + public void write(ByteBuf buf) + { + Map indexMap = new LinkedHashMap<>(); + Deque nodeQueue = new ArrayDeque<>(); + nodeQueue.add( root ); + + while ( !nodeQueue.isEmpty() ) + { + CommandNode command = nodeQueue.pollFirst(); + + if ( !indexMap.containsKey( command ) ) + { + // Index the new node + int currentIndex = indexMap.size(); + indexMap.put( command, currentIndex ); + + // Queue children and redirect for processing + nodeQueue.addAll( command.getChildren() ); + if ( command.getRedirect() != null ) + { + nodeQueue.add( command.getRedirect() ); + } + } + } + + // Write out size + writeVarInt( indexMap.size(), buf ); + + int currentIndex = 0; + for ( Map.Entry entry : indexMap.entrySet() ) + { + // Using a LinkedHashMap, but sanity check this assumption + Preconditions.checkState( entry.getValue() == currentIndex++, "Iteration out of order!" ); + + CommandNode node = entry.getKey(); + byte flags = 0; + + if ( node.getRedirect() != null ) + { + flags |= FLAG_REDIRECT; + } + if ( node.getCommand() != null ) + { + flags |= FLAG_EXECUTABLE; + } + + if ( node instanceof RootCommandNode ) + { + flags |= NODE_ROOT; + } else if ( node instanceof LiteralCommandNode ) + { + flags |= NODE_LITERAL; + } else if ( node instanceof ArgumentCommandNode ) + { + flags |= NODE_ARGUMENT; + if ( ( (ArgumentCommandNode) node ).getCustomSuggestions() != null ) + { + flags |= FLAG_SUGGESTIONS; + } + } else + { + throw new IllegalArgumentException( "Unhandled node type " + node ); + } + + buf.writeByte( flags ); + + writeVarInt( node.getChildren().size(), buf ); + for ( CommandNode child : (Collection) node.getChildren() ) + { + writeVarInt( indexMap.get( child ), buf ); + } + if ( node.getRedirect() != null ) + { + writeVarInt( indexMap.get( node.getRedirect() ), buf ); + } + + if ( node instanceof LiteralCommandNode ) + { + writeString( ( (LiteralCommandNode) node ).getLiteral(), buf ); + } else if ( node instanceof ArgumentCommandNode ) + { + ArgumentCommandNode argumentNode = (ArgumentCommandNode) node; + + writeString( argumentNode.getName(), buf ); + ArgumentRegistry.write( argumentNode.getType(), buf ); + + if ( argumentNode.getCustomSuggestions() != null ) + { + writeString( SuggestionRegistry.getKey( argumentNode.getCustomSuggestions() ), buf ); + } + } + } + + // Get, check, and write the root index (should be first) + int rootIndex = indexMap.get( root ); + Preconditions.checkState( rootIndex == 0, "How did root not land up at index 0?!?" ); + writeVarInt( rootIndex, buf ); + } + + @Override + public void handle(AbstractPacketHandler handler) throws Exception + { + handler.handle( this ); + } + + @Data + private static class NetworkNode + { + + private final ArgumentBuilder argumentBuilder; + private final byte flags; + private final int redirectNode; + private final int[] children; + private CommandNode command; + + private boolean buildSelf(NetworkNode[] otherNodes) + { + // First cycle + if ( command == null ) + { + // Root node is merely the root + if ( argumentBuilder == null ) + { + command = new RootCommandNode(); + } else + { + // Add the redirect + if ( ( flags & FLAG_REDIRECT ) != 0 ) + { + if ( otherNodes[redirectNode].command == null ) + { + return false; + } + + argumentBuilder.redirect( otherNodes[redirectNode].command ); + } + + // Add dummy executable + if ( ( flags & FLAG_EXECUTABLE ) != 0 ) + { + argumentBuilder.executes( new Command() + { + @Override + public int run(CommandContext context) throws CommandSyntaxException + { + return 0; + } + } ); + } + + // Build our self command + command = argumentBuilder.build(); + } + } + + // Check that we have processed all children thus far + for ( int childIndex : children ) + { + if ( otherNodes[childIndex].command == null ) + { + // If not, we have to do another cycle + return false; + } + } + + for ( int childIndex : children ) + { + CommandNode child = otherNodes[childIndex].command; + Preconditions.checkArgument( !( child instanceof RootCommandNode ), "Cannot have RootCommandNode as child" ); + + command.addChild( child ); + } + + return true; + } + } + + @Data + private static class ArgumentRegistry + { + + private static final Map PROVIDERS = new HashMap<>(); + private static final Map, ProperArgumentSerializer> PROPER_PROVIDERS = new HashMap<>(); + // + private static final ArgumentSerializer VOID = new ArgumentSerializer() + { + @Override + protected Void read(ByteBuf buf) + { + return null; + } + + @Override + protected void write(ByteBuf buf, Void t) + { + } + }; + private static final ArgumentSerializer BOOLEAN = new ArgumentSerializer() + { + @Override + protected Boolean read(ByteBuf buf) + { + return buf.readBoolean(); + } + + @Override + protected void write(ByteBuf buf, Boolean t) + { + buf.writeBoolean( t ); + } + }; + private static final ArgumentSerializer BYTE = new ArgumentSerializer() + { + @Override + protected Byte read(ByteBuf buf) + { + return buf.readByte(); + } + + @Override + protected void write(ByteBuf buf, Byte t) + { + buf.writeByte( t ); + } + }; + private static final ArgumentSerializer FLOAT = new ArgumentSerializer() + { + @Override + protected FloatArgumentType read(ByteBuf buf) + { + byte flags = buf.readByte(); + float min = ( flags & 0x1 ) != 0 ? buf.readFloat() : -Float.MAX_VALUE; + float max = ( flags & 0x2 ) != 0 ? buf.readFloat() : -Float.MAX_VALUE; + + return FloatArgumentType.floatArg( min, max ); + } + + @Override + protected void write(ByteBuf buf, FloatArgumentType t) + { + boolean hasMin = t.getMinimum() != -Float.MAX_VALUE; + boolean hasMax = t.getMaximum() != Float.MAX_VALUE; + + buf.writeByte( binaryFlag( hasMin, hasMax ) ); + if ( hasMin ) + { + buf.writeFloat( t.getMinimum() ); + } + if ( hasMax ) + { + buf.writeFloat( t.getMaximum() ); + } + } + }; + private static final ArgumentSerializer DOUBLE = new ArgumentSerializer() + { + @Override + protected DoubleArgumentType read(ByteBuf buf) + { + byte flags = buf.readByte(); + double min = ( flags & 0x1 ) != 0 ? buf.readDouble() : -Double.MAX_VALUE; + double max = ( flags & 0x2 ) != 0 ? buf.readDouble() : -Double.MAX_VALUE; + + return DoubleArgumentType.doubleArg( min, max ); + } + + @Override + protected void write(ByteBuf buf, DoubleArgumentType t) + { + boolean hasMin = t.getMinimum() != -Double.MAX_VALUE; + boolean hasMax = t.getMaximum() != Double.MAX_VALUE; + + buf.writeByte( binaryFlag( hasMin, hasMax ) ); + if ( hasMin ) + { + buf.writeDouble( t.getMinimum() ); + } + if ( hasMax ) + { + buf.writeDouble( t.getMaximum() ); + } + } + }; + private static final ArgumentSerializer INTEGER = new ArgumentSerializer() + { + @Override + protected IntegerArgumentType read(ByteBuf buf) + { + byte flags = buf.readByte(); + int min = ( flags & 0x1 ) != 0 ? buf.readInt() : Integer.MIN_VALUE; + int max = ( flags & 0x2 ) != 0 ? buf.readInt() : Integer.MAX_VALUE; + + return IntegerArgumentType.integer( min, max ); + } + + @Override + protected void write(ByteBuf buf, IntegerArgumentType t) + { + boolean hasMin = t.getMinimum() != Integer.MIN_VALUE; + boolean hasMax = t.getMaximum() != Integer.MAX_VALUE; + + buf.writeByte( binaryFlag( hasMin, hasMax ) ); + if ( hasMin ) + { + buf.writeInt( t.getMinimum() ); + } + if ( hasMax ) + { + buf.writeInt( t.getMaximum() ); + } + } + }; + private static final ProperArgumentSerializer STRING = new ProperArgumentSerializer() + { + @Override + protected StringArgumentType read(ByteBuf buf) + { + int val = readVarInt( buf ); + switch ( val ) + { + case 0: + return StringArgumentType.word(); + case 1: + return StringArgumentType.string(); + case 2: + return StringArgumentType.greedyString(); + default: + throw new IllegalArgumentException( "Unknown string type " + val ); + } + } + + @Override + protected void write(ByteBuf buf, StringArgumentType t) + { + writeVarInt( t.getType().ordinal(), buf ); + } + + @Override + protected String getKey() + { + return "brigadier:string"; + } + }; + + static + { + PROVIDERS.put( "brigadier:bool", VOID ); + PROVIDERS.put( "brigadier:float", FLOAT ); + PROVIDERS.put( "brigadier:double", DOUBLE ); + PROVIDERS.put( "brigadier:integer", INTEGER ); + + PROVIDERS.put( "brigadier:string", STRING ); + PROPER_PROVIDERS.put( StringArgumentType.class, STRING ); + + PROVIDERS.put( "minecraft:entity", BYTE ); + PROVIDERS.put( "minecraft:game_profile", VOID ); + PROVIDERS.put( "minecraft:block_pos", VOID ); + PROVIDERS.put( "minecraft:column_pos", VOID ); + PROVIDERS.put( "minecraft:vec3", VOID ); + PROVIDERS.put( "minecraft:vec2", VOID ); + PROVIDERS.put( "minecraft:block_state", VOID ); + PROVIDERS.put( "minecraft:block_predicate", VOID ); + PROVIDERS.put( "minecraft:item_stack", VOID ); + PROVIDERS.put( "minecraft:item_predicate", VOID ); + PROVIDERS.put( "minecraft:color", VOID ); + PROVIDERS.put( "minecraft:component", VOID ); + PROVIDERS.put( "minecraft:message", VOID ); + PROVIDERS.put( "minecraft:nbt", VOID ); + PROVIDERS.put( "minecraft:nbt_path", VOID ); + PROVIDERS.put( "minecraft:objective", VOID ); + PROVIDERS.put( "minecraft:objective_criteria", VOID ); + PROVIDERS.put( "minecraft:operation", VOID ); + PROVIDERS.put( "minecraft:particle", VOID ); + PROVIDERS.put( "minecraft:rotation", VOID ); + PROVIDERS.put( "minecraft:scoreboard_slot", VOID ); + PROVIDERS.put( "minecraft:score_holder", BYTE ); + PROVIDERS.put( "minecraft:swizzle", VOID ); + PROVIDERS.put( "minecraft:team", VOID ); + PROVIDERS.put( "minecraft:item_slot", VOID ); + PROVIDERS.put( "minecraft:resource_location", VOID ); + PROVIDERS.put( "minecraft:mob_effect", VOID ); + PROVIDERS.put( "minecraft:function", VOID ); + PROVIDERS.put( "minecraft:entity_anchor", VOID ); + PROVIDERS.put( "minecraft:int_range", VOID ); + PROVIDERS.put( "minecraft:float_range", VOID ); + PROVIDERS.put( "minecraft:item_enchantment", VOID ); + PROVIDERS.put( "minecraft:entity_summon", VOID ); + PROVIDERS.put( "minecraft:dimension", VOID ); + } + + private static ArgumentType read(String key, ByteBuf buf) + { + ArgumentSerializer reader = PROVIDERS.get( key ); + Preconditions.checkArgument( reader != null, "No provider for argument " + key ); + + Object val = reader.read( buf ); + return val != null && PROPER_PROVIDERS.containsKey( val.getClass() ) ? (ArgumentType) val : new DummyType( key, reader, val ); + } + + private static void write(ArgumentType arg, ByteBuf buf) + { + ProperArgumentSerializer proper = PROPER_PROVIDERS.get( arg.getClass() ); + if ( proper != null ) + { + writeString( proper.getKey(), buf ); + proper.write( buf, arg ); + } else + { + Preconditions.checkArgument( arg instanceof DummyType, "Non dummy arg " + arg.getClass() ); + + DummyType dummy = (DummyType) arg; + writeString( dummy.key, buf ); + dummy.serializer.write( buf, dummy.value ); + } + } + + @Data + private static class DummyType implements ArgumentType + { + + private final String key; + private final ArgumentSerializer serializer; + private final T value; + + @Override + public T parse(StringReader reader) throws CommandSyntaxException + { + throw new UnsupportedOperationException( "Not supported." ); + } + } + + private static abstract class ArgumentSerializer + { + + protected abstract T read(ByteBuf buf); + + protected abstract void write(ByteBuf buf, T t); + } + + private static abstract class ProperArgumentSerializer extends ArgumentSerializer + { + + protected abstract String getKey(); + } + } + + @Data + public static class SuggestionRegistry + { + + public static final SuggestionProvider ASK_SERVER = new DummyProvider( "minecraft:ask_server" ); + private static final Map> PROVIDERS = new HashMap<>(); + + static + { + PROVIDERS.put( "minecraft:ask_server", ASK_SERVER ); + registerDummy( "minecraft:all_recipes" ); + registerDummy( "minecraft:available_sounds" ); + registerDummy( "minecraft:summonable_entities" ); + } + + private static void registerDummy(String name) + { + PROVIDERS.put( name, new DummyProvider( name ) ); + } + + private static SuggestionProvider getProvider(String key) + { + SuggestionProvider provider = PROVIDERS.get( key ); + Preconditions.checkArgument( provider != null, "Unknown completion provider " + key ); + + return provider; + } + + private static String getKey(SuggestionProvider provider) + { + Preconditions.checkArgument( provider instanceof DummyProvider, "Non dummy provider " + provider ); + + return ( (DummyProvider) provider ).key; + } + + @Data + private static final class DummyProvider implements SuggestionProvider + { + + private final String key; + + @Override + public CompletableFuture getSuggestions(CommandContext context, SuggestionsBuilder builder) throws CommandSyntaxException + { + return builder.buildFuture(); + } + } + } + + private static byte binaryFlag(boolean first, boolean second) + { + byte ret = 0; + + if ( first ) + { + ret = (byte) ( ret | 0x1 ); + } + if ( second ) + { + ret = (byte) ( ret | 0x2 ); + } + + return ret; + } +} diff --git a/protocol/src/main/java/net/md_5/bungee/protocol/packet/TabCompleteResponse.java b/protocol/src/main/java/net/md_5/bungee/protocol/packet/TabCompleteResponse.java index e99ceb54..64851dde 100644 --- a/protocol/src/main/java/net/md_5/bungee/protocol/packet/TabCompleteResponse.java +++ b/protocol/src/main/java/net/md_5/bungee/protocol/packet/TabCompleteResponse.java @@ -1,10 +1,13 @@ package net.md_5.bungee.protocol.packet; +import com.mojang.brigadier.LiteralMessage; +import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.Suggestions; import net.md_5.bungee.protocol.DefinedPacket; import io.netty.buffer.ByteBuf; import java.util.LinkedList; import java.util.List; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -18,17 +21,14 @@ public class TabCompleteResponse extends DefinedPacket { private int transactionId; - private int start; - private int length; - private List matches; + private Suggestions suggestions; + // private List commands; - public TabCompleteResponse(int transactionId, int start, int length, List matches) + public TabCompleteResponse(int transactionId, Suggestions suggestions) { this.transactionId = transactionId; - this.start = start; - this.length = length; - this.matches = matches; + this.suggestions = suggestions; } public TabCompleteResponse(List commands) @@ -42,18 +42,21 @@ public class TabCompleteResponse extends DefinedPacket if ( protocolVersion >= ProtocolConstants.MINECRAFT_1_13 ) { transactionId = readVarInt( buf ); - start = readVarInt( buf ); - length = readVarInt( buf ); + int start = readVarInt( buf ); + int length = readVarInt( buf ); + StringRange range = StringRange.between( start, start + length ); int cnt = readVarInt( buf ); - matches = new LinkedList<>(); + List matches = new LinkedList<>(); for ( int i = 0; i < cnt; i++ ) { String match = readString( buf ); String tooltip = buf.readBoolean() ? readString( buf ) : null; - matches.add( new CommandMatch( match, tooltip ) ); + matches.add( new Suggestion( range, match, new LiteralMessage( tooltip ) ) ); } + + suggestions = new Suggestions( range, matches ); } if ( protocolVersion < ProtocolConstants.MINECRAFT_1_13 ) @@ -68,15 +71,18 @@ public class TabCompleteResponse extends DefinedPacket if ( protocolVersion >= ProtocolConstants.MINECRAFT_1_13 ) { writeVarInt( transactionId, buf ); - writeVarInt( start, buf ); - writeVarInt( length, buf ); + writeVarInt( suggestions.getRange().getStart(), buf ); + writeVarInt( suggestions.getRange().getLength(), buf ); - writeVarInt( matches.size(), buf ); - for ( CommandMatch match : matches ) + writeVarInt( suggestions.getList().size(), buf ); + for ( Suggestion suggestion : suggestions.getList() ) { - writeString( match.match, buf ); - buf.writeBoolean( match.tooltip != null ); - writeString( match.tooltip, buf ); + writeString( suggestion.getText(), buf ); + buf.writeBoolean( suggestion.getTooltip() != null ); + if ( suggestion.getTooltip() != null ) + { + writeString( suggestion.getTooltip().getString(), buf ); + } } } @@ -91,14 +97,4 @@ public class TabCompleteResponse extends DefinedPacket { handler.handle( this ); } - - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class CommandMatch - { - - private String match; - private String tooltip; - } } diff --git a/proxy/src/main/java/net/md_5/bungee/conf/Configuration.java b/proxy/src/main/java/net/md_5/bungee/conf/Configuration.java index d7879d10..36cac069 100644 --- a/proxy/src/main/java/net/md_5/bungee/conf/Configuration.java +++ b/proxy/src/main/java/net/md_5/bungee/conf/Configuration.java @@ -60,6 +60,7 @@ public class Configuration implements ProxyConfig private int compressionThreshold = 256; private boolean preventProxyConnections; private boolean forgeSupport; + private boolean injectCommands; public void load() { @@ -90,6 +91,11 @@ public class Configuration implements ProxyConfig compressionThreshold = adapter.getInt( "network_compression_threshold", compressionThreshold ); preventProxyConnections = adapter.getBoolean( "prevent_proxy_connections", preventProxyConnections ); forgeSupport = adapter.getBoolean( "forge_support", forgeSupport ); + injectCommands = adapter.getBoolean( "inject_commands", injectCommands ); + if ( injectCommands ) + { + System.setProperty( "net.md-5.bungee.protocol.register_commands", "true" ); + } disabledCommands = new CaseInsensitiveSet( (Collection) adapter.getList( "disabled_commands", Arrays.asList( "disabledcommandhere" ) ) ); diff --git a/proxy/src/main/java/net/md_5/bungee/connection/DownstreamBridge.java b/proxy/src/main/java/net/md_5/bungee/connection/DownstreamBridge.java index 95a6f39f..2248a685 100644 --- a/proxy/src/main/java/net/md_5/bungee/connection/DownstreamBridge.java +++ b/proxy/src/main/java/net/md_5/bungee/connection/DownstreamBridge.java @@ -3,11 +3,17 @@ package net.md_5.bungee.connection; import com.google.common.base.Preconditions; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.tree.LiteralCommandNode; import java.io.DataInput; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import lombok.RequiredArgsConstructor; +import net.md_5.bungee.BungeeCord; import net.md_5.bungee.ServerConnection; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.event.ServerDisconnectEvent; @@ -32,6 +38,7 @@ import net.md_5.bungee.protocol.DefinedPacket; import net.md_5.bungee.protocol.PacketWrapper; import net.md_5.bungee.protocol.ProtocolConstants; import net.md_5.bungee.protocol.packet.BossBar; +import net.md_5.bungee.protocol.packet.Commands; import net.md_5.bungee.protocol.packet.KeepAlive; import net.md_5.bungee.protocol.packet.PlayerListItem; import net.md_5.bungee.protocol.packet.Respawn; @@ -526,6 +533,35 @@ public class DownstreamBridge extends PacketHandler con.setDimension( respawn.getDimension() ); } + @Override + public void handle(Commands commands) throws Exception + { + boolean modified = false; + + if ( BungeeCord.getInstance().config.isInjectCommands() ) + { + for ( String command : bungee.getPluginManager().getCommands() ) + { + if ( commands.getRoot().getChild( command ) == null ) + { + LiteralCommandNode dummy = LiteralArgumentBuilder.literal( command ) + .then( RequiredArgumentBuilder.argument( "args", StringArgumentType.greedyString() ) + .suggests( Commands.SuggestionRegistry.ASK_SERVER ) ) + .build(); + commands.getRoot().addChild( dummy ); + + modified = true; + } + } + } + + if ( modified ) + { + con.unsafe().sendPacket( commands ); + throw CancelSendSignal.INSTANCE; + } + } + @Override public String toString() { diff --git a/proxy/src/main/java/net/md_5/bungee/connection/UpstreamBridge.java b/proxy/src/main/java/net/md_5/bungee/connection/UpstreamBridge.java index 9666e7a2..83d7630e 100644 --- a/proxy/src/main/java/net/md_5/bungee/connection/UpstreamBridge.java +++ b/proxy/src/main/java/net/md_5/bungee/connection/UpstreamBridge.java @@ -1,8 +1,12 @@ package net.md_5.bungee.connection; import com.google.common.base.Preconditions; +import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.Suggestions; import io.netty.channel.Channel; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import net.md_5.bungee.BungeeCord; import net.md_5.bungee.UserConnection; @@ -172,6 +176,19 @@ public class UpstreamBridge extends PacketHandler if ( con.getPendingConnection().getVersion() < ProtocolConstants.MINECRAFT_1_13 ) { con.unsafe().sendPacket( new TabCompleteResponse( results ) ); + } else if ( BungeeCord.getInstance().config.isInjectCommands() ) + { + int start = tabComplete.getCursor().lastIndexOf( ' ' ) + 1; + int end = tabComplete.getCursor().length(); + StringRange range = StringRange.between( start, end ); + + List brigadier = new LinkedList<>(); + for ( String s : results ) + { + brigadier.add( new Suggestion( range, s ) ); + } + + con.unsafe().sendPacket( new TabCompleteResponse( tabComplete.getTransactionId(), new Suggestions( range, brigadier ) ) ); } throw CancelSendSignal.INSTANCE; }