Store arguments in order, fix errors, perm check

- Resolved all Command2 errors, Command2MC is next
This commit is contained in:
Norbi Peti 2022-11-24 01:48:28 +01:00
parent b53813fa2e
commit c0c3fc68dc
4 changed files with 125 additions and 104 deletions

View file

@ -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<TC extends ICommand2<TP>, 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<TC extends ICommand2<TP>, TP extends Command2Send
private final ArrayList<String> commandHelp = new ArrayList<>(); //Mainly needed by Discord
private final CommandDispatcher<TP> 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<TC extends ICommand2<TP>, 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<TC> sd, ArrayList<Object> params) {
private boolean processSenderType(TP sender, SubcommandData<TC, TP> sd, ArrayList<Object> params) {
val sendertype = sd.senderType;
final ChromaGamerBase cg;
if (sendertype.isAssignableFrom(sender.getClass()))
@ -280,18 +224,18 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
* @return The executable node
*/
private LiteralCommandNode<TP> 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<String, CommandArgument>();
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.<TP, TC>literal(path, params[0].type, paramMap, command)
.helps(command.getHelpText(method, ann)).executes(this::executeCommand);
val node = CoreCommandBuilder.<TP, TC>literal(path, params[0].type, paramMap, params, command)
.helps(command.getHelpText(method, ann)).permits(sender -> hasPermission(sender, command, method))
.executes(this::executeCommand);
ArgumentBuilder<TP, ?> 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<TC extends ICommand2<TP>, 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<CommandArgument[], Class<?>> 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<TC extends ICommand2<TP>, TP extends Command2Send
private int executeCommand(CommandContext<TP> 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;
}

View file

@ -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<ICommand2MC, Command2MCSender> implements Listener {
public Command2MC() {
super('/');
super('/', true);
}
/**
@ -56,8 +57,8 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> 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<ICommand2MC, Command2MCSender> implemen
private boolean shouldRegisterOfficially = true;
private Command registerOfficially(ICommand2MC command, List<SubcommandData<ICommand2MC>> subcmds) {
private Command registerOfficially(ICommand2MC command, LiteralCommandNode<Command2MCSender> 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<ICommand2MC, Command2MCSender> 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<ICommand2MC, Command2MCSender> 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<SubcommandData<ICommand2MC>> subcmds, Command bukkitCommand) {
private static void registerTabcomplete(ICommand2MC command2MC, LiteralCommandNode<Command2MCSender> 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<ICommand2MC, Command2MCSender> 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<Object>) node).findAny()
.orElseGet(() -> LiteralArgumentBuilder.literal(path[0]).build()); //Commodore 1.8 removes previous nodes
var cmd = maincmd;

View file

@ -8,12 +8,13 @@ import com.mojang.brigadier.tree.CommandNode;
import java.util.Map;
import java.util.function.Function;
public class CoreCommandBuilder<S, TC extends ICommand2<?>> extends LiteralArgumentBuilder<S> {
private final SubcommandData.SubcommandDataBuilder<TC> dataBuilder;
public class CoreCommandBuilder<S extends Command2Sender, TC extends ICommand2<?>> extends LiteralArgumentBuilder<S> {
private final SubcommandData.SubcommandDataBuilder<TC, S> dataBuilder;
protected CoreCommandBuilder(String literal, Class<?> senderType, Map<String, CommandArgument> arguments, TC command) {
protected CoreCommandBuilder(String literal, Class<?> senderType, Map<String, CommandArgument> arguments, CommandArgument[] argumentsInOrder, TC command) {
super(literal);
dataBuilder = SubcommandData.<TC>builder().senderType(senderType).arguments(arguments).command(command);
dataBuilder = SubcommandData.<TC, S>builder().senderType(senderType).arguments(arguments)
.argumentsInOrder(argumentsInOrder).command(command);
}
@Override
@ -21,12 +22,12 @@ public class CoreCommandBuilder<S, TC extends ICommand2<?>> extends LiteralArgum
return this;
}
public static <S, TC extends ICommand2<?>> CoreCommandBuilder<S, TC> literal(String name, Class<?> senderType, Map<String, CommandArgument> arguments, TC command) {
return new CoreCommandBuilder<>(name, senderType, arguments, command);
public static <S extends Command2Sender, TC extends ICommand2<?>> CoreCommandBuilder<S, TC> literal(String name, Class<?> senderType, Map<String, CommandArgument> arguments, CommandArgument[] argumentsInOrder, TC command) {
return new CoreCommandBuilder<>(name, senderType, arguments, argumentsInOrder, command);
}
public static <S, TC extends ICommand2<?>> CoreCommandBuilder<S, TC> literalNoOp(String name) {
return literal(name, Command2Sender.class, Map.of(), null);
public static <S extends Command2Sender, TC extends ICommand2<?>> CoreCommandBuilder<S, TC> literalNoOp(String name) {
return literal(name, Command2Sender.class, Map.of(), new CommandArgument[0], null);
}
/**
@ -53,9 +54,14 @@ public class CoreCommandBuilder<S, TC extends ICommand2<?>> extends LiteralArgum
return this;
}
public CoreCommandBuilder<S, TC> permits(Function<S, Boolean> permChecker) {
dataBuilder.hasPermission(permChecker);
return this;
}
@Override
public CoreCommandNode<S, TC> build() {
var result = new CoreCommandNode<>(this.getLiteral(), this.getCommand(), this.getRequirement(),
var result = new CoreCommandNode<S, TC>(this.getLiteral(), this.getCommand(), this.getRequirement(),
this.getRedirect(), this.getRedirectModifier(), this.isFork(),
dataBuilder.build());

View file

@ -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<TC extends ICommand2<?>> {
public final class SubcommandData<TC extends ICommand2<?>, 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<TC extends ICommand2<?>> {
* Used to construct the arguments for Brigadier and to hold extra information.
*/
public final Map<String, CommandArgument> 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<TC extends ICommand2<?>> {
* It will either match or be a Command2Sender, however.
*/
private final Function<Object, String[]> helpTextGetter;
/**
* A function that determines whether the user has permission to run this subcommand.
*/
private final Function<TP, Boolean> hasPermission;
/**
* Get help text for this subcommand.
@ -53,4 +62,14 @@ public final class SubcommandData<TC extends ICommand2<?>> {
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);
}
}