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 902b40d..1429d92 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.java @@ -7,10 +7,10 @@ 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.*; import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; import lombok.RequiredArgsConstructor; @@ -26,11 +26,9 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; -import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -42,8 +40,6 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public abstract class Command2, TP extends Command2Sender> { - private static final String SENDER_ARG_NAME = "#$@Sender"; - /** * Parameters annotated with this receive all the remaining arguments */ @@ -118,7 +114,14 @@ public abstract class Command2, TP extends Command2Send private final ArrayList commandHelp = new ArrayList<>(); //Mainly needed by Discord private final CommandDispatcher dispatcher = new CommandDispatcher<>(); + /** + * The first character in the command line that shows that it's a command. + */ private final char commandChar; + /** + * Whether the command's actual code has to be run on the primary thread. + */ + private final boolean runOnPrimaryThread; /** * Adds a param converter that obtains a specific object from a string parameter. @@ -136,85 +139,26 @@ public abstract class Command2, TP extends Command2Send public boolean handleCommand(TP sender, String commandline) { var results = dispatcher.parse(commandline, sender); - boolean sync = Bukkit.isPrimaryThread(); + if (results.getReader().canRead()) { + return false; // Unknown command + } Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, () -> { try { - handleCommandAsync(sender, results, sync); + dispatcher.execute(results); + } catch (CommandSyntaxException e) { + sender.sendMessage(e.getMessage()); } catch (Exception e) { TBMCCoreAPI.SendException("Command execution failed for sender " + sender.getName() + "(" + sender.getClass().getCanonicalName() + ") and message " + commandline, e, MainPlugin.Instance); } }); - return true; //We found a method - TODO + return true; //We found a method } //Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread - /** - * Handles a command asynchronously - * - * @param sender The command sender - * @param commandNode The processed command the sender sent - * @param sd The subcommand data - * @param sync Whether the command was originally sync - */ - private void handleCommandAsync(TP sender, ParseResults parsed, boolean sync) { - if (sd.method == null || sd.command == null) { //Main command not registered, but we have subcommands - sender.sendMessage(sd.helpText); - return; - } - if (!hasPermission(sender, sd.command, sd.method)) { - sender.sendMessage("§cYou don't have permission to use this command"); - return; - } - // TODO: WIP - if (processSenderType(sender, sd, params, parameterTypes)) return; // Checks if the sender is the wrong type - val args = parsed.getContext().getArguments(); - for (var arg : sd.arguments.entrySet()) { - // TODO: Invoke using custom method - /*if (pj == commandline.length() + 1) { //No param given - if (paramArr[i1].isAnnotationPresent(OptionalArg.class)) { - if (cl.isPrimitive()) - params.add(Defaults.defaultValue(cl)); - else if (Number.class.isAssignableFrom(cl) - || Number.class.isAssignableFrom(cl)) - params.add(Defaults.defaultValue(Primitives.unwrap(cl))); - else - params.add(null); - continue; //Fill the remaining params with nulls - } else { - sender.sendMessage(sd.helpText); //Required param missing - return; - } - }*/ - /*if (paramArr[i1].isVarArgs()) { - TODO: Varargs support? (colors?) - params.add(commandline.substring(j + 1).split(" +")); - continue; - }*/ - // TODO: Character handling (strlen) - // TODO: Param converter - } - Runnable invokeCommand = () -> { - try { - sd.method.setAccessible(true); //It may be part of a private class - val ret = sd.method.invoke(sd.command, params.toArray()); //I FORGOT TO TURN IT INTO AN ARRAY (for a long time) - if (ret instanceof Boolean) { - if (!(boolean) ret) //Show usage - sender.sendMessage(sd.helpText); - } else if (ret != null) - throw new Exception("Wrong return type! Must return a boolean or void. Return value: " + ret); - } catch (InvocationTargetException e) { - TBMCCoreAPI.SendException("An error occurred in a command handler for " + subcommand + "!", e.getCause(), MainPlugin.Instance); - } catch (Exception e) { - TBMCCoreAPI.SendException("Command handling failed for sender " + sender + " and subcommand " + subcommand, e, MainPlugin.Instance); - } - }; - if (sync) - Bukkit.getScheduler().runTask(MainPlugin.Instance, invokeCommand); - else - invokeCommand.run(); - } //TODO: Add to the help + //TODO: Add to the help - private boolean processSenderType(TP sender, SubcommandData sd, ArrayList params) { + private boolean processSenderType(TP sender, SubcommandData sd, ArrayList params) { val sendertype = sd.senderType; final ChromaGamerBase cg; if (sendertype.isAssignableFrom(sender.getClass())) @@ -280,18 +224,18 @@ public abstract class Command2, TP extends Command2Send * @return The executable node */ private LiteralCommandNode getExecutableNode(Method method, TC command, Subcommand ann, String path) { - val params = getCommandParameters(method); // Param order is important + val paramsAndSenderType = getCommandParameters(method); // Param order is important + val params = paramsAndSenderType.getValue0(); val paramMap = new HashMap(); for (val param : params) { - if (!Objects.equals(param.name, SENDER_ARG_NAME)) - paramMap.put(param.name, param); + paramMap.put(param.name, param); } - val node = CoreCommandBuilder.literal(path, params[0].type, paramMap, command) - .helps(command.getHelpText(method, ann)).executes(this::executeCommand); + val node = CoreCommandBuilder.literal(path, params[0].type, paramMap, params, command) + .helps(command.getHelpText(method, ann)).permits(sender -> hasPermission(sender, command, method)) + .executes(this::executeCommand); ArgumentBuilder parent = node; for (val param : params) { // Register parameters in the right order - if (!Objects.equals(param.name, SENDER_ARG_NAME)) - parent.then(parent = CoreArgumentBuilder.argument(param.name, getParameterType(param), param.optional)); + parent.then(parent = CoreArgumentBuilder.argument(param.name, getParameterType(param), param.optional)); } return node.build(); } @@ -323,32 +267,31 @@ public abstract class Command2, TP extends Command2Send * The first parameter is always the sender both in the methods themselves and in the returned array. * * @param method The method the subcommand is created from - * @return Parameter data objects + * @return Parameter data objects and the sender type * @throws RuntimeException If there is no sender parameter declared in the method */ - private CommandArgument[] getCommandParameters(Method method) { + private Pair> getCommandParameters(Method method) { 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].getType(), false, null, false, "Sender"); if (usage == null) { for (int i = 1; i < parameters.length; i++) { - ret[i] = new CommandArgument("param" + i, parameters[i].getType(), false, null, false, "param" + i); + ret[i - 1] = 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++) { val numAnn = parameters[i].getAnnotation(NumberArg.class); - ret[i] = new CommandArgument(paramNames[i], parameters[i].getType(), + ret[i - 1] = 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; + return new Pair<>(ret, parameters[0].getType()); } private ArgumentType getParameterType(CommandArgument arg) { @@ -384,6 +327,58 @@ public abstract class Command2, TP extends Command2Send private int executeCommand(CommandContext context) { System.out.println("Execute command"); + System.out.println("Should be running sync: " + runOnPrimaryThread); + + /*if (!hasPermission(sender, sd.command, sd.method)) { + sender.sendMessage("§cYou don't have permission to use this command"); + return; + } + // TODO: WIP + if (processSenderType(sender, sd, params, parameterTypes)) return; // Checks if the sender is the wrong type + val args = parsed.getContext().getArguments(); + for (var arg : sd.arguments.entrySet()) {*/ + // TODO: Invoke using custom method + /*if (pj == commandline.length() + 1) { //No param given + if (paramArr[i1].isAnnotationPresent(OptionalArg.class)) { + if (cl.isPrimitive()) + params.add(Defaults.defaultValue(cl)); + else if (Number.class.isAssignableFrom(cl) + || Number.class.isAssignableFrom(cl)) + params.add(Defaults.defaultValue(Primitives.unwrap(cl))); + else + params.add(null); + continue; //Fill the remaining params with nulls + } else { + sender.sendMessage(sd.helpText); //Required param missing + return; + } + }*/ + /*if (paramArr[i1].isVarArgs()) { - TODO: Varargs support? (colors?) + params.add(commandline.substring(j + 1).split(" +")); + continue; + }*/ + // TODO: Character handling (strlen) + // TODO: Param converter + /*} + Runnable invokeCommand = () -> { + try { + sd.method.setAccessible(true); //It may be part of a private class + val ret = sd.method.invoke(sd.command, params.toArray()); //I FORGOT TO TURN IT INTO AN ARRAY (for a long time) + if (ret instanceof Boolean) { + if (!(boolean) ret) //Show usage + sender.sendMessage(sd.helpText); + } else if (ret != null) + throw new Exception("Wrong return type! Must return a boolean or void. Return value: " + ret); + } catch (InvocationTargetException e) { + TBMCCoreAPI.SendException("An error occurred in a command handler for " + subcommand + "!", e.getCause(), MainPlugin.Instance); + } catch (Exception e) { + TBMCCoreAPI.SendException("Command handling failed for sender " + sender + " and subcommand " + subcommand, e, MainPlugin.Instance); + } + }; + if (sync) + Bukkit.getScheduler().runTask(MainPlugin.Instance, invokeCommand); + else + invokeCommand.run();*/ return 0; } 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 a85d13a..7d00212 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.java @@ -4,6 +4,7 @@ import buttondevteam.core.MainPlugin; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.architecture.ButtonPlugin; import buttondevteam.lib.architecture.Component; +import buttondevteam.lib.chat.commands.SubcommandData; import buttondevteam.lib.player.ChromaGamerBase; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; @@ -39,7 +40,7 @@ import java.util.stream.Stream; public class Command2MC extends Command2 implements Listener { public Command2MC() { - super('/'); + super('/', true); } /** @@ -56,8 +57,8 @@ public class Command2MC extends Command2 implemen int i = cpath.indexOf(' '); mainpath = cpath.substring(0, i == -1 ? cpath.length() : i); }*/ - var subcmds = super.registerCommandSuper(command); - var bcmd = registerOfficially(command, subcmds); + var commandNode = super.registerCommandSuper(command); + var bcmd = registerOfficially(command, commandNode); if (bcmd != null) for (String alias : bcmd.getAliases()) super.registerCommand(command, command.getCommandPath().replaceFirst("^" + bcmd.getName(), Matcher.quoteReplacement(alias)), '/'); @@ -208,7 +209,7 @@ public class Command2MC extends Command2 implemen private boolean shouldRegisterOfficially = true; - private Command registerOfficially(ICommand2MC command, List> subcmds) { + private Command registerOfficially(ICommand2MC command, LiteralCommandNode node) { if (!shouldRegisterOfficially || command.getPlugin() == null) return null; try { var cmdmap = (SimpleCommandMap) Bukkit.getServer().getClass().getMethod("getCommandMap").invoke(Bukkit.getServer()); @@ -229,7 +230,7 @@ public class Command2MC extends Command2 implemen bukkitCommand = oldcmd == null ? new BukkitCommand(mainPath) : oldcmd; } if (CommodoreProvider.isSupported()) - TabcompleteHelper.registerTabcomplete(command, subcmds, bukkitCommand); + TabcompleteHelper.registerTabcomplete(command, node, bukkitCommand); return bukkitCommand; } catch (Exception e) { if (command.getComponent() == null) @@ -286,14 +287,14 @@ public class Command2MC extends Command2 implemen if (subcommand != null) scmdBuilder.requires(o -> { var sender = commodore.getBukkitSender(o); - return ButtonPlugin.getCommand2MC().hasPermission(sender, subcommand.command, subcommand.method); + return subcommand.hasPermission(sender); }); scmd = scmdBuilder.build(); parent.addChild(scmd); return scmd; } - private static void registerTabcomplete(ICommand2MC command2MC, List> subcmds, Command bukkitCommand) { + private static void registerTabcomplete(ICommand2MC command2MC, LiteralCommandNode commandNode, Command bukkitCommand) { if (commodore == null) { commodore = CommodoreProvider.getCommodore(MainPlugin.Instance); //Register all to the Core, it's easier commodore.register(LiteralArgumentBuilder.literal("un").redirect(RequiredArgumentBuilder.argument("unsomething", @@ -303,7 +304,7 @@ public class Command2MC extends Command2 implemen var shouldRegister = new AtomicBoolean(true); @SuppressWarnings("unchecked") var maincmd = commodore.getRegisteredNodes().stream() .filter(node -> node.getLiteral().equalsIgnoreCase(path[0])) - .filter(node -> { shouldRegister.set(false); return true; }) + .filter(node -> {shouldRegister.set(false); return true;}) .map(node -> (LiteralCommandNode) node).findAny() .orElseGet(() -> LiteralArgumentBuilder.literal(path[0]).build()); //Commodore 1.8 removes previous nodes var cmd = maincmd; diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.java index 29b50f4..5fa44c0 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CoreCommandBuilder.java @@ -8,12 +8,13 @@ import com.mojang.brigadier.tree.CommandNode; import java.util.Map; import java.util.function.Function; -public class CoreCommandBuilder> extends LiteralArgumentBuilder { - private final SubcommandData.SubcommandDataBuilder dataBuilder; +public class CoreCommandBuilder> extends LiteralArgumentBuilder { + private final SubcommandData.SubcommandDataBuilder dataBuilder; - protected CoreCommandBuilder(String literal, Class senderType, Map arguments, TC command) { + protected CoreCommandBuilder(String literal, Class senderType, Map arguments, CommandArgument[] argumentsInOrder, TC command) { super(literal); - dataBuilder = SubcommandData.builder().senderType(senderType).arguments(arguments).command(command); + dataBuilder = SubcommandData.builder().senderType(senderType).arguments(arguments) + .argumentsInOrder(argumentsInOrder).command(command); } @Override @@ -21,12 +22,12 @@ public class CoreCommandBuilder> extends LiteralArgum return this; } - public static > CoreCommandBuilder literal(String name, Class senderType, Map arguments, TC command) { - return new CoreCommandBuilder<>(name, senderType, arguments, command); + public static > CoreCommandBuilder literal(String name, Class senderType, Map arguments, CommandArgument[] argumentsInOrder, TC command) { + return new CoreCommandBuilder<>(name, senderType, arguments, argumentsInOrder, command); } - public static > CoreCommandBuilder literalNoOp(String name) { - return literal(name, Command2Sender.class, Map.of(), null); + public static > CoreCommandBuilder literalNoOp(String name) { + return literal(name, Command2Sender.class, Map.of(), new CommandArgument[0], null); } /** @@ -53,9 +54,14 @@ public class CoreCommandBuilder> extends LiteralArgum return this; } + public CoreCommandBuilder permits(Function permChecker) { + dataBuilder.hasPermission(permChecker); + return this; + } + @Override public CoreCommandNode build() { - var result = new CoreCommandNode<>(this.getLiteral(), this.getCommand(), this.getRequirement(), + var result = new CoreCommandNode(this.getLiteral(), this.getCommand(), this.getRequirement(), this.getRedirect(), this.getRedirectModifier(), this.isFork(), dataBuilder.build()); diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.java index 4711b04..178822d 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commands/SubcommandData.java @@ -1,5 +1,6 @@ package buttondevteam.lib.chat.commands; +import buttondevteam.lib.chat.Command2Sender; import buttondevteam.lib.chat.ICommand2; import lombok.Builder; import lombok.RequiredArgsConstructor; @@ -15,7 +16,7 @@ import java.util.function.Function; */ @Builder @RequiredArgsConstructor -public final class SubcommandData> { +public final class SubcommandData, TP extends Command2Sender> { /** * The type of the sender running the command. * The actual sender type may not be represented by Command2Sender (TP). @@ -27,6 +28,10 @@ public final class SubcommandData> { * Used to construct the arguments for Brigadier and to hold extra information. */ public final Map arguments; + /** + * Command arguments in the order they appear in code and in game. + */ + public final CommandArgument[] argumentsInOrder; /** * The original command class that this data belongs to. If null, that meaans only the help text can be used. */ @@ -43,6 +48,10 @@ public final class SubcommandData> { * It will either match or be a Command2Sender, however. */ private final Function helpTextGetter; + /** + * A function that determines whether the user has permission to run this subcommand. + */ + private final Function hasPermission; /** * Get help text for this subcommand. @@ -53,4 +62,14 @@ public final class SubcommandData> { public String[] getHelpText(Object sender) { return staticHelpText == null ? helpTextGetter.apply(sender) : staticHelpText; } + + /** + * Check if the user has permission to execute this subcommand. + * + * @param sender The sender running the command + * @return Whether the user has permission + */ + public boolean hasPermission(TP sender) { + return hasPermission.apply(sender); + } }