Add argument type handling and add return type for registerCommandSuper

- Also added help text back and cleaned some stuff up
- Added support for number argument limits
This commit is contained in:
Norbi Peti 2022-11-21 01:20:31 +01:00
parent 05477641a4
commit b53813fa2e
4 changed files with 85 additions and 104 deletions

View file

@ -3,11 +3,12 @@ package buttondevteam.lib.chat;
import buttondevteam.core.MainPlugin; import buttondevteam.core.MainPlugin;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.commands.CommandArgument; import buttondevteam.lib.chat.commands.CommandArgument;
import buttondevteam.lib.chat.commands.NumberArg;
import buttondevteam.lib.chat.commands.SubcommandData; import buttondevteam.lib.chat.commands.SubcommandData;
import buttondevteam.lib.player.ChromaGamerBase; import buttondevteam.lib.player.ChromaGamerBase;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults; 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.builder.ArgumentBuilder;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.CommandNode;
@ -16,6 +17,8 @@ import lombok.RequiredArgsConstructor;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import org.javatuples.Pair;
import org.javatuples.Triplet;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -235,33 +238,6 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
return false; 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<TP, TC> getSubcommandNode(TC command, Method method, Subcommand subcommand) throws Exception {
var pdata = getParameterData(method);
val arguments = new HashMap<String, CommandArgument>(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. * 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. * Always invoke {@link #registerCommandSuper(ICommand2)} when implementing this method.
@ -277,14 +253,23 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
* @return The Brigadier command node if you need it for something (like tab completion) * @return The Brigadier command node if you need it for something (like tab completion)
*/ */
protected LiteralCommandNode<TP> registerCommandSuper(TC command) { protected LiteralCommandNode<TP> registerCommandSuper(TC command) {
LiteralCommandNode<TP> mainCommandNode = null;
for (val meth : command.getClass().getMethods()) { for (val meth : command.getClass().getMethods()) {
val ann = meth.getAnnotation(Subcommand.class); val ann = meth.getAnnotation(Subcommand.class);
if (ann == null) continue; if (ann == null) continue;
String methodPath = getCommandPath(meth.getName(), ' '); String methodPath = getCommandPath(meth.getName(), ' ');
registerNodeFromPath(command.getCommandPath() + methodPath) val result = registerNodeFromPath(command.getCommandPath() + methodPath);
.addChild(getExecutableNode(meth, command, methodPath.substring(methodPath.lastIndexOf(' ') + 1))); 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;
}
/** /**
* Returns the node that can actually execute the given subcommand. * Returns the node that can actually execute the given subcommand.
@ -294,17 +279,19 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
* @param path The command path * @param path The command path
* @return The executable node * @return The executable node
*/ */
private LiteralCommandNode<TP> getExecutableNode(Method method, TC command, String path) { private LiteralCommandNode<TP> getExecutableNode(Method method, TC command, Subcommand ann, String path) {
val params = getCommandParameters(method); // Param order is important val params = getCommandParameters(method); // Param order is important
val paramMap = new HashMap<String, CommandArgument>(); val paramMap = new HashMap<String, CommandArgument>();
for (val param : params) { for (val param : params) {
if (!Objects.equals(param.name, SENDER_ARG_NAME)) 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).executes(this::executeCommand); val node = CoreCommandBuilder.<TP, TC>literal(path, params[0].type, paramMap, command)
.helps(command.getHelpText(method, ann)).executes(this::executeCommand);
ArgumentBuilder<TP, ?> parent = node; ArgumentBuilder<TP, ?> parent = node;
for (val param : params) { // Register parameters in the right order 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(); return node.build();
} }
@ -313,19 +300,22 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
* Registers all necessary no-op nodes for the given path. * Registers all necessary no-op nodes for the given path.
* *
* @param path The full command 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<TP> registerNodeFromPath(String path) { private Triplet<CommandNode<TP>, LiteralCommandNode<TP>, String> registerNodeFromPath(String path) {
String[] split = path.split(" "); String[] split = path.split(" ");
CommandNode<TP> parent = dispatcher.getRoot(); CommandNode<TP> parent = dispatcher.getRoot();
LiteralCommandNode<TP> mainCommand = null;
for (int i = 0; i < split.length - 1; i++) { for (int i = 0; i < split.length - 1; i++) {
String part = split[i]; String part = split[i];
var child = parent.getChild(part); var child = parent.getChild(part);
if (child == null) if (child == null)
parent.addChild(parent = CoreCommandBuilder.<TP, TC>literalNoOp(part).executes(this::executeHelpText).build()); parent.addChild(parent = CoreCommandBuilder.<TP, TC>literalNoOp(part).executes(this::executeHelpText).build());
else parent = child; else parent = child;
if (i == 0) mainCommand = (LiteralCommandNode<TP>) 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<TC extends ICommand2<TP>, TP extends Command2Send
* @throws RuntimeException If there is no sender parameter declared in the method * @throws RuntimeException If there is no sender parameter declared in the method
*/ */
private CommandArgument[] getCommandParameters(Method method) { private CommandArgument[] getCommandParameters(Method method) {
val parameters = method.getParameterTypes(); val parameters = method.getParameters();
if (parameters.length == 0) if (parameters.length == 0)
throw new RuntimeException("No sender parameter for method '" + method + "'"); throw new RuntimeException("No sender parameter for method '" + method + "'");
val ret = new CommandArgument[parameters.length]; val ret = new CommandArgument[parameters.length];
val usage = getParameterHelp(method); 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) { if (usage == null) {
for (int i = 1; i < parameters.length; i++) { 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 { } else {
val paramNames = usage.split(" "); val paramNames = usage.split(" ");
for (int i = 1; i < parameters.length; i++) { 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; return ret;
} }
private ArgumentType<?> getParameterType(Class<?> type) { private ArgumentType<?> getParameterType(CommandArgument arg) {
// TODO: Move from registerTabcomplete 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<TP> context) { private int executeHelpText(CommandContext<TP> context) {
@ -450,42 +467,12 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
return null; return null;
} }
private void registerCommand(String path, String methodName, Subcommand ann, SubcommandData<TC> 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 abstract boolean hasPermission(TP sender, TC command, Method subcommand);
public String[] getCommandsText() { public String[] getCommandsText() {
return commandHelp.toArray(new String[0]); return commandHelp.toArray(new String[0]);
} }
/**
* Unregisters all of the subcommands in the given command.
*
* @param command The command object
*/
public void unregisterCommand(ICommand2<TP> 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. * It will start with the given replace char.
* *

View file

@ -5,7 +5,7 @@ import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.ButtonPlugin; import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.player.ChromaGamerBase; 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.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.CommandNode;
@ -333,38 +333,8 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implemen
Parameter[] parameters = subcmd.method.getParameters(); Parameter[] parameters = subcmd.method.getParameters();
for (int i = 1; i < parameters.length; i++) { //Skip sender for (int i = 1; i < parameters.length; i++) { //Skip sender
Parameter parameter = parameters[i]; Parameter parameter = parameters[i];
ArgumentType<?> type;
final Class<?> ptype = parameter.getType();
final boolean customParamType; final boolean customParamType;
{ // TODO: Arg type
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;
}
val param = subcmd.parameters[i - 1]; val param = subcmd.parameters[i - 1];
val customTC = Optional.ofNullable(parameter.getAnnotation(CustomTabComplete.class)) val customTC = Optional.ofNullable(parameter.getAnnotation(CustomTabComplete.class))
.map(CustomTabComplete::value); .map(CustomTabComplete::value);

View file

@ -1,6 +1,7 @@
package buttondevteam.lib.chat.commands; package buttondevteam.lib.chat.commands;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.javatuples.Pair;
/** /**
* A command argument's information to be used to construct the command. * A command argument's information to be used to construct the command.
@ -9,5 +10,8 @@ import lombok.RequiredArgsConstructor;
public class CommandArgument { public class CommandArgument {
public final String name; public final String name;
public final Class<?> type; public final Class<?> type;
public final boolean greedy;
public final Pair<Double, Double> limits;
public final boolean optional;
public final String description; public final String description;
} }

View file

@ -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;
}