diff --git a/api/src/main/java/net/md_5/bungee/api/event/CommandsDeclareEvent.java b/api/src/main/java/net/md_5/bungee/api/event/CommandsDeclareEvent.java new file mode 100644 index 00000000..ab96dd9b --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/event/CommandsDeclareEvent.java @@ -0,0 +1,157 @@ +package net.md_5.bungee.api.event; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.RootCommandNode; +import lombok.AccessLevel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Setter; +import lombok.ToString; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.connection.Connection; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.plugin.PluginManager; +import net.md_5.bungee.api.plugin.TabExecutor; + +/** + * Event called when a downstream server (on 1.13+) sends the command structure + * to a player, but before BungeeCord adds the dummy command nodes of + * registered commands. + *

+ * BungeeCord will not overwrite the modifications made by the listeners. + * + *

Usage example

+ * Here is a usage example of this event, to declare a command structure. + * This illustrates the commands /server and /send of Bungee. + *
+ * event.getRoot().addChild( LiteralArgumentBuilder.<CommandSender>literal( "server" )
+ *         .requires( sender -> sender.hasPermission( "bungeecord.command.server" ) )
+ *         .executes( a -> 0 )
+ *         .then( RequiredArgumentBuilder.argument( "serverName", StringArgumentType.greedyString() )
+ *                 .suggests( SuggestionRegistry.ASK_SERVER )
+ *         )
+ *         .build()
+ * );
+ * event.getRoot().addChild( LiteralArgumentBuilder.<CommandSender>literal( "send" )
+ *         .requires( sender -> sender.hasPermission( "bungeecord.command.send" ) )
+ *         .then( RequiredArgumentBuilder.argument( "playerName", StringArgumentType.word() )
+ *                 .suggests( SuggestionRegistry.ASK_SERVER )
+ *                 .then( RequiredArgumentBuilder.argument( "serverName", StringArgumentType.greedyString() )
+ *                         .suggests( SuggestionRegistry.ASK_SERVER )
+ *                 )
+ *         )
+ *         .build()
+ * );
+ * 
+ * + *

Flag a {@link CommandNode} as executable or not

+ * The implementation of a {@link com.mojang.brigadier.Command Command} used in + * {@link ArgumentBuilder#executes(com.mojang.brigadier.Command)} will never be + * executed. This will only tell to the client if the current node is + * executable or not. + * + * + *

{@link CommandNode}’s suggestions management

+ * The implementation of a SuggestionProvider used in + * {@link RequiredArgumentBuilder#suggests(SuggestionProvider)} will never be + * executed. This will only tell to the client how to deal with the + * auto-completion of the argument. + * + * + *

Argument types

+ * When building a new argument command node using + * {@link RequiredArgumentBuilder#argument(String, ArgumentType)}, you have to + * specify an {@link ArgumentType}. You can use all subclasses of + * {@link ArgumentType} provided with brigadier (for instance, + * {@link StringArgumentType} or {@link IntegerArgumentType}), or call any + * {@code ArgumentRegistry.minecraft*()} methods to use a {@code minecraft:*} + * argument type. + * + *

Limitations with brigadier API

+ * This event is only used for the client to show command syntax, suggest + * sub-commands and color the arguments in the chat box. The command execution + * needs to be implemented using {@link PluginManager#registerCommand(Plugin, + * Command)} and the server-side tab-completion using {@link TabCompleteEvent} + * or {@link TabExecutor}. + */ +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class CommandsDeclareEvent extends TargetedEvent +{ + /** + * Wether or not the command tree is modified by this event. + * + * If this value is set to true, BungeeCord will ensure that the + * modifications made in the command tree, will be sent to the player. + * If this is false, the modifications may not be taken into account. + * + * When calling {@link #getRoot()}, this value is automatically set + * to true. + */ + @Setter(value = AccessLevel.NONE) + private boolean modified = false; + + /** + * The root command node of the command structure that will be send to the + * player. + */ + private final RootCommandNode root; + + public CommandsDeclareEvent(Connection sender, Connection receiver, RootCommandNode root) + { + super( sender, receiver ); + this.root = root; + } + + /** + * The root command node of the command structure that will be send to the + * player. + * @return The root command node + */ + public RootCommandNode getRoot() + { + modified = true; + return root; + } +} 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 index 26edbca8..534a36da 100644 --- 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 @@ -4,6 +4,7 @@ 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.BoolArgumentType; import com.mojang.brigadier.arguments.DoubleArgumentType; import com.mojang.brigadier.arguments.FloatArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType; @@ -36,6 +37,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import net.md_5.bungee.protocol.AbstractPacketHandler; import net.md_5.bungee.protocol.DefinedPacket; +import net.md_5.bungee.protocol.ProtocolConstants; @Data @NoArgsConstructor @@ -126,7 +128,7 @@ public class Commands extends DefinedPacket } @Override - public void write(ByteBuf buf) + public void write(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { Map indexMap = new LinkedHashMap<>(); Deque nodeQueue = new ArrayDeque<>(); @@ -210,7 +212,7 @@ public class Commands extends DefinedPacket ArgumentCommandNode argumentNode = (ArgumentCommandNode) node; writeString( argumentNode.getName(), buf ); - ArgumentRegistry.write( argumentNode.getType(), buf ); + ArgumentRegistry.write( argumentNode.getType(), buf, protocolVersion ); if ( argumentNode.getCustomSuggestions() != null ) { @@ -304,11 +306,12 @@ public class Commands extends DefinedPacket } @Data - private static class ArgumentRegistry + public static class ArgumentRegistry { private static final Map PROVIDERS = new HashMap<>(); private static final Map, ProperArgumentSerializer> PROPER_PROVIDERS = new HashMap<>(); + private static final Map CONVERSION_PROVIDERS = new HashMap<>(); // private static final ArgumentSerializer VOID = new ArgumentSerializer() { @@ -323,18 +326,23 @@ public class Commands extends DefinedPacket { } }; - private static final ArgumentSerializer BOOLEAN = new ArgumentSerializer() + private static final ProperArgumentSerializer BOOLEAN = new ProperArgumentSerializer() { @Override - protected Boolean read(ByteBuf buf) + protected BoolArgumentType read(ByteBuf buf) { - return buf.readBoolean(); + return BoolArgumentType.bool(); } @Override - protected void write(ByteBuf buf, Boolean t) + protected void write(ByteBuf buf, BoolArgumentType t) { - buf.writeBoolean( t ); + } + + @Override + protected String getKey() + { + return "brigadier:bool"; } }; private static final ArgumentSerializer BYTE = new ArgumentSerializer() @@ -351,7 +359,7 @@ public class Commands extends DefinedPacket buf.writeByte( t ); } }; - private static final ArgumentSerializer FLOAT = new ArgumentSerializer() + private static final ProperArgumentSerializer FLOAT = new ProperArgumentSerializer() { @Override protected FloatArgumentType read(ByteBuf buf) @@ -379,8 +387,14 @@ public class Commands extends DefinedPacket buf.writeFloat( t.getMaximum() ); } } + + @Override + protected String getKey() + { + return "brigadier:float"; + } }; - private static final ArgumentSerializer DOUBLE = new ArgumentSerializer() + private static final ProperArgumentSerializer DOUBLE = new ProperArgumentSerializer() { @Override protected DoubleArgumentType read(ByteBuf buf) @@ -408,8 +422,14 @@ public class Commands extends DefinedPacket buf.writeDouble( t.getMaximum() ); } } + + @Override + protected String getKey() + { + return "brigadier:double"; + } }; - private static final ArgumentSerializer INTEGER = new ArgumentSerializer() + private static final ProperArgumentSerializer INTEGER = new ProperArgumentSerializer() { @Override protected IntegerArgumentType read(ByteBuf buf) @@ -437,8 +457,14 @@ public class Commands extends DefinedPacket buf.writeInt( t.getMaximum() ); } } + + @Override + protected String getKey() + { + return "brigadier:integer"; + } }; - private static final ArgumentSerializer LONG = new ArgumentSerializer() + private static final ProperArgumentSerializer LONG = new ProperArgumentSerializer() { @Override protected LongArgumentType read(ByteBuf buf) @@ -466,6 +492,12 @@ public class Commands extends DefinedPacket buf.writeLong( t.getMaximum() ); } } + + @Override + protected String getKey() + { + return "brigadier:long"; + } }; private static final ProperArgumentSerializer STRING = new ProperArgumentSerializer() { @@ -501,11 +533,31 @@ public class Commands extends DefinedPacket static { - PROVIDERS.put( "brigadier:bool", VOID ); + PROVIDERS.put( "brigadier:bool", BOOLEAN ); + PROPER_PROVIDERS.put( BoolArgumentType.class, BOOLEAN ); + PROVIDERS.put( "brigadier:float", FLOAT ); + PROPER_PROVIDERS.put( FloatArgumentType.class, FLOAT ); + PROVIDERS.put( "brigadier:double", DOUBLE ); + PROPER_PROVIDERS.put( DoubleArgumentType.class, DOUBLE ); + PROVIDERS.put( "brigadier:integer", INTEGER ); - PROVIDERS.put( "brigadier:long", LONG ); + PROPER_PROVIDERS.put( IntegerArgumentType.class, INTEGER ); + + PROVIDERS.put( "brigadier:long", LONG ); // 1.14+ + PROPER_PROVIDERS.put( LongArgumentType.class, LONG ); + CONVERSION_PROVIDERS.put( "brigadier:long", ( originalType, protocolVersion ) -> + { + if ( protocolVersion < ProtocolConstants.MINECRAFT_1_14 ) + { + LongArgumentType type = (LongArgumentType) originalType; + int min = (int) Math.max( Integer.MIN_VALUE, Math.min( Integer.MAX_VALUE, type.getMinimum() ) ); + int max = (int) Math.max( Integer.MIN_VALUE, Math.min( Integer.MAX_VALUE, type.getMaximum() ) ); + return IntegerArgumentType.integer( min, max ); + } + return originalType; + } ); PROVIDERS.put( "brigadier:string", STRING ); PROPER_PROVIDERS.put( StringArgumentType.class, STRING ); @@ -523,9 +575,37 @@ public class Commands extends DefinedPacket PROVIDERS.put( "minecraft:color", VOID ); PROVIDERS.put( "minecraft:component", VOID ); PROVIDERS.put( "minecraft:message", VOID ); - PROVIDERS.put( "minecraft:nbt_compound_tag", VOID ); // 1.14 - PROVIDERS.put( "minecraft:nbt_tag", VOID ); // 1.14 + + PROVIDERS.put( "minecraft:nbt_compound_tag", VOID ); // 1.14+, replaces minecraft:nbt + CONVERSION_PROVIDERS.put( "minecraft:nbt_compound_tag", ( originalType, protocolVersion ) -> + { + if ( protocolVersion < ProtocolConstants.MINECRAFT_1_14 ) + { + return minecraftNBT(); + } + return originalType; + } ); + + PROVIDERS.put( "minecraft:nbt_tag", VOID ); // 1.14+ + CONVERSION_PROVIDERS.put( "minecraft:nbt_tag", ( originalType, protocolVersion ) -> + { + if ( protocolVersion < ProtocolConstants.MINECRAFT_1_14 ) + { + return minecraftNBT(); + } + return originalType; + } ); + PROVIDERS.put( "minecraft:nbt", VOID ); // 1.13 + CONVERSION_PROVIDERS.put( "minecraft:nbt", ( originalType, protocolVersion ) -> + { + if ( protocolVersion >= ProtocolConstants.MINECRAFT_1_14 ) + { + return minecraftNBTCompoundTag(); + } + return originalType; + } ); + PROVIDERS.put( "minecraft:nbt_path", VOID ); PROVIDERS.put( "minecraft:objective", VOID ); PROVIDERS.put( "minecraft:objective_criteria", VOID ); @@ -546,11 +626,417 @@ public class Commands extends DefinedPacket PROVIDERS.put( "minecraft:item_enchantment", VOID ); PROVIDERS.put( "minecraft:entity_summon", VOID ); PROVIDERS.put( "minecraft:dimension", VOID ); - PROVIDERS.put( "minecraft:time", VOID ); // 1.14 - PROVIDERS.put( "minecraft:uuid", VOID ); // 1.16 - PROVIDERS.put( "minecraft:test_argument", VOID ); // 1.16, debug - PROVIDERS.put( "minecraft:test_class", VOID ); // 1.16, debug - PROVIDERS.put( "minecraft:angle", VOID ); // 1.16.2 + + PROVIDERS.put( "minecraft:time", VOID ); // 1.14+ + CONVERSION_PROVIDERS.put( "minecraft:time", ( originalType, protocolVersion ) -> + { + if ( protocolVersion < ProtocolConstants.MINECRAFT_1_14 ) + { + return StringArgumentType.word(); + } + return originalType; + } ); + + PROVIDERS.put( "minecraft:uuid", VOID ); // 1.16+ + CONVERSION_PROVIDERS.put( "minecraft:uuid", ( originalType, protocolVersion ) -> + { + if ( protocolVersion < ProtocolConstants.MINECRAFT_1_16 ) + { + return StringArgumentType.word(); + } + return originalType; + } ); + + PROVIDERS.put( "minecraft:test_argument", VOID ); // 1.16+, debug + PROVIDERS.put( "minecraft:test_class", VOID ); // 1.16+, debug + + PROVIDERS.put( "minecraft:angle", VOID ); // 1.16.2+ + CONVERSION_PROVIDERS.put( "minecraft:angle", ( originalType, protocolVersion ) -> + { + if ( protocolVersion < ProtocolConstants.MINECRAFT_1_16_2 ) + { + return FloatArgumentType.floatArg(); + } + return originalType; + } ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:entity}. + * @param singleEntity if the argument restrict to only one entity + * @param onlyPlayers if the argument restrict to players only + * @return an ArgumentType instance + */ + public static ArgumentType minecraftEntity(boolean singleEntity, boolean onlyPlayers) + { + byte flags = 0; + if ( singleEntity ) + { + flags |= 1; + } + if ( onlyPlayers ) + { + flags |= 2; + } + + return minecraftArgumentType( "minecraft:entity", flags ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:game_profile}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftGameProfile() + { + return minecraftArgumentType( "minecraft:game_profile", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:block_pos}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftBlockPos() + { + return minecraftArgumentType( "minecraft:block_pos", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:column_pos}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftColumnPos() + { + return minecraftArgumentType( "minecraft:column_pos", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:vec3}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftVec3() + { + return minecraftArgumentType( "minecraft:vec3", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:vec2}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftVec2() + { + return minecraftArgumentType( "minecraft:vec2", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:block_state}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftBlockState() + { + return minecraftArgumentType( "minecraft:block_state", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:block_predicate}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftBlockPredicate() + { + return minecraftArgumentType( "minecraft:block_predicate", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:item_stack}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftItemStack() + { + return minecraftArgumentType( "minecraft:item_stack", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:item_predicate}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftItemPredicate() + { + return minecraftArgumentType( "minecraft:item_predicate", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:color}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftColor() + { + return minecraftArgumentType( "minecraft:color", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:component}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftComponent() + { + return minecraftArgumentType( "minecraft:component", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:message}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftMessage() + { + return minecraftArgumentType( "minecraft:message", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:nbt_compound_tag}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftNBTCompoundTag() + { + return minecraftArgumentType( "minecraft:nbt_compound_tag", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:nbt_tag}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftNBTTag() + { + return minecraftArgumentType( "minecraft:nbt_tag", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:nbt}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftNBT() + { + return minecraftArgumentType( "minecraft:nbt", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:nbt_path}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftNBTPath() + { + return minecraftArgumentType( "minecraft:nbt_path", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:objective}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftObjective() + { + return minecraftArgumentType( "minecraft:objective", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:objective_criteria}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftObjectiveCriteria() + { + return minecraftArgumentType( "minecraft:objective_criteria", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:operation}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftOperation() + { + return minecraftArgumentType( "minecraft:operation", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:particle}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftParticle() + { + return minecraftArgumentType( "minecraft:particle", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:rotation}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftRotation() + { + return minecraftArgumentType( "minecraft:rotation", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:scoreboard_slot}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftScoreboardSlot() + { + return minecraftArgumentType( "minecraft:scoreboard_slot", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:score_holder}. + * @param allowMultiple if the argument allows multiple entities + * @return an ArgumentType instance + */ + public static ArgumentType minecraftScoreHolder(boolean allowMultiple) + { + byte flags = 0; + if ( allowMultiple ) + { + flags |= 1; + } + + return minecraftArgumentType( "minecraft:score_holder", flags ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:swizzle}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftSwizzle() + { + return minecraftArgumentType( "minecraft:swizzle", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:team}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftTeam() + { + return minecraftArgumentType( "minecraft:team", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:item_slot}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftItemSlot() + { + return minecraftArgumentType( "minecraft:item_slot", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:resource_location}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftResourceLocation() + { + return minecraftArgumentType( "minecraft:resource_location", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:mob_effect}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftMobEffect() + { + return minecraftArgumentType( "minecraft:mob_effect", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:function}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftFunction() + { + return minecraftArgumentType( "minecraft:function", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:entity_anchor}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftEntityAnchor() + { + return minecraftArgumentType( "minecraft:entity_anchor", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:int_range}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftIntRange() + { + return minecraftArgumentType( "minecraft:int_range", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:float_range}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftFloatRange() + { + return minecraftArgumentType( "minecraft:float_range", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:item_enchantment}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftItemEnchantment() + { + return minecraftArgumentType( "minecraft:item_enchantment", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:entity_summon}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftEntitySummon() + { + return minecraftArgumentType( "minecraft:entity_summon", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:dimension}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftDimension() + { + return minecraftArgumentType( "minecraft:dimension", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:time}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftTime() + { + return minecraftArgumentType( "minecraft:time", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:uuid}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftUUID() + { + return minecraftArgumentType( "minecraft:uuid", null ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:angle}. + * @return an ArgumentType instance + */ + public static ArgumentType minecraftAngle() + { + return minecraftArgumentType( "minecraft:angle", null ); + } + + private static ArgumentType minecraftArgumentType(String key, Object rawValue) + { + ArgumentSerializer reader = PROVIDERS.get( key ); + Preconditions.checkArgument( reader != null, "No provider for argument " + key ); + + return new DummyType( key, reader, rawValue ); } private static ArgumentType read(String key, ByteBuf buf) @@ -562,8 +1048,10 @@ public class Commands extends DefinedPacket return val != null && PROPER_PROVIDERS.containsKey( val.getClass() ) ? (ArgumentType) val : new DummyType( key, reader, val ); } - private static void write(ArgumentType arg, ByteBuf buf) + private static void write(ArgumentType arg, ByteBuf buf, int protocolVersion) { + arg = convertToVersion( arg, protocolVersion ); + ProperArgumentSerializer proper = PROPER_PROVIDERS.get( arg.getClass() ); if ( proper != null ) { @@ -579,6 +1067,34 @@ public class Commands extends DefinedPacket } } + // Convert the provided argument type to a new one compatible with the provided protocol version + private static ArgumentType convertToVersion(ArgumentType arg, int protocolVersion) + { + ProperArgumentSerializer proper = PROPER_PROVIDERS.get( arg.getClass() ); + String key; + if ( proper != null ) + { + key = proper.getKey(); + } else + { + Preconditions.checkArgument( arg instanceof DummyType, "Non dummy arg " + arg.getClass() ); + key = ( (DummyType) arg ).key; + } + + ProtocolConversionProvider converter = CONVERSION_PROVIDERS.get( key ); + + if ( converter != null ) + { + return converter.convert( arg, protocolVersion ); + } + return arg; + } + + private interface ProtocolConversionProvider + { + public ArgumentType convert(ArgumentType originalType, int protocolVersion); + } + @Data private static class DummyType implements ArgumentType { @@ -612,17 +1128,39 @@ public class Commands extends DefinedPacket @Data public static class SuggestionRegistry { - + /** + * Tells the client to ask suggestions to the server. + */ public static final SuggestionProvider ASK_SERVER = new DummyProvider( "minecraft:ask_server" ); + + /** + * Tells the client to suggest all the available recipes. The suggestions are stored client side. + */ + public static final SuggestionProvider ALL_RECIPES = new DummyProvider( "minecraft:all_recipes" ); + + /** + * Tells the client to suggest all the available sounds. The suggestions are stored client side. + */ + public static final SuggestionProvider AVAILABLE_SOUNDS = new DummyProvider( "minecraft:available_sounds" ); + + /** + * Tells the client to suggest all the available biomes. The suggestions are stored client side. + */ + public static final SuggestionProvider AVAILABLE_BIOMES = new DummyProvider( "minecraft:available_biomes" ); + + /** + * Tells the client to suggest all the available entities. The suggestions are stored client side. + */ + public static final SuggestionProvider SUMMONABLE_ENTITIES = new DummyProvider( "minecraft:summonable_entities" ); 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:available_biomes" ); - registerDummy( "minecraft:summonable_entities" ); + PROVIDERS.put( "minecraft:all_recipes", ALL_RECIPES ); + PROVIDERS.put( "minecraft:available_sounds", AVAILABLE_SOUNDS ); + PROVIDERS.put( "minecraft:available_biomes", AVAILABLE_BIOMES ); + PROVIDERS.put( "minecraft:summonable_entities", SUMMONABLE_ENTITIES ); } private static void registerDummy(String name) @@ -640,9 +1178,13 @@ public class Commands extends DefinedPacket private static String getKey(SuggestionProvider provider) { - Preconditions.checkArgument( provider instanceof DummyProvider, "Non dummy provider " + provider ); + Preconditions.checkNotNull( provider ); + if ( provider instanceof DummyProvider ) + { + return ( (DummyProvider) provider ).key; + } - return ( (DummyProvider) provider ).key; + return ( (DummyProvider) ASK_SERVER ).key; } @Data 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 4c03bfb2..7c964232 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 @@ -6,12 +6,15 @@ import com.google.common.collect.Lists; import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; 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.StringRange; import com.mojang.brigadier.suggestion.Suggestion; import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.RootCommandNode; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; @@ -19,8 +22,11 @@ import io.netty.channel.unix.DomainSocketAddress; import java.io.DataInput; import java.net.InetSocketAddress; import java.util.ArrayList; +import java.util.Collection; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.logging.Level; import lombok.RequiredArgsConstructor; import net.md_5.bungee.ServerConnection; import net.md_5.bungee.ServerConnection.KeepAliveData; @@ -31,6 +37,7 @@ import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.CommandsDeclareEvent; import net.md_5.bungee.api.event.PluginMessageEvent; import net.md_5.bungee.api.event.ServerConnectEvent; import net.md_5.bungee.api.event.ServerDisconnectEvent; @@ -630,6 +637,11 @@ public class DownstreamBridge extends PacketHandler { boolean modified = false; + CommandsDeclareEvent commandsDeclareEvent = new CommandsDeclareEvent( server, con, commands.getRoot() ); + bungee.getPluginManager().callEvent( commandsDeclareEvent ); + + modified = commandsDeclareEvent.isModified(); + for ( Map.Entry command : bungee.getPluginManager().getCommands() ) { if ( !bungee.getDisabledCommands().contains( command.getKey() ) && commands.getRoot().getChild( command.getKey() ) == null && command.getValue().hasPermission( con ) ) @@ -646,11 +658,65 @@ public class DownstreamBridge extends PacketHandler if ( modified ) { + commands.setRoot( (RootCommandNode) filterCommandNode( commands.getRoot(), new IdentityHashMap<>() ) ); con.unsafe().sendPacket( commands ); throw CancelSendSignal.INSTANCE; } } + /* + * Create a deep copy of the provided command node but removes any node that are not accessible by the player + * (using {@link CommandNode#getRequirement()}) + */ + private CommandNode filterCommandNode(CommandNode source, IdentityHashMap commandNodeMapping) + { + CommandNode dest; + if ( source instanceof RootCommandNode ) + { + dest = new RootCommandNode(); + } else + { + if ( source.getRequirement() != null ) + { + try + { + if ( !source.getRequirement().test( con ) ) + { + commandNodeMapping.put( source, null ); + return null; + } + } catch ( Throwable t ) + { + ProxyServer.getInstance().getLogger().log( Level.SEVERE, "Requirement test for command node " + source + " encountered an exception", t ); + } + } + + ArgumentBuilder destChildBuilder = source.createBuilder(); + destChildBuilder.requires( sender -> true ); + if ( destChildBuilder.getRedirect() != null ) + { + if ( commandNodeMapping.containsKey( destChildBuilder.getRedirect() ) ) + destChildBuilder.redirect( commandNodeMapping.get( destChildBuilder.getRedirect() ) ); + else + destChildBuilder.redirect( filterCommandNode( destChildBuilder.getRedirect(), commandNodeMapping ) ); + } + + dest = destChildBuilder.build(); + } + + commandNodeMapping.put( source, dest ); + + for ( CommandNode sourceChild : (Collection) source.getChildren() ) + { + CommandNode destChild = filterCommandNode( sourceChild, commandNodeMapping ); + if ( destChild == null ) + continue; + dest.addChild( destChild ); + } + + return dest; + } + @Override public String toString() {