Implement argument handling and add a bunch of TODOs

This commit is contained in:
Norbi Peti 2022-08-03 22:11:40 +02:00
parent 5f7f3d7747
commit b59a090e13
6 changed files with 115 additions and 78 deletions

View file

@ -1,17 +1,16 @@
package buttondevteam.lib.chat; package buttondevteam.lib.chat;
import buttondevteam.core.MainPlugin; import buttondevteam.core.MainPlugin;
import buttondevteam.lib.ChromaUtils;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.commands.CommandArgument;
import buttondevteam.lib.chat.commands.ParameterData;
import buttondevteam.lib.chat.commands.SubcommandData;
import buttondevteam.lib.player.ChromaGamerBase; import buttondevteam.lib.player.ChromaGamerBase;
import com.google.common.base.Defaults;
import com.google.common.primitives.Primitives;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults; import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.LiteralCommandNode;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@ -26,8 +25,6 @@ import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Parameter; import java.lang.reflect.Parameter;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
@ -41,13 +38,10 @@ import static buttondevteam.lib.chat.CoreCommandBuilder.literal;
* The method name is the subcommand, use underlines (_) to add further subcommands. * The method name is the subcommand, use underlines (_) to add further subcommands.
* The args may be null if the conversion failed and it's optional. * The args may be null if the conversion failed and it's optional.
*/ */
@RequiredArgsConstructor
public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Sender> { public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Sender> {
protected Command2() {
commandHelp.add("§6---- Commands ----");
}
/** /**
* Parameters annotated with this receive all of the remaining arguments * Parameters annotated with this receive all the remaining arguments
*/ */
@Target(ElementType.PARAMETER) @Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ -84,13 +78,6 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
public @interface OptionalArg { public @interface OptionalArg {
} }
@AllArgsConstructor
protected static class SubcommandData<T extends ICommand2<?>> {
public final Method method;
public final T command;
public String[] helpText;
}
/*protected static class SubcommandHelpData<T extends ICommand2> extends SubcommandData<T> { /*protected static class SubcommandHelpData<T extends ICommand2> extends SubcommandData<T> {
private final TreeSet<String> ht = new TreeSet<>(); private final TreeSet<String> ht = new TreeSet<>();
private BukkitTask task; private BukkitTask task;
@ -127,7 +114,7 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
private final ArrayList<String> commandHelp = new ArrayList<>(); //Mainly needed by Discord private final ArrayList<String> commandHelp = new ArrayList<>(); //Mainly needed by Discord
private final CommandDispatcher<TP> dispatcher = new CommandDispatcher<>(); private final CommandDispatcher<TP> dispatcher = new CommandDispatcher<>();
private char commandChar; private final char commandChar;
/** /**
* Adds a param converter that obtains a specific object from a string parameter. * Adds a param converter that obtains a specific object from a string parameter.
@ -165,9 +152,8 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
* @param commandNode The processed command the sender sent * @param commandNode The processed command the sender sent
* @param sd The subcommand data * @param sd The subcommand data
* @param sync Whether the command was originally sync * @param sync Whether the command was originally sync
* @throws Exception If something's not right
*/ */
private void handleCommandAsync(TP sender, ParseResults<?> parsed, SubcommandData<TC> sd, boolean sync) throws Exception { private void handleCommandAsync(TP sender, ParseResults<?> parsed, SubcommandData<TC> sd, boolean sync) {
if (sd.method == null || sd.command == null) { //Main command not registered, but we have subcommands if (sd.method == null || sd.command == null) { //Main command not registered, but we have subcommands
sender.sendMessage(sd.helpText); sender.sendMessage(sd.helpText);
return; return;
@ -176,17 +162,12 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
sender.sendMessage("§cYou don't have permission to use this command"); sender.sendMessage("§cYou don't have permission to use this command");
return; return;
} }
val params = new ArrayList<Object>(sd.method.getParameterCount()); // TODO: WIP
Class<?>[] parameterTypes = sd.method.getParameterTypes();
if (parameterTypes.length == 0)
throw new Exception("No sender parameter for method '" + sd.method + "'");
if (processSenderType(sender, sd, params, parameterTypes)) return; // Checks if the sender is the wrong type if (processSenderType(sender, sd, params, parameterTypes)) return; // Checks if the sender is the wrong type
val paramArr = sd.method.getParameters();
val args = parsed.getContext().getArguments(); val args = parsed.getContext().getArguments();
for (int i1 = 1; i1 < parameterTypes.length; i1++) { for (var arg : sd.arguments.entrySet()) {
Class<?> cl = parameterTypes[i1]; // TODO: Invoke using custom method
pj = j + 1; //Start index /*if (pj == commandline.length() + 1) { //No param given
if (pj == commandline.length() + 1) { //No param given
if (paramArr[i1].isAnnotationPresent(OptionalArg.class)) { if (paramArr[i1].isAnnotationPresent(OptionalArg.class)) {
if (cl.isPrimitive()) if (cl.isPrimitive())
params.add(Defaults.defaultValue(cl)); params.add(Defaults.defaultValue(cl));
@ -200,50 +181,13 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
sender.sendMessage(sd.helpText); //Required param missing sender.sendMessage(sd.helpText); //Required param missing
return; return;
} }
} }*/
if (paramArr[i1].isVarArgs()) { /*if (paramArr[i1].isVarArgs()) { - TODO: Varargs support? (colors?)
params.add(commandline.substring(j + 1).split(" +")); params.add(commandline.substring(j + 1).split(" +"));
continue; continue;
} }*/
j = commandline.indexOf(' ', j + 1); //End index // TODO: Character handling (strlen)
if (j == -1 || paramArr[i1].isAnnotationPresent(TextArg.class)) //Last parameter // TODO: Param converter
j = commandline.length();
String param = commandline.substring(pj, j);
if (cl == String.class) {
params.add(param);
continue;
} else if (Number.class.isAssignableFrom(cl) || cl.isPrimitive()) {
try {
if (cl == boolean.class) {
params.add(Boolean.parseBoolean(param));
continue;
}
if (cl == char.class) {
if (param.length() != 1) {
sender.sendMessage("§c'" + param + "' is not a character.");
return;
}
params.add(param.charAt(0));
continue;
}
//noinspection unchecked
Number n = ChromaUtils.convertNumber(NumberFormat.getInstance().parse(param), (Class<? extends Number>) cl);
params.add(n);
} catch (ParseException e) {
sender.sendMessage("§c'" + param + "' is not a number.");
return;
}
continue;
}
val conv = paramConverters.get(cl);
if (conv == null)
throw new Exception("No suitable converter found for parameter type '" + cl.getCanonicalName() + "' for command '" + sd.method + "'");
val cparam = conv.converter.apply(param);
if (cparam == null) {
sender.sendMessage(conv.errormsg); //Param conversion failed - ex. plugin not found
return;
}
params.add(cparam);
} }
Runnable invokeCommand = () -> { Runnable invokeCommand = () -> {
try { try {
@ -266,12 +210,12 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
invokeCommand.run(); invokeCommand.run();
} //TODO: Add to the help } //TODO: Add to the help
private boolean processSenderType(TP sender, SubcommandData<TC> sd, ArrayList<Object> params, Class<?>[] parameterTypes) { private boolean processSenderType(TP sender, SubcommandData<TC> sd, ArrayList<Object> params) {
val sendertype = parameterTypes[0]; val sendertype = sd.senderType;
final ChromaGamerBase cg; final ChromaGamerBase cg;
if (sendertype.isAssignableFrom(sender.getClass())) if (sendertype.isAssignableFrom(sender.getClass()))
params.add(sender); //The command either expects a CommandSender or it is a Player, or some other expected type params.add(sender); //The command either expects a CommandSender or it is a Player, or some other expected type
else if (sender instanceof Command2MCSender else if (sender instanceof Command2MCSender // TODO: This is Minecraft only
&& sendertype.isAssignableFrom(((Command2MCSender) sender).getSender().getClass())) && sendertype.isAssignableFrom(((Command2MCSender) sender).getSender().getClass()))
params.add(((Command2MCSender) sender).getSender()); params.add(((Command2MCSender) sender).getSender());
else if (ChromaGamerBase.class.isAssignableFrom(sendertype) else if (ChromaGamerBase.class.isAssignableFrom(sendertype)
@ -281,15 +225,54 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
params.add(cg); params.add(cg);
else { else {
sender.sendMessage("§cYou need to be a " + sendertype.getSimpleName() + " to use this command."); sender.sendMessage("§cYou need to be a " + sendertype.getSimpleName() + " to use this command.");
sender.sendMessage(sd.helpText); //Send what the command is about, could be useful for commands like /member where some subcommands aren't player-only sender.sendMessage(sd.getHelpText(sender)); //Send what the command is about, could be useful for commands like /member where some subcommands aren't player-only
return true; return true;
} }
return false; return false;
} }
protected LiteralCommandNode<TP> processSubcommand(TC command, Method method) throws Exception {
val params = new ArrayList<Object>(method.getParameterCount());
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 0)
throw new Exception("No sender parameter for method '" + method + "'");
val paramArr = method.getParameters();
val arguments = new HashMap<String, CommandArgument>(parameterTypes.length - 1);
for (int i1 = 1; i1 < parameterTypes.length; i1++) {
Class<?> cl = parameterTypes[i1];
var pdata = getParameterData(method, i1);
arguments.put(pdata.name, new CommandArgument(pdata.name, cl, pdata.description));
}
var sd = new SubcommandData<TC>(parameterTypes[0], arguments, command, command.getHelpText(method, method.getAnnotation(Subcommand.class)), null); // TODO: Help text
return getSubcommandNode(method, sd); // TODO: Integrate with getCommandNode and store SubcommandData instead of help text
}
/**
* 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
*/
private ParameterData getParameterData(Method method, int i) {
return null; // TODO: Parameter data (from help text method)
}
/**
* 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.
*
* @param command The command to register
*/
public abstract void registerCommand(TC command); public abstract void registerCommand(TC command);
protected LiteralCommandNode<TP> registerCommand(TC command, char commandChar) { /**
* Registers a command in the Command2 system, so it can be looked up and executed.
*
* @param command The command to register
* @return The Brigadier command node if you need it for something (like tab completion)
*/
protected LiteralCommandNode<TP> registerCommandSuper(TC command) {
return dispatcher.register(getCommandNode(command)); return dispatcher.register(getCommandNode(command));
} }

View file

@ -38,6 +38,10 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implements Listener { public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implements Listener {
public Command2MC() {
super('/');
}
/** /**
* Don't use directly, use the method in Component and ButtonPlugin to automatically unregister the command when needed. * Don't use directly, use the method in Component and ButtonPlugin to automatically unregister the command when needed.
* *
@ -52,7 +56,7 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implemen
int i = cpath.indexOf(' '); int i = cpath.indexOf(' ');
mainpath = cpath.substring(0, i == -1 ? cpath.length() : i); mainpath = cpath.substring(0, i == -1 ? cpath.length() : i);
}*/ }*/
var subcmds = super.registerCommand(command, '/'); var subcmds = super.registerCommandSuper(command);
var bcmd = registerOfficially(command, subcmds); var bcmd = registerOfficially(command, subcmds);
if (bcmd != null) if (bcmd != null)
for (String alias : bcmd.getAliases()) for (String alias : bcmd.getAliases())

View file

@ -0,0 +1,10 @@
package buttondevteam.lib.chat.commands;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class CommandArgument {
public final String name;
public final Class<?> type;
public final String description;
}

View file

@ -0,0 +1,9 @@
package buttondevteam.lib.chat.commands;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class ParameterData {
public final String name;
public final String description;
}

View file

@ -0,0 +1,30 @@
package buttondevteam.lib.chat.commands;
import buttondevteam.lib.chat.ICommand2;
import lombok.RequiredArgsConstructor;
import java.util.Map;
import java.util.function.Function;
@RequiredArgsConstructor
public final class SubcommandData<TC extends ICommand2<?>> {
// The actual sender type may not be represented by Command2Sender (TP)
public final Class<?> senderType;
public final Map<String, CommandArgument> arguments;
public final TC command;
/**
* Static help text added through annotations. May be overwritten with the getter.
*/
private final String[] staticHelpText;
/**
* Custom help text that depends on the context. Overwrites the static one.
* The function receives the sender but its type is not guaranteed to match the one at the subcommand.
* It will either match or be a Command2Sender, however.
*/
private final Function<Object, String[]> helpTextGetter;
public String[] getHelpText(Object sender) {
return staticHelpText == null ? helpTextGetter.apply(sender) : staticHelpText;
}
}

View file

@ -41,6 +41,7 @@ public abstract class ChromaGamerBase {
/** /**
* Used for connecting with every type of user ({@link #connectWith(ChromaGamerBase)}) and to init the configs. * Used for connecting with every type of user ({@link #connectWith(ChromaGamerBase)}) and to init the configs.
* Also, to construct an instance if an abstract class is provided.
*/ */
public static <T extends ChromaGamerBase> void RegisterPluginUserClass(Class<T> userclass, Supplier<T> constructor) { public static <T extends ChromaGamerBase> void RegisterPluginUserClass(Class<T> userclass, Supplier<T> constructor) {
Class<? extends T> cl; Class<? extends T> cl;