diff --git a/.idea/compiler.xml b/.idea/compiler.xml index c61c5f2b..e9b5c8e4 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,20 +7,22 @@ + + + + + + + + - - - - - - - + @@ -36,6 +38,7 @@ + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index d07ce074..05af6095 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -2,6 +2,8 @@ + + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index b70af4c1..0dfa5228 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -35,6 +35,7 @@ + diff --git a/brigadier/pom.xml b/brigadier/pom.xml new file mode 100644 index 00000000..4109ca75 --- /dev/null +++ b/brigadier/pom.xml @@ -0,0 +1,74 @@ + + + + + 4.0.0 + + + co.aikar + acf-parent + 0.5.0-SNAPSHOT + ../pom.xml + + + acf-brigadier + 0.5.0-SNAPSHOT + + ACF (Brigadier) + + + + mojang-repo + https://libraries.minecraft.net/ + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + true + + + + + + + co.aikar + acf-core + 0.5.0-SNAPSHOT + provided + + + com.mojang + brigadier + 1.0.17 + provided + + + diff --git a/brigadier/src/main/java/co.aikar.commands/ACFBrigadierManager.java b/brigadier/src/main/java/co.aikar.commands/ACFBrigadierManager.java new file mode 100644 index 00000000..1787abd8 --- /dev/null +++ b/brigadier/src/main/java/co.aikar.commands/ACFBrigadierManager.java @@ -0,0 +1,164 @@ +package co.aikar.commands; + +import com.mojang.brigadier.Command; +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.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +/** + * Handles registering of commands into brigadier + * + * @param + * @author MiniDigger + * @deprecated Unstable API + */ +@Deprecated +@UnstableAPI +public class ACFBrigadierManager { + + protected final CommandManager, ?, ?, ?, ?, ?> manager; + + private final Map, ArgumentType>> arguments = new HashMap<>(); + + /** + * Constructs a new brigadier manager, utilizing the currently active command manager + * + * @param manager + */ + ACFBrigadierManager(CommandManager, ?, ?, ?, ?, ?> manager) { + manager.verifyUnstableAPI("brigadier"); + + this.manager = manager; + + // TODO support stuff like min max via brigadier? + registerArgument(String.class, StringArgumentType.word()); + registerArgument(float.class, FloatArgumentType.floatArg()); + registerArgument(Float.class, FloatArgumentType.floatArg()); + registerArgument(double.class, DoubleArgumentType.doubleArg()); + registerArgument(Double.class, DoubleArgumentType.doubleArg()); + registerArgument(boolean.class, BoolArgumentType.bool()); + registerArgument(Boolean.class, BoolArgumentType.bool()); + registerArgument(int.class, IntegerArgumentType.integer()); + registerArgument(Integer.class, IntegerArgumentType.integer()); + // We use integer for long due to Bungee bug, plus should really be considered same on client + registerArgument(long.class, IntegerArgumentType.integer()); + registerArgument(Long.class, IntegerArgumentType.integer()); + } + + void registerArgument(Class clazz, ArgumentType> type) { + arguments.put(clazz, type); + } + + ArgumentType getArgumentTypeByClazz(CommandParameter param) { + if (param.consumesRest) { + //noinspection unchecked + return (ArgumentType) (ArgumentType>) StringArgumentType.greedyString(); + } + //noinspection unchecked + return (ArgumentType) arguments.getOrDefault(param.getType(), StringArgumentType.string()); + } + + /** + * Registers the given RootCommand into the given brigadir command node, utilizing the provided suggestion provider, executor and permission predicate. + * + * It recreates the root command node! + */ + LiteralCommandNode register(RootCommand rootCommand, + LiteralCommandNode root, + SuggestionProvider suggestionProvider, + Command executor, + BiPredicate permCheckerRoot, + BiPredicate permCheckerSub) { + // recreate root to get rid of bukkits default arg + LiteralArgumentBuilder rootBuilder = LiteralArgumentBuilder.literal(root.getLiteral()) + .requires(sender -> permCheckerRoot.test(rootCommand, sender)); + + root = rootBuilder.build(); + boolean isForwardingCommand = rootCommand.getDefCommand() instanceof ForwardingCommand; + + for (Map.Entry subCommand : rootCommand.getSubCommands().entries()) { + if ((BaseCommand.isSpecialSubcommand(subCommand.getKey()) && !isForwardingCommand) || (!subCommand.getKey().equals("help") && subCommand.getValue().prefSubCommand.equals("help"))) { + // don't register stuff like __catchunknown and don't help command aliases + continue; + } + + // handle sub sub commands + String commandName = subCommand.getKey(); + CommandNode currentParent = root; + CommandNode subCommandNode; + Predicate subPermChecker = sender -> permCheckerSub.test(subCommand.getValue(), sender); + if (!isForwardingCommand) { + if (commandName.contains(" ")) { + String[] split = ACFPatterns.SPACE.split(commandName); + for (int i = 0; i < split.length - 1; i++) { + if (currentParent.getChild(split[i]) == null) { + LiteralCommandNode sub = LiteralArgumentBuilder.literal(split[i]) + .requires(subPermChecker).build(); + currentParent.addChild(sub); + currentParent = sub; + } else { + currentParent = currentParent.getChild(split[i]); + } + } + commandName = split[split.length - 1]; + } + + subCommandNode = currentParent.getChild(commandName); + if (subCommandNode == null) { + LiteralArgumentBuilder argumentBuilder = LiteralArgumentBuilder.literal(commandName) + .requires(subPermChecker); + + // if we have no params, this command is actually executable + if (subCommand.getValue().consumeInputResolvers == 0) { + argumentBuilder.executes(executor); + } + subCommandNode = argumentBuilder.build(); + } + } else { + subCommandNode = root; + } + + CommandNode paramNode = subCommandNode; + CommandParameter[] parameters = subCommand.getValue().parameters; + for (int i = 0; i < parameters.length; i++) { + CommandParameter param = parameters[i]; + CommandParameter nextParam = param.getNextParam(); + if (param.isCommandIssuer() || (param.canExecuteWithoutInput() && nextParam != null && !nextParam.canExecuteWithoutInput())) { + continue; + } + RequiredArgumentBuilder builder = RequiredArgumentBuilder + .argument(param.getName(), getArgumentTypeByClazz(param)) + .suggests(suggestionProvider) + .requires(sender -> permCheckerSub.test(subCommand.getValue(), sender)); + + if (nextParam != null && nextParam.canExecuteWithoutInput()) { + builder.executes(executor); + } + + CommandNode subSubCommand = builder.build(); + paramNode.addChild(subSubCommand); + paramNode = subSubCommand; + } + + if (!isForwardingCommand) { + currentParent.addChild(subCommandNode); + } + } + + return root; + } + +} diff --git a/core/src/main/java/co/aikar/commands/BaseCommand.java b/core/src/main/java/co/aikar/commands/BaseCommand.java index 560b5604..24d1a346 100644 --- a/core/src/main/java/co/aikar/commands/BaseCommand.java +++ b/core/src/main/java/co/aikar/commands/BaseCommand.java @@ -662,7 +662,7 @@ public abstract class BaseCommand { String argString = ApacheCommonsLangUtil.join(args, " ").toLowerCase(Locale.ENGLISH); for (Map.Entry entry : subCommands.entries()) { final String key = entry.getKey(); - if (key.startsWith(argString) && !CATCHUNKNOWN.equals(key) && !DEFAULT.equals(key)) { + if (key.startsWith(argString) && !isSpecialSubcommand(key)) { final RegisteredCommand value = entry.getValue(); if (!value.hasPermission(issuer) || value.isPrivate) { continue; @@ -675,6 +675,10 @@ public abstract class BaseCommand { return new ArrayList<>(cmds); } + static boolean isSpecialSubcommand(String key) { + return CATCHUNKNOWN.equals(key) || DEFAULT.equals(key); + } + /** * Complete a command properly per issuer and input. * @@ -839,4 +843,8 @@ public abstract class BaseCommand { registeredCommands.addAll(this.subCommands.values()); return registeredCommands; } + + protected SetMultimap getSubCommands() { + return subCommands; + } } diff --git a/core/src/main/java/co/aikar/commands/CommandParameter.java b/core/src/main/java/co/aikar/commands/CommandParameter.java index ba4f7ec5..dfeb0de2 100644 --- a/core/src/main/java/co/aikar/commands/CommandParameter.java +++ b/core/src/main/java/co/aikar/commands/CommandParameter.java @@ -66,9 +66,13 @@ public class CommandParameter nextParam; public CommandParameter(RegisteredCommand command, Parameter param, int paramIndex, boolean isLast) { this.parameter = param; + this.isLast = isLast; this.type = param.getType(); this.name = param.getName(); // do we care for an annotation to supply name? this.manager = command.manager; @@ -99,10 +103,12 @@ public class CommandParameter"; @@ -184,6 +190,10 @@ public class CommandParameter getRequiredPermissions() { return permissions; } + + public void setNextParam(CommandParameter nextParam) { + this.nextParam = nextParam; + } + + public CommandParameter getNextParam() { + return nextParam; + } + + public boolean canExecuteWithoutInput() { + return (!canConsumeInput || isOptionalInput()) && (nextParam == null || nextParam.canExecuteWithoutInput()); + } + + public boolean isLast() { + return isLast; + } } diff --git a/core/src/main/java/co/aikar/commands/ForwardingCommand.java b/core/src/main/java/co/aikar/commands/ForwardingCommand.java index 438528dd..67e6e798 100644 --- a/core/src/main/java/co/aikar/commands/ForwardingCommand.java +++ b/core/src/main/java/co/aikar/commands/ForwardingCommand.java @@ -23,8 +23,6 @@ package co.aikar.commands; -import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil; - import java.util.Collections; import java.util.List; import java.util.Set; diff --git a/core/src/main/java/co/aikar/commands/RegisteredCommand.java b/core/src/main/java/co/aikar/commands/RegisteredCommand.java index 6241e170..ba809435 100644 --- a/core/src/main/java/co/aikar/commands/RegisteredCommand.java +++ b/core/src/main/java/co/aikar/commands/RegisteredCommand.java @@ -84,7 +84,7 @@ public class RegisteredCommand previousParam = null; for (int i = 0; i < parameters.length; i++) { CommandParameter parameter = this.parameters[i] = new CommandParameter<>(this, parameters[i], i, i == parameters.length - 1); + if (previousParam != null) { + previousParam.setNextParam(parameter); + } + previousParam = parameter; if (!parameter.isCommandIssuer()) { if (!parameter.requiresInput()) { optionalResolvers++; @@ -247,9 +252,9 @@ public class RegisteredCommand com.destroystokyo.paper paper-api - 1.13.2-R0.1-SNAPSHOT + 1.15.2-R0.1-SNAPSHOT provided diff --git a/example/src/main/java/co/aikar/acfexample/ACFExample.java b/example/src/main/java/co/aikar/acfexample/ACFExample.java index 51a1c1dd..b9901f41 100644 --- a/example/src/main/java/co/aikar/acfexample/ACFExample.java +++ b/example/src/main/java/co/aikar/acfexample/ACFExample.java @@ -23,10 +23,11 @@ package co.aikar.acfexample; -import co.aikar.commands.PaperCommandManager; import co.aikar.commands.ConditionFailedException; import co.aikar.commands.MessageKeys; import co.aikar.commands.MessageType; +import co.aikar.commands.PaperBrigadierManager; +import co.aikar.commands.PaperCommandManager; import org.bukkit.plugin.java.JavaPlugin; import java.util.Arrays; @@ -46,6 +47,9 @@ public final class ACFExample extends JavaPlugin { // 1: Create Command Manager for your respective platform commandManager = new PaperCommandManager(this); + // enable brigadier integration for paper servers + commandManager.enableUnstableAPI("brigadier"); + // optional: enable unstable api to use help commandManager.enableUnstableAPI("help"); @@ -109,6 +113,12 @@ public final class ACFExample extends JavaPlugin { getLogger().warning("Error occurred while executing command " + command.getName()); return false; // mark as unhandeled, sender will see default message }); + + // test command for brigadier + commandManager.getCommandCompletions().registerAsyncCompletion("someobject", c -> + Arrays.asList("1", "2", "3", "4", "5") + ); + commandManager.registerCommand(new BrigadierTest()); } // Typical Bukkit Plugin Scaffolding diff --git a/example/src/main/java/co/aikar/acfexample/BrigadierTest.java b/example/src/main/java/co/aikar/acfexample/BrigadierTest.java new file mode 100644 index 00000000..e6fc4c63 --- /dev/null +++ b/example/src/main/java/co/aikar/acfexample/BrigadierTest.java @@ -0,0 +1,92 @@ +package co.aikar.acfexample; + +import co.aikar.commands.BaseCommand; +import co.aikar.commands.CommandHelp; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandCompletion; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import co.aikar.commands.annotation.Description; +import co.aikar.commands.annotation.Flags; +import co.aikar.commands.annotation.HelpCommand; +import co.aikar.commands.annotation.Optional; +import co.aikar.commands.annotation.Single; +import co.aikar.commands.annotation.Subcommand; +import co.aikar.commands.annotation.Syntax; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +@CommandAlias("brigadiertest") +public class BrigadierTest extends BaseCommand { + + @Subcommand("hello") + @Syntax("") + @CommandCompletion("@players") + @Description("Says hello to a player") + public static void onHello(Player player, @Flags("other") Player arg) { + player.sendMessage("You said hello to " + arg.getDisplayName()); + } + + @Default + @HelpCommand + public static void onHelp(CommandSender sender, CommandHelp help) { + help.showHelp(); + } + + @Subcommand("test|test2") + @Description("Says hello to a player") + @CommandCompletion("true|false @range:20 @range:1-5 @range:20-30 test|test2|foo|bar true|false") + public static void onTest(CommandSender sender, boolean booleanParam, float floatParam, double doubleParam, int integerParam, String stringParam, @Optional Boolean test2) { + sender.sendMessage("You said: " + booleanParam + " - " + floatParam + " - " + doubleParam + " - " + integerParam + " - " + stringParam + " - " + test2 + "!"); + } + + @Subcommand("custom") + @Description("Try custom completions") + @Syntax("") + @CommandCompletion("@someobject") + public static void onCustom(Player player, SomeObject object) { + player.sendMessage("You said: " + object); + } + + @Subcommand("dummy admin") + @CommandPermission("dummy") + public static void onPerm(Player player) { + player.sendMessage("You shall pass"); + } + + @Subcommand("sub3") + public static void sub3(Player player, String wooo) { + player.sendMessage("Wooo " + wooo); + } + + @Subcommand("sub sub") + public static void onSubSub(Player player, String wooo) { + player.sendMessage("Wooo " + wooo); + } + + @Subcommand("sub2") + public static void onSub2(Player player) { + player.sendMessage("Sub2"); + } + + @Subcommand("sub2 sub") + public static void onSub2Sub(Player player, String test) { + player.sendMessage("Sub2 sub " + test); + } + + @Subcommand("find|where") + @CommandPermission("hyperverse.find") + @CommandAlias("hvf|hvfind") + @CommandCompletion("@players") + public void findPlayer(final CommandSender sender, final String player) { /* stub */ } + + @Subcommand("greedy") + public void onGreedy(Player player, String greedyString) { + player.sendMessage(greedyString); + } + + @Subcommand("notgreedy") + public void notGreedy(Player player, @Single String notGreedy) { + player.sendMessage(notGreedy); + } +} diff --git a/paper/pom.xml b/paper/pom.xml index 91cc0ef2..a60f3b54 100644 --- a/paper/pom.xml +++ b/paper/pom.xml @@ -46,10 +46,28 @@ 0.5.0-SNAPSHOT compile + + co.aikar + acf-brigadier + 0.5.0-SNAPSHOT + compile + com.destroystokyo.paper paper-api - 1.12.2-R0.1-SNAPSHOT + 1.15.2-R0.1-SNAPSHOT + provided + + + com.destroystokyo.paper + paper-mojangapi + 1.15.2-R0.1-SNAPSHOT + provided + + + com.destroystokyo.paper + paper-mojangapi + 1.15.2-R0.1-SNAPSHOT provided diff --git a/paper/src/main/java/co/aikar/commands/PaperAsyncTabCompleteHandler.java b/paper/src/main/java/co/aikar/commands/PaperAsyncTabCompleteHandler.java index f7ff035b..2ce6a3cd 100644 --- a/paper/src/main/java/co/aikar/commands/PaperAsyncTabCompleteHandler.java +++ b/paper/src/main/java/co/aikar/commands/PaperAsyncTabCompleteHandler.java @@ -51,6 +51,10 @@ class PaperAsyncTabCompleteHandler implements Listener { if (false) throw new CommandCompletions.SyncCompletionRequired(); // fake compiler due to SneakyThrows List completions = getCompletions(buffer, event.getCompletions(), event.getSender(), true); if (completions != null) { + // if we have no completion data, client will display an error, lets just send a space instead (https://bugs.mojang.com/browse/MC-165562) + if (completions.size() == 1 && completions.get(0).equals("")) { + completions.set(0, " "); + } event.setCompletions(completions); event.setHandled(true); } diff --git a/paper/src/main/java/co/aikar/commands/PaperBrigadierManager.java b/paper/src/main/java/co/aikar/commands/PaperBrigadierManager.java new file mode 100644 index 00000000..fc0ed497 --- /dev/null +++ b/paper/src/main/java/co/aikar/commands/PaperBrigadierManager.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-2020 Daniel Ennis (Aikar) - MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package co.aikar.commands; + +import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource; +import com.destroystokyo.paper.event.brigadier.CommandRegisteredEvent; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; + +/** + * Handles registering of commands into brigadier + * + * @author MiniDigger + * @deprecated Unstable API + */ +@Deprecated +@UnstableAPI +public class PaperBrigadierManager implements Listener { + + private final PaperCommandManager manager; + private final ACFBrigadierManager brigadierManager; + + public PaperBrigadierManager(Plugin plugin, PaperCommandManager manager) { + manager.verifyUnstableAPI("brigadier"); + manager.log(LogLevel.INFO, "Enabled Brigadier Support!"); + + this.manager = manager; + this.brigadierManager = new ACFBrigadierManager<>(manager); + + Bukkit.getPluginManager().registerEvents(this, plugin); + } + + @EventHandler + public void onCommandRegister(CommandRegisteredEvent event) { + RootCommand acfCommand = manager.getRootCommand(event.getCommandLabel()); + if (acfCommand != null) { + event.setLiteral(brigadierManager.register( + acfCommand, + event.getLiteral(), + event.getBrigadierCommand(), + event.getBrigadierCommand(), + this::checkPermRoot, + this::checkPermSub + )); + } + } + + private boolean checkPermSub(RegisteredCommand registeredCommand, BukkitBrigadierCommandSource sender) { + return registeredCommand.hasPermission(manager.getCommandIssuer(sender.getBukkitSender())); + } + + private boolean checkPermRoot(RootCommand rootCommand, BukkitBrigadierCommandSource sender) { + return rootCommand.hasAnyPermission(manager.getCommandIssuer(sender.getBukkitSender())); + } +} diff --git a/paper/src/main/java/co/aikar/commands/PaperCommandManager.java b/paper/src/main/java/co/aikar/commands/PaperCommandManager.java index fe06bd32..6de574ff 100644 --- a/paper/src/main/java/co/aikar/commands/PaperCommandManager.java +++ b/paper/src/main/java/co/aikar/commands/PaperCommandManager.java @@ -28,6 +28,8 @@ import org.bukkit.plugin.Plugin; @SuppressWarnings("WeakerAccess") public class PaperCommandManager extends BukkitCommandManager { + private boolean brigadierAvailable; + // If we get anything Paper specific public PaperCommandManager(Plugin plugin) { super(plugin); @@ -37,6 +39,21 @@ public class PaperCommandManager extends BukkitCommandManager { } catch (ClassNotFoundException ignored) { // Ignored } + try { + Class.forName("com.destroystokyo.paper.event.brigadier.CommandRegisteredEvent"); + brigadierAvailable = true; + } catch (ClassNotFoundException ignored) { + // Ignored + } + } + + @Override + public void enableUnstableAPI(String api) { + super.enableUnstableAPI(api); + + if ("brigadier".equals(api) && brigadierAvailable) { + new PaperBrigadierManager(plugin, this); + } } @Override diff --git a/pom.xml b/pom.xml index d784cf9c..4d6f6a75 100644 --- a/pom.xml +++ b/pom.xml @@ -197,11 +197,13 @@ core + brigadier bukkit paper bungee sponge jda velocity + example
+ * It recreates the root command node! + */ + LiteralCommandNode register(RootCommand rootCommand, + LiteralCommandNode root, + SuggestionProvider suggestionProvider, + Command executor, + BiPredicate permCheckerRoot, + BiPredicate permCheckerSub) { + // recreate root to get rid of bukkits default arg + LiteralArgumentBuilder rootBuilder = LiteralArgumentBuilder.literal(root.getLiteral()) + .requires(sender -> permCheckerRoot.test(rootCommand, sender)); + + root = rootBuilder.build(); + boolean isForwardingCommand = rootCommand.getDefCommand() instanceof ForwardingCommand; + + for (Map.Entry subCommand : rootCommand.getSubCommands().entries()) { + if ((BaseCommand.isSpecialSubcommand(subCommand.getKey()) && !isForwardingCommand) || (!subCommand.getKey().equals("help") && subCommand.getValue().prefSubCommand.equals("help"))) { + // don't register stuff like __catchunknown and don't help command aliases + continue; + } + + // handle sub sub commands + String commandName = subCommand.getKey(); + CommandNode currentParent = root; + CommandNode subCommandNode; + Predicate subPermChecker = sender -> permCheckerSub.test(subCommand.getValue(), sender); + if (!isForwardingCommand) { + if (commandName.contains(" ")) { + String[] split = ACFPatterns.SPACE.split(commandName); + for (int i = 0; i < split.length - 1; i++) { + if (currentParent.getChild(split[i]) == null) { + LiteralCommandNode sub = LiteralArgumentBuilder.literal(split[i]) + .requires(subPermChecker).build(); + currentParent.addChild(sub); + currentParent = sub; + } else { + currentParent = currentParent.getChild(split[i]); + } + } + commandName = split[split.length - 1]; + } + + subCommandNode = currentParent.getChild(commandName); + if (subCommandNode == null) { + LiteralArgumentBuilder argumentBuilder = LiteralArgumentBuilder.literal(commandName) + .requires(subPermChecker); + + // if we have no params, this command is actually executable + if (subCommand.getValue().consumeInputResolvers == 0) { + argumentBuilder.executes(executor); + } + subCommandNode = argumentBuilder.build(); + } + } else { + subCommandNode = root; + } + + CommandNode paramNode = subCommandNode; + CommandParameter[] parameters = subCommand.getValue().parameters; + for (int i = 0; i < parameters.length; i++) { + CommandParameter param = parameters[i]; + CommandParameter nextParam = param.getNextParam(); + if (param.isCommandIssuer() || (param.canExecuteWithoutInput() && nextParam != null && !nextParam.canExecuteWithoutInput())) { + continue; + } + RequiredArgumentBuilder builder = RequiredArgumentBuilder + .argument(param.getName(), getArgumentTypeByClazz(param)) + .suggests(suggestionProvider) + .requires(sender -> permCheckerSub.test(subCommand.getValue(), sender)); + + if (nextParam != null && nextParam.canExecuteWithoutInput()) { + builder.executes(executor); + } + + CommandNode subSubCommand = builder.build(); + paramNode.addChild(subSubCommand); + paramNode = subSubCommand; + } + + if (!isForwardingCommand) { + currentParent.addChild(subCommandNode); + } + } + + return root; + } + +} diff --git a/core/src/main/java/co/aikar/commands/BaseCommand.java b/core/src/main/java/co/aikar/commands/BaseCommand.java index 560b5604..24d1a346 100644 --- a/core/src/main/java/co/aikar/commands/BaseCommand.java +++ b/core/src/main/java/co/aikar/commands/BaseCommand.java @@ -662,7 +662,7 @@ public abstract class BaseCommand { String argString = ApacheCommonsLangUtil.join(args, " ").toLowerCase(Locale.ENGLISH); for (Map.Entry entry : subCommands.entries()) { final String key = entry.getKey(); - if (key.startsWith(argString) && !CATCHUNKNOWN.equals(key) && !DEFAULT.equals(key)) { + if (key.startsWith(argString) && !isSpecialSubcommand(key)) { final RegisteredCommand value = entry.getValue(); if (!value.hasPermission(issuer) || value.isPrivate) { continue; @@ -675,6 +675,10 @@ public abstract class BaseCommand { return new ArrayList<>(cmds); } + static boolean isSpecialSubcommand(String key) { + return CATCHUNKNOWN.equals(key) || DEFAULT.equals(key); + } + /** * Complete a command properly per issuer and input. * @@ -839,4 +843,8 @@ public abstract class BaseCommand { registeredCommands.addAll(this.subCommands.values()); return registeredCommands; } + + protected SetMultimap getSubCommands() { + return subCommands; + } } diff --git a/core/src/main/java/co/aikar/commands/CommandParameter.java b/core/src/main/java/co/aikar/commands/CommandParameter.java index ba4f7ec5..dfeb0de2 100644 --- a/core/src/main/java/co/aikar/commands/CommandParameter.java +++ b/core/src/main/java/co/aikar/commands/CommandParameter.java @@ -66,9 +66,13 @@ public class CommandParameter nextParam; public CommandParameter(RegisteredCommand command, Parameter param, int paramIndex, boolean isLast) { this.parameter = param; + this.isLast = isLast; this.type = param.getType(); this.name = param.getName(); // do we care for an annotation to supply name? this.manager = command.manager; @@ -99,10 +103,12 @@ public class CommandParameter"; @@ -184,6 +190,10 @@ public class CommandParameter getRequiredPermissions() { return permissions; } + + public void setNextParam(CommandParameter nextParam) { + this.nextParam = nextParam; + } + + public CommandParameter getNextParam() { + return nextParam; + } + + public boolean canExecuteWithoutInput() { + return (!canConsumeInput || isOptionalInput()) && (nextParam == null || nextParam.canExecuteWithoutInput()); + } + + public boolean isLast() { + return isLast; + } } diff --git a/core/src/main/java/co/aikar/commands/ForwardingCommand.java b/core/src/main/java/co/aikar/commands/ForwardingCommand.java index 438528dd..67e6e798 100644 --- a/core/src/main/java/co/aikar/commands/ForwardingCommand.java +++ b/core/src/main/java/co/aikar/commands/ForwardingCommand.java @@ -23,8 +23,6 @@ package co.aikar.commands; -import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil; - import java.util.Collections; import java.util.List; import java.util.Set; diff --git a/core/src/main/java/co/aikar/commands/RegisteredCommand.java b/core/src/main/java/co/aikar/commands/RegisteredCommand.java index 6241e170..ba809435 100644 --- a/core/src/main/java/co/aikar/commands/RegisteredCommand.java +++ b/core/src/main/java/co/aikar/commands/RegisteredCommand.java @@ -84,7 +84,7 @@ public class RegisteredCommand previousParam = null; for (int i = 0; i < parameters.length; i++) { CommandParameter parameter = this.parameters[i] = new CommandParameter<>(this, parameters[i], i, i == parameters.length - 1); + if (previousParam != null) { + previousParam.setNextParam(parameter); + } + previousParam = parameter; if (!parameter.isCommandIssuer()) { if (!parameter.requiresInput()) { optionalResolvers++; @@ -247,9 +252,9 @@ public class RegisteredCommand com.destroystokyo.paper paper-api - 1.13.2-R0.1-SNAPSHOT + 1.15.2-R0.1-SNAPSHOT provided diff --git a/example/src/main/java/co/aikar/acfexample/ACFExample.java b/example/src/main/java/co/aikar/acfexample/ACFExample.java index 51a1c1dd..b9901f41 100644 --- a/example/src/main/java/co/aikar/acfexample/ACFExample.java +++ b/example/src/main/java/co/aikar/acfexample/ACFExample.java @@ -23,10 +23,11 @@ package co.aikar.acfexample; -import co.aikar.commands.PaperCommandManager; import co.aikar.commands.ConditionFailedException; import co.aikar.commands.MessageKeys; import co.aikar.commands.MessageType; +import co.aikar.commands.PaperBrigadierManager; +import co.aikar.commands.PaperCommandManager; import org.bukkit.plugin.java.JavaPlugin; import java.util.Arrays; @@ -46,6 +47,9 @@ public final class ACFExample extends JavaPlugin { // 1: Create Command Manager for your respective platform commandManager = new PaperCommandManager(this); + // enable brigadier integration for paper servers + commandManager.enableUnstableAPI("brigadier"); + // optional: enable unstable api to use help commandManager.enableUnstableAPI("help"); @@ -109,6 +113,12 @@ public final class ACFExample extends JavaPlugin { getLogger().warning("Error occurred while executing command " + command.getName()); return false; // mark as unhandeled, sender will see default message }); + + // test command for brigadier + commandManager.getCommandCompletions().registerAsyncCompletion("someobject", c -> + Arrays.asList("1", "2", "3", "4", "5") + ); + commandManager.registerCommand(new BrigadierTest()); } // Typical Bukkit Plugin Scaffolding diff --git a/example/src/main/java/co/aikar/acfexample/BrigadierTest.java b/example/src/main/java/co/aikar/acfexample/BrigadierTest.java new file mode 100644 index 00000000..e6fc4c63 --- /dev/null +++ b/example/src/main/java/co/aikar/acfexample/BrigadierTest.java @@ -0,0 +1,92 @@ +package co.aikar.acfexample; + +import co.aikar.commands.BaseCommand; +import co.aikar.commands.CommandHelp; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandCompletion; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Default; +import co.aikar.commands.annotation.Description; +import co.aikar.commands.annotation.Flags; +import co.aikar.commands.annotation.HelpCommand; +import co.aikar.commands.annotation.Optional; +import co.aikar.commands.annotation.Single; +import co.aikar.commands.annotation.Subcommand; +import co.aikar.commands.annotation.Syntax; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +@CommandAlias("brigadiertest") +public class BrigadierTest extends BaseCommand { + + @Subcommand("hello") + @Syntax("") + @CommandCompletion("@players") + @Description("Says hello to a player") + public static void onHello(Player player, @Flags("other") Player arg) { + player.sendMessage("You said hello to " + arg.getDisplayName()); + } + + @Default + @HelpCommand + public static void onHelp(CommandSender sender, CommandHelp help) { + help.showHelp(); + } + + @Subcommand("test|test2") + @Description("Says hello to a player") + @CommandCompletion("true|false @range:20 @range:1-5 @range:20-30 test|test2|foo|bar true|false") + public static void onTest(CommandSender sender, boolean booleanParam, float floatParam, double doubleParam, int integerParam, String stringParam, @Optional Boolean test2) { + sender.sendMessage("You said: " + booleanParam + " - " + floatParam + " - " + doubleParam + " - " + integerParam + " - " + stringParam + " - " + test2 + "!"); + } + + @Subcommand("custom") + @Description("Try custom completions") + @Syntax("") + @CommandCompletion("@someobject") + public static void onCustom(Player player, SomeObject object) { + player.sendMessage("You said: " + object); + } + + @Subcommand("dummy admin") + @CommandPermission("dummy") + public static void onPerm(Player player) { + player.sendMessage("You shall pass"); + } + + @Subcommand("sub3") + public static void sub3(Player player, String wooo) { + player.sendMessage("Wooo " + wooo); + } + + @Subcommand("sub sub") + public static void onSubSub(Player player, String wooo) { + player.sendMessage("Wooo " + wooo); + } + + @Subcommand("sub2") + public static void onSub2(Player player) { + player.sendMessage("Sub2"); + } + + @Subcommand("sub2 sub") + public static void onSub2Sub(Player player, String test) { + player.sendMessage("Sub2 sub " + test); + } + + @Subcommand("find|where") + @CommandPermission("hyperverse.find") + @CommandAlias("hvf|hvfind") + @CommandCompletion("@players") + public void findPlayer(final CommandSender sender, final String player) { /* stub */ } + + @Subcommand("greedy") + public void onGreedy(Player player, String greedyString) { + player.sendMessage(greedyString); + } + + @Subcommand("notgreedy") + public void notGreedy(Player player, @Single String notGreedy) { + player.sendMessage(notGreedy); + } +} diff --git a/paper/pom.xml b/paper/pom.xml index 91cc0ef2..a60f3b54 100644 --- a/paper/pom.xml +++ b/paper/pom.xml @@ -46,10 +46,28 @@ 0.5.0-SNAPSHOT compile + + co.aikar + acf-brigadier + 0.5.0-SNAPSHOT + compile + com.destroystokyo.paper paper-api - 1.12.2-R0.1-SNAPSHOT + 1.15.2-R0.1-SNAPSHOT + provided + + + com.destroystokyo.paper + paper-mojangapi + 1.15.2-R0.1-SNAPSHOT + provided + + + com.destroystokyo.paper + paper-mojangapi + 1.15.2-R0.1-SNAPSHOT provided diff --git a/paper/src/main/java/co/aikar/commands/PaperAsyncTabCompleteHandler.java b/paper/src/main/java/co/aikar/commands/PaperAsyncTabCompleteHandler.java index f7ff035b..2ce6a3cd 100644 --- a/paper/src/main/java/co/aikar/commands/PaperAsyncTabCompleteHandler.java +++ b/paper/src/main/java/co/aikar/commands/PaperAsyncTabCompleteHandler.java @@ -51,6 +51,10 @@ class PaperAsyncTabCompleteHandler implements Listener { if (false) throw new CommandCompletions.SyncCompletionRequired(); // fake compiler due to SneakyThrows List completions = getCompletions(buffer, event.getCompletions(), event.getSender(), true); if (completions != null) { + // if we have no completion data, client will display an error, lets just send a space instead (https://bugs.mojang.com/browse/MC-165562) + if (completions.size() == 1 && completions.get(0).equals("")) { + completions.set(0, " "); + } event.setCompletions(completions); event.setHandled(true); } diff --git a/paper/src/main/java/co/aikar/commands/PaperBrigadierManager.java b/paper/src/main/java/co/aikar/commands/PaperBrigadierManager.java new file mode 100644 index 00000000..fc0ed497 --- /dev/null +++ b/paper/src/main/java/co/aikar/commands/PaperBrigadierManager.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-2020 Daniel Ennis (Aikar) - MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package co.aikar.commands; + +import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource; +import com.destroystokyo.paper.event.brigadier.CommandRegisteredEvent; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; + +/** + * Handles registering of commands into brigadier + * + * @author MiniDigger + * @deprecated Unstable API + */ +@Deprecated +@UnstableAPI +public class PaperBrigadierManager implements Listener { + + private final PaperCommandManager manager; + private final ACFBrigadierManager brigadierManager; + + public PaperBrigadierManager(Plugin plugin, PaperCommandManager manager) { + manager.verifyUnstableAPI("brigadier"); + manager.log(LogLevel.INFO, "Enabled Brigadier Support!"); + + this.manager = manager; + this.brigadierManager = new ACFBrigadierManager<>(manager); + + Bukkit.getPluginManager().registerEvents(this, plugin); + } + + @EventHandler + public void onCommandRegister(CommandRegisteredEvent event) { + RootCommand acfCommand = manager.getRootCommand(event.getCommandLabel()); + if (acfCommand != null) { + event.setLiteral(brigadierManager.register( + acfCommand, + event.getLiteral(), + event.getBrigadierCommand(), + event.getBrigadierCommand(), + this::checkPermRoot, + this::checkPermSub + )); + } + } + + private boolean checkPermSub(RegisteredCommand registeredCommand, BukkitBrigadierCommandSource sender) { + return registeredCommand.hasPermission(manager.getCommandIssuer(sender.getBukkitSender())); + } + + private boolean checkPermRoot(RootCommand rootCommand, BukkitBrigadierCommandSource sender) { + return rootCommand.hasAnyPermission(manager.getCommandIssuer(sender.getBukkitSender())); + } +} diff --git a/paper/src/main/java/co/aikar/commands/PaperCommandManager.java b/paper/src/main/java/co/aikar/commands/PaperCommandManager.java index fe06bd32..6de574ff 100644 --- a/paper/src/main/java/co/aikar/commands/PaperCommandManager.java +++ b/paper/src/main/java/co/aikar/commands/PaperCommandManager.java @@ -28,6 +28,8 @@ import org.bukkit.plugin.Plugin; @SuppressWarnings("WeakerAccess") public class PaperCommandManager extends BukkitCommandManager { + private boolean brigadierAvailable; + // If we get anything Paper specific public PaperCommandManager(Plugin plugin) { super(plugin); @@ -37,6 +39,21 @@ public class PaperCommandManager extends BukkitCommandManager { } catch (ClassNotFoundException ignored) { // Ignored } + try { + Class.forName("com.destroystokyo.paper.event.brigadier.CommandRegisteredEvent"); + brigadierAvailable = true; + } catch (ClassNotFoundException ignored) { + // Ignored + } + } + + @Override + public void enableUnstableAPI(String api) { + super.enableUnstableAPI(api); + + if ("brigadier".equals(api) && brigadierAvailable) { + new PaperBrigadierManager(plugin, this); + } } @Override diff --git a/pom.xml b/pom.xml index d784cf9c..4d6f6a75 100644 --- a/pom.xml +++ b/pom.xml @@ -197,11 +197,13 @@ core + brigadier bukkit paper bungee sponge jda velocity + example