diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.java index b10ce51..902b40d 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.java @@ -3,11 +3,12 @@ package buttondevteam.lib.chat; import buttondevteam.core.MainPlugin; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.chat.commands.CommandArgument; +import buttondevteam.lib.chat.commands.NumberArg; import buttondevteam.lib.chat.commands.SubcommandData; import buttondevteam.lib.player.ChromaGamerBase; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.ParseResults; -import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.*; import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.tree.CommandNode; @@ -16,6 +17,8 @@ import lombok.RequiredArgsConstructor; import lombok.val; import org.bukkit.Bukkit; import org.bukkit.configuration.file.YamlConfiguration; +import org.javatuples.Pair; +import org.javatuples.Triplet; import org.jetbrains.annotations.NotNull; import java.io.InputStreamReader; @@ -235,33 +238,6 @@ public abstract class Command2, TP extends Command2Send return false; } - /** - * Constructs a command node for the given subcommand that can be used for a custom registering logic (Discord). - * - * @param command The command object - * @param method The subcommand method - * @return The processed command node - * @throws Exception Something broke - */ - protected CoreCommandNode getSubcommandNode(TC command, Method method, Subcommand subcommand) throws Exception { - var pdata = getParameterData(method); - val arguments = new HashMap(pdata.length - 1); - for (var param : pdata) { - arguments.put(param.name, param); - } - // TODO: Dynamic help text - //return new SubcommandData<>(pdata[0].type, command.getCommandPath() + getCommandPath(method.getName(), ' '), arguments, command, command.getHelpText(method, subcommand), null); - return getSubcommandNode(method, pdata[0].type, command).helps(command.getHelpText(method, subcommand)).build(); - } - - /** - * Get parameter data for the given subcommand. Attempts to read it from the commands file, if it fails, it will return generic info. - * - * @param method The method the subcommand is created from - * @param i The index to use if no name was found - * @return Parameter data object - */ - /** * Register a command in the command system. The way this command gets registered may change depending on the implementation. * Always invoke {@link #registerCommandSuper(ICommand2)} when implementing this method. @@ -277,13 +253,22 @@ public abstract class Command2, TP extends Command2Send * @return The Brigadier command node if you need it for something (like tab completion) */ protected LiteralCommandNode registerCommandSuper(TC command) { + LiteralCommandNode mainCommandNode = null; for (val meth : command.getClass().getMethods()) { val ann = meth.getAnnotation(Subcommand.class); if (ann == null) continue; String methodPath = getCommandPath(meth.getName(), ' '); - registerNodeFromPath(command.getCommandPath() + methodPath) - .addChild(getExecutableNode(meth, command, methodPath.substring(methodPath.lastIndexOf(' ') + 1))); + val result = registerNodeFromPath(command.getCommandPath() + methodPath); + result.getValue0().addChild(getExecutableNode(meth, command, ann, result.getValue2())); + if (mainCommandNode == null) mainCommandNode = result.getValue1(); + else if (!result.getValue1().getName().equals(mainCommandNode.getName())) { + MainPlugin.Instance.getLogger().warning("Multiple commands are defined in the same class! This is not supported. Class: " + command.getClass().getSimpleName()); + } } + if (mainCommandNode == null) { + throw new RuntimeException("There are no subcommands defined in the command class " + command.getClass().getSimpleName() + "!"); + } + return mainCommandNode; } /** @@ -294,17 +279,19 @@ public abstract class Command2, TP extends Command2Send * @param path The command path * @return The executable node */ - private LiteralCommandNode getExecutableNode(Method method, TC command, String path) { + private LiteralCommandNode getExecutableNode(Method method, TC command, Subcommand ann, String path) { val params = getCommandParameters(method); // Param order is important val paramMap = new HashMap(); for (val param : params) { if (!Objects.equals(param.name, SENDER_ARG_NAME)) paramMap.put(param.name, param); } - val node = CoreCommandBuilder.literal(path, params[0].type, paramMap, command).executes(this::executeCommand); + val node = CoreCommandBuilder.literal(path, params[0].type, paramMap, command) + .helps(command.getHelpText(method, ann)).executes(this::executeCommand); ArgumentBuilder parent = node; for (val param : params) { // Register parameters in the right order - parent.then(parent = CoreArgumentBuilder.argument(param.name, getParameterType(param.type), false)); // TODO: Optional arg + if (!Objects.equals(param.name, SENDER_ARG_NAME)) + parent.then(parent = CoreArgumentBuilder.argument(param.name, getParameterType(param), param.optional)); } return node.build(); } @@ -313,19 +300,22 @@ public abstract class Command2, TP extends Command2Send * Registers all necessary no-op nodes for the given path. * * @param path The full command path - * @return The last no-op node that can be used to register the executable node + * @return The last no-op node that can be used to register the executable node, + * the main command node and the last part of the command path (that isn't registered yet) */ - private CommandNode registerNodeFromPath(String path) { + private Triplet, LiteralCommandNode, String> registerNodeFromPath(String path) { String[] split = path.split(" "); CommandNode parent = dispatcher.getRoot(); + LiteralCommandNode mainCommand = null; for (int i = 0; i < split.length - 1; i++) { String part = split[i]; var child = parent.getChild(part); if (child == null) parent.addChild(parent = CoreCommandBuilder.literalNoOp(part).executes(this::executeHelpText).build()); else parent = child; + if (i == 0) mainCommand = (LiteralCommandNode) parent; // Has to be a literal, if not, well, error } - return parent; + return new Triplet<>(parent, mainCommand, split[split.length - 1]); } /** @@ -337,27 +327,54 @@ public abstract class Command2, TP extends Command2Send * @throws RuntimeException If there is no sender parameter declared in the method */ private CommandArgument[] getCommandParameters(Method method) { - val parameters = method.getParameterTypes(); + val parameters = method.getParameters(); if (parameters.length == 0) throw new RuntimeException("No sender parameter for method '" + method + "'"); val ret = new CommandArgument[parameters.length]; val usage = getParameterHelp(method); - ret[0] = new CommandArgument(SENDER_ARG_NAME, parameters[0], "Sender"); + ret[0] = new CommandArgument(SENDER_ARG_NAME, parameters[0].getType(), false, null, false, "Sender"); if (usage == null) { for (int i = 1; i < parameters.length; i++) { - ret[i] = new CommandArgument("param" + i, parameters[i], "param" + i); + ret[i] = new CommandArgument("param" + i, parameters[i].getType(), false, null, false, "param" + i); } } else { val paramNames = usage.split(" "); for (int i = 1; i < parameters.length; i++) { - ret[i] = new CommandArgument(paramNames[i], parameters[i], paramNames[i]); // TODO: Description (JavaDoc?) + val numAnn = parameters[i].getAnnotation(NumberArg.class); + ret[i] = new CommandArgument(paramNames[i], parameters[i].getType(), + parameters[i].isVarArgs() || parameters[i].isAnnotationPresent(TextArg.class), + numAnn == null ? null : new Pair<>(numAnn.lowerLimit(), numAnn.upperLimit()), + parameters[i].isAnnotationPresent(OptionalArg.class), + paramNames[i]); // TODO: Description (JavaDoc?) } } return ret; } - private ArgumentType getParameterType(Class type) { - // TODO: Move from registerTabcomplete + private ArgumentType getParameterType(CommandArgument arg) { + final Class ptype = arg.type; + Number lowerLimit = Double.NEGATIVE_INFINITY, upperLimit = Double.POSITIVE_INFINITY; + if (arg.greedy) + return StringArgumentType.greedyString(); + else if (ptype == String.class) + return StringArgumentType.word(); + else if (ptype == int.class || ptype == Integer.class + || ptype == byte.class || ptype == Byte.class + || ptype == short.class || ptype == Short.class) + return IntegerArgumentType.integer(lowerLimit.intValue(), upperLimit.intValue()); + else if (ptype == long.class || ptype == Long.class) + return LongArgumentType.longArg(lowerLimit.longValue(), upperLimit.longValue()); + else if (ptype == float.class || ptype == Float.class) + return FloatArgumentType.floatArg(lowerLimit.floatValue(), upperLimit.floatValue()); + else if (ptype == double.class || ptype == Double.class) + return DoubleArgumentType.doubleArg(lowerLimit.doubleValue(), upperLimit.doubleValue()); + else if (ptype == char.class || ptype == Character.class) + return StringArgumentType.word(); + else if (ptype == boolean.class || ptype == Boolean.class) + return BoolArgumentType.bool(); + else { + return StringArgumentType.word(); + } } private int executeHelpText(CommandContext context) { @@ -450,42 +467,12 @@ public abstract class Command2, TP extends Command2Send return null; } - private void registerCommand(String path, String methodName, Subcommand ann, SubcommandData sd) { - val subcommand = commandChar + path + getCommandPath(methodName, ' '); - subcommands.put(subcommand, sd); - for (String alias : ann.aliases()) - subcommands.put(commandChar + path + alias, sd); - } - public abstract boolean hasPermission(TP sender, TC command, Method subcommand); public String[] getCommandsText() { return commandHelp.toArray(new String[0]); } - /** - * Unregisters all of the subcommands in the given command. - * - * @param command The command object - */ - public void unregisterCommand(ICommand2 command) { - var path = command.getCommandPath(); - for (val method : command.getClass().getMethods()) { - val ann = method.getAnnotation(Subcommand.class); - if (ann == null) continue; - unregisterCommand(path, method.getName(), ann); - for (String p : command.getCommandPaths()) - unregisterCommand(p, method.getName(), ann); - } - } - - private void unregisterCommand(String path, String methodName, Subcommand ann) { - val subcommand = commandChar + path + getCommandPath(methodName, ' '); - subcommands.remove(subcommand); - for (String alias : ann.aliases()) - subcommands.remove(commandChar + path + alias); - } - /** * It will start with the given replace char. * diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.java index e970e9b..a85d13a 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.java @@ -5,7 +5,7 @@ import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.architecture.ButtonPlugin; import buttondevteam.lib.architecture.Component; import buttondevteam.lib.player.ChromaGamerBase; -import com.mojang.brigadier.arguments.*; +import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.tree.CommandNode; @@ -333,38 +333,8 @@ public class Command2MC extends Command2 implemen Parameter[] parameters = subcmd.method.getParameters(); for (int i = 1; i < parameters.length; i++) { //Skip sender Parameter parameter = parameters[i]; - ArgumentType type; - final Class ptype = parameter.getType(); final boolean customParamType; - { - boolean customParamTypeTemp = false; - if (ptype == String.class) - if (parameter.isAnnotationPresent(TextArg.class)) - type = StringArgumentType.greedyString(); - else - type = StringArgumentType.word(); - else if (ptype == int.class || ptype == Integer.class - || ptype == byte.class || ptype == Byte.class - || ptype == short.class || ptype == Short.class) - type = IntegerArgumentType.integer(); //TODO: Min, max - else if (ptype == long.class || ptype == Long.class) - type = LongArgumentType.longArg(); - else if (ptype == float.class || ptype == Float.class) - type = FloatArgumentType.floatArg(); - else if (ptype == double.class || ptype == Double.class) - type = DoubleArgumentType.doubleArg(); - else if (ptype == char.class || ptype == Character.class) - type = StringArgumentType.word(); - else if (ptype == boolean.class || ptype == Boolean.class) - type = BoolArgumentType.bool(); - else if (parameter.isVarArgs()) - type = StringArgumentType.greedyString(); - else { - type = StringArgumentType.word(); - customParamTypeTemp = true; - } - customParamType = customParamTypeTemp; - } + // TODO: Arg type val param = subcmd.parameters[i - 1]; val customTC = Optional.ofNullable(parameter.getAnnotation(CustomTabComplete.class)) .map(CustomTabComplete::value); diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandArgument.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandArgument.java index 832f182..8620c10 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandArgument.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/CommandArgument.java @@ -1,6 +1,7 @@ package buttondevteam.lib.chat.commands; import lombok.RequiredArgsConstructor; +import org.javatuples.Pair; /** * A command argument's information to be used to construct the command. @@ -9,5 +10,8 @@ import lombok.RequiredArgsConstructor; public class CommandArgument { public final String name; public final Class type; + public final boolean greedy; + public final Pair limits; + public final boolean optional; public final String description; } diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/NumberArg.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/NumberArg.java new file mode 100644 index 0000000..7482d7b --- /dev/null +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/NumberArg.java @@ -0,0 +1,20 @@ +package buttondevteam.lib.chat.commands; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A command argument that can have a number as a value. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface NumberArg { + /** + * The highest value that can be used for this argument. + */ + double upperLimit() default Double.POSITIVE_INFINITY; + + /** + * The lowest value that can be used for this argument. + */ + double lowerLimit() default Double.NEGATIVE_INFINITY; +}