Begin using Brigadier for the command system
Added custom command node types for help text support
This commit is contained in:
parent
66c1b1b14f
commit
a26d031ce2
4 changed files with 165 additions and 48 deletions
|
@ -6,6 +6,10 @@ import buttondevteam.lib.TBMCCoreAPI;
|
||||||
import buttondevteam.lib.player.ChromaGamerBase;
|
import buttondevteam.lib.player.ChromaGamerBase;
|
||||||
import com.google.common.base.Defaults;
|
import com.google.common.base.Defaults;
|
||||||
import com.google.common.primitives.Primitives;
|
import com.google.common.primitives.Primitives;
|
||||||
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import com.mojang.brigadier.ParseResults;
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
|
@ -20,6 +24,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
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.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -29,6 +34,8 @@ import java.util.List;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static com.mojang.brigadier.builder.LiteralArgumentBuilder.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.
|
||||||
|
@ -80,7 +87,6 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
||||||
protected static class SubcommandData<T extends ICommand2<?>> {
|
protected static class SubcommandData<T extends ICommand2<?>> {
|
||||||
public final Method method;
|
public final Method method;
|
||||||
public final T command;
|
public final T command;
|
||||||
public final String[] parameters;
|
|
||||||
public String[] helpText;
|
public String[] helpText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,10 +122,9 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
||||||
public final Supplier<Iterable<String>> allSupplier;
|
public final Supplier<Iterable<String>> allSupplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final HashMap<String, SubcommandData<TC>> subcommands = new HashMap<>();
|
|
||||||
protected final HashMap<Class<?>, ParamConverter<?>> paramConverters = new HashMap<>();
|
protected final HashMap<Class<?>, ParamConverter<?>> paramConverters = new HashMap<>();
|
||||||
|
|
||||||
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 char commandChar;
|
private char commandChar;
|
||||||
|
|
||||||
|
@ -138,21 +143,16 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleCommand(TP sender, String commandline) {
|
public boolean handleCommand(TP sender, String commandline) {
|
||||||
for (int i = commandline.length(); i != -1; i = commandline.lastIndexOf(' ', i - 1)) {
|
var results = dispatcher.parse(commandline, sender);
|
||||||
String subcommand = commandline.substring(0, i).toLowerCase();
|
boolean sync = Bukkit.isPrimaryThread();
|
||||||
SubcommandData<TC> sd = subcommands.get(subcommand);
|
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, () -> {
|
||||||
if (sd == null) continue;
|
try {
|
||||||
boolean sync = Bukkit.isPrimaryThread();
|
handleCommandAsync(sender, results, results.getContext().getNodes(), sync);
|
||||||
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, () -> {
|
} catch (Exception e) {
|
||||||
try {
|
TBMCCoreAPI.SendException("Command execution failed for sender " + sender.getName() + "(" + sender.getClass().getCanonicalName() + ") and message " + commandline, e, MainPlugin.Instance);
|
||||||
handleCommandAsync(sender, commandline, sd, subcommand, sync);
|
}
|
||||||
} 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
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread
|
//Needed because permission checking may load the (perhaps offline) sender's file which is disallowed on the main thread
|
||||||
|
@ -161,13 +161,12 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
||||||
* Handles a command asynchronously
|
* Handles a command asynchronously
|
||||||
*
|
*
|
||||||
* @param sender The command sender
|
* @param sender The command sender
|
||||||
* @param commandline The command line the sender sent
|
* @param commandNode The processed command the sender sent
|
||||||
* @param sd The subcommand data
|
* @param sd The subcommand data
|
||||||
* @param subcommand The subcommand text
|
|
||||||
* @param sync Whether the command was originally sync
|
* @param sync Whether the command was originally sync
|
||||||
* @throws Exception If something's not right
|
* @throws Exception If something's not right
|
||||||
*/
|
*/
|
||||||
private void handleCommandAsync(TP sender, String commandline, SubcommandData<TC> sd, String subcommand, boolean sync) throws Exception {
|
private void handleCommandAsync(TP sender, ParseResults<?> parsed, SubcommandData<TC> sd, boolean sync) throws Exception {
|
||||||
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;
|
||||||
|
@ -177,28 +176,12 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
val params = new ArrayList<Object>(sd.method.getParameterCount());
|
val params = new ArrayList<Object>(sd.method.getParameterCount());
|
||||||
int j = subcommand.length(), pj;
|
|
||||||
Class<?>[] parameterTypes = sd.method.getParameterTypes();
|
Class<?>[] parameterTypes = sd.method.getParameterTypes();
|
||||||
if (parameterTypes.length == 0)
|
if (parameterTypes.length == 0)
|
||||||
throw new Exception("No sender parameter for method '" + sd.method + "'");
|
throw new Exception("No sender parameter for method '" + sd.method + "'");
|
||||||
val sendertype = parameterTypes[0];
|
if (processSenderType(sender, sd, params, parameterTypes)) return; // Checks if the sender is the wrong type
|
||||||
final ChromaGamerBase cg;
|
|
||||||
if (sendertype.isAssignableFrom(sender.getClass()))
|
|
||||||
params.add(sender); //The command either expects a CommandSender or it is a Player, or some other expected type
|
|
||||||
else if (sender instanceof Command2MCSender
|
|
||||||
&& sendertype.isAssignableFrom(((Command2MCSender) sender).getSender().getClass()))
|
|
||||||
params.add(((Command2MCSender) sender).getSender());
|
|
||||||
else if (ChromaGamerBase.class.isAssignableFrom(sendertype)
|
|
||||||
&& sender instanceof Command2MCSender
|
|
||||||
&& (cg = ChromaGamerBase.getFromSender(((Command2MCSender) sender).getSender())) != null
|
|
||||||
&& cg.getClass() == sendertype) //The command expects a user of our system
|
|
||||||
params.add(cg);
|
|
||||||
else {
|
|
||||||
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
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
val paramArr = sd.method.getParameters();
|
val paramArr = sd.method.getParameters();
|
||||||
|
val args = parsed.getContext().getArguments();
|
||||||
for (int i1 = 1; i1 < parameterTypes.length; i1++) {
|
for (int i1 = 1; i1 < parameterTypes.length; i1++) {
|
||||||
Class<?> cl = parameterTypes[i1];
|
Class<?> cl = parameterTypes[i1];
|
||||||
pj = j + 1; //Start index
|
pj = j + 1; //Start index
|
||||||
|
@ -261,7 +244,7 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
||||||
}
|
}
|
||||||
params.add(cparam);
|
params.add(cparam);
|
||||||
}
|
}
|
||||||
Runnable lol = () -> {
|
Runnable invokeCommand = () -> {
|
||||||
try {
|
try {
|
||||||
sd.method.setAccessible(true); //It may be part of a private class
|
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)
|
val ret = sd.method.invoke(sd.command, params.toArray()); //I FORGOT TO TURN IT INTO AN ARRAY (for a long time)
|
||||||
|
@ -277,23 +260,76 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (sync)
|
if (sync)
|
||||||
Bukkit.getScheduler().runTask(MainPlugin.Instance, lol);
|
Bukkit.getScheduler().runTask(MainPlugin.Instance, invokeCommand);
|
||||||
else
|
else
|
||||||
lol.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) {
|
||||||
|
val sendertype = parameterTypes[0];
|
||||||
|
final ChromaGamerBase cg;
|
||||||
|
if (sendertype.isAssignableFrom(sender.getClass()))
|
||||||
|
params.add(sender); //The command either expects a CommandSender or it is a Player, or some other expected type
|
||||||
|
else if (sender instanceof Command2MCSender
|
||||||
|
&& sendertype.isAssignableFrom(((Command2MCSender) sender).getSender().getClass()))
|
||||||
|
params.add(((Command2MCSender) sender).getSender());
|
||||||
|
else if (ChromaGamerBase.class.isAssignableFrom(sendertype)
|
||||||
|
&& sender instanceof Command2MCSender
|
||||||
|
&& (cg = ChromaGamerBase.getFromSender(((Command2MCSender) sender).getSender())) != null
|
||||||
|
&& cg.getClass() == sendertype) //The command expects a user of our system
|
||||||
|
params.add(cg);
|
||||||
|
else {
|
||||||
|
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
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract void registerCommand(TC command);
|
public abstract void registerCommand(TC command);
|
||||||
|
|
||||||
protected List<SubcommandData<TC>> registerCommand(TC command, char commandChar) {
|
protected List<SubcommandData<TC>> registerCommand(TC command, char commandChar) {
|
||||||
return registerCommand(command, command.getCommandPath(), commandChar);
|
return registerCommand(command, dispatcher.register(getCommandNode(command)), commandChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<SubcommandData<TC>> registerCommand(TC command, String path, @SuppressWarnings("SameParameterValue") char commandChar) {
|
private LiteralArgumentBuilder<TP> getCommandNode(TC command) {
|
||||||
|
var path = command.getCommandPath().split(" ");
|
||||||
|
if (path.length == 0)
|
||||||
|
throw new IllegalArgumentException("Attempted to register a command with no command path!");
|
||||||
|
LiteralArgumentBuilder<TP> inner = literal(path[0]);
|
||||||
|
var outer = inner;
|
||||||
|
for (int i = path.length - 1; i >= 0; i--) {
|
||||||
|
LiteralArgumentBuilder<TP> literal = literal(path[i]);
|
||||||
|
outer = literal.executes(this::executeHelpText).then(outer);
|
||||||
|
}
|
||||||
|
var subcommandMethods = command.getClass().getMethods();
|
||||||
|
for (var subcommandMethod : subcommandMethods) {
|
||||||
|
var ann = subcommandMethod.getAnnotation(Subcommand.class);
|
||||||
|
if (ann == null) continue;
|
||||||
|
inner.then(getSubcommandNode(subcommandMethod, ann.helpText()));
|
||||||
|
}
|
||||||
|
return outer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LiteralArgumentBuilder<TP> getSubcommandNode(Method method, String[] helpText) {
|
||||||
|
LiteralArgumentBuilder<TP> ret = literal(method.getName());
|
||||||
|
return ret.executes(this::executeCommand); // TODO: CoreCommandNode helpText
|
||||||
|
}
|
||||||
|
|
||||||
|
private CoreArgumentBuilder<TP, ?> getCommandParameters(Parameter[] parameters) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private int executeHelpText(CommandContext<TP> context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private int executeCommand(CommandContext<TP> context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<SubcommandData<TC>> registerCommand(TC command, @SuppressWarnings("SameParameterValue") char commandChar) {
|
||||||
this.commandChar = commandChar;
|
this.commandChar = commandChar;
|
||||||
int x = path.indexOf(' ');
|
|
||||||
val mainPath = commandChar + path.substring(0, x == -1 ? path.length() : x);
|
|
||||||
//var scmdmap = subcommandStrings.computeIfAbsent(mainPath, k -> new HashSet<>()); //Used to display subcommands
|
|
||||||
val scmdHelpList = new ArrayList<String>();
|
|
||||||
Method mainMethod = null;
|
Method mainMethod = null;
|
||||||
boolean nosubs = true;
|
boolean nosubs = true;
|
||||||
boolean isSubcommand = x != -1;
|
boolean isSubcommand = x != -1;
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package buttondevteam.lib.chat;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.arguments.ArgumentType;
|
||||||
|
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CoreArgumentBuilder<S, T> extends ArgumentBuilder<S, CoreArgumentBuilder<S, T>> {
|
||||||
|
private final String name;
|
||||||
|
private final ArgumentType<T> type;
|
||||||
|
private final boolean optional;
|
||||||
|
private SuggestionProvider<S> suggestionsProvider = null;
|
||||||
|
private String[] helpText = null; // TODO: Don't need the help text for arguments
|
||||||
|
|
||||||
|
public CoreArgumentBuilder<S, T> suggests(SuggestionProvider<S> provider) {
|
||||||
|
this.suggestionsProvider = provider;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CoreArgumentBuilder<S, T> helps(String[] helpText) {
|
||||||
|
this.helpText = helpText;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected CoreArgumentBuilder<S, T> getThis() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CoreArgumentCommandNode<S, T> build() {
|
||||||
|
return new CoreArgumentCommandNode<>(name, type, getCommand(), getRequirement(), getRedirect(), getRedirectModifier(), isFork(), suggestionsProvider, optional, helpText);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package buttondevteam.lib.chat;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.Command;
|
||||||
|
import com.mojang.brigadier.RedirectModifier;
|
||||||
|
import com.mojang.brigadier.arguments.ArgumentType;
|
||||||
|
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||||
|
import com.mojang.brigadier.tree.ArgumentCommandNode;
|
||||||
|
import com.mojang.brigadier.tree.CommandNode;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public class CoreArgumentCommandNode<S, T> extends ArgumentCommandNode<S, T> {
|
||||||
|
private final boolean optional;
|
||||||
|
@lombok.Getter private final String[] helpText;
|
||||||
|
|
||||||
|
public CoreArgumentCommandNode(String name, ArgumentType<T> type, Command<S> command, Predicate<S> requirement, CommandNode<S> redirect, RedirectModifier<S> modifier, boolean forks, SuggestionProvider<S> customSuggestions, boolean optional, String[] helpText) {
|
||||||
|
super(name, type, command, requirement, redirect, modifier, forks, customSuggestions);
|
||||||
|
this.optional = optional;
|
||||||
|
this.helpText = helpText;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsageText() {
|
||||||
|
return optional ? "[" + getName() + "]" : "<" + getName() + ">";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredArgumentBuilder<S, T> createBuilder() {
|
||||||
|
return super.createBuilder();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package buttondevteam.lib.chat;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.Command;
|
||||||
|
import com.mojang.brigadier.RedirectModifier;
|
||||||
|
import com.mojang.brigadier.tree.CommandNode;
|
||||||
|
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public class CoreCommandNode<T> extends LiteralCommandNode<T> {
|
||||||
|
public CoreCommandNode(String literal, Command<T> command, Predicate<T> requirement, CommandNode<T> redirect, RedirectModifier<T> modifier, boolean forks, String helpText) {
|
||||||
|
super(literal, command, requirement, redirect, modifier, forks);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue