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 0eb3661b..bd6b1755 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; @@ -305,7 +306,7 @@ public class Commands extends DefinedPacket } @Data - private static class ArgumentRegistry + public static class ArgumentRegistry { private static final Map PROVIDERS = new HashMap<>(); @@ -325,18 +326,29 @@ 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 int getIntKey() + { + return 0; + } + + @Override + protected String getKey() + { + return "brigadier:bool"; } }; private static final ArgumentSerializer BYTE = new ArgumentSerializer() @@ -353,7 +365,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) @@ -381,8 +393,20 @@ public class Commands extends DefinedPacket buf.writeFloat( t.getMaximum() ); } } + + @Override + protected int getIntKey() + { + return 1; + } + + @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) @@ -410,8 +434,20 @@ public class Commands extends DefinedPacket buf.writeDouble( t.getMaximum() ); } } + + @Override + protected int getIntKey() + { + return 2; + } + + @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) @@ -439,8 +475,20 @@ public class Commands extends DefinedPacket buf.writeInt( t.getMaximum() ); } } + + @Override + protected int getIntKey() + { + return 3; + } + + @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) @@ -468,6 +516,18 @@ public class Commands extends DefinedPacket buf.writeLong( t.getMaximum() ); } } + + @Override + protected int getIntKey() + { + return 4; + } + + @Override + protected String getKey() + { + return "brigadier:long"; + } }; private static final ProperArgumentSerializer STRING = new ProperArgumentSerializer() { @@ -523,11 +583,20 @@ public class Commands extends DefinedPacket static { - register( "brigadier:bool", VOID ); + register( "brigadier:bool", BOOLEAN ); + PROPER_PROVIDERS.put( BoolArgumentType.class, BOOLEAN ); + register( "brigadier:float", FLOAT ); + PROPER_PROVIDERS.put( FloatArgumentType.class, FLOAT ); + register( "brigadier:double", DOUBLE ); + PROPER_PROVIDERS.put( DoubleArgumentType.class, DOUBLE ); + register( "brigadier:integer", INTEGER ); - register( "brigadier:long", LONG ); + PROPER_PROVIDERS.put( IntegerArgumentType.class, INTEGER ); + + register( "brigadier:long", LONG ); // 1.14+ + PROPER_PROVIDERS.put( LongArgumentType.class, LONG ); register( "brigadier:string", STRING ); PROPER_PROVIDERS.put( StringArgumentType.class, STRING ); @@ -584,6 +653,404 @@ public class Commands extends DefinedPacket PROVIDER_LIST.add( serializer ); } + /** + * 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 ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:resource}. + * @param rawString the raw string for the argument + * @return an ArgumentType instance + */ + public static ArgumentType minecraftResource(String rawString) + { + return minecraftArgumentType( "minecraft:resource", rawString ); + } + + /** + * Returns the Minecraft ArgumentType {@code minecraft:resource_or_tag}. + * @param rawString the raw string for the argument + * @return an ArgumentType instance + */ + public static ArgumentType minecraftResourceOrTag(String rawString) + { + return minecraftArgumentType( "minecraft:resource_or_tag", rawString ); + } + + 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(ByteBuf buf, int protocolVersion) { Object key; @@ -697,9 +1164,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 3e71b705..447b7696 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,6 +6,7 @@ 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; @@ -19,8 +20,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 +35,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; @@ -657,6 +662,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 ) ) @@ -673,11 +683,65 @@ public class DownstreamBridge extends PacketHandler if ( modified ) { + commands.setRoot( (com.mojang.brigadier.tree.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 com.mojang.brigadier.tree.RootCommandNode ) + { + dest = new com.mojang.brigadier.tree.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() {