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 com.google.common.base.Defaults;
|
||||
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.RequiredArgsConstructor;
|
||||
import lombok.val;
|
||||
|
@ -20,6 +24,7 @@ import java.lang.annotation.RetentionPolicy;
|
|||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -29,6 +34,8 @@ import java.util.List;
|
|||
import java.util.function.Function;
|
||||
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 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<?>> {
|
||||
public final Method method;
|
||||
public final T command;
|
||||
public final String[] parameters;
|
||||
public String[] helpText;
|
||||
}
|
||||
|
||||
|
@ -116,10 +122,9 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
|||
public final Supplier<Iterable<String>> allSupplier;
|
||||
}
|
||||
|
||||
protected final HashMap<String, SubcommandData<TC>> subcommands = new HashMap<>();
|
||||
protected final HashMap<Class<?>, ParamConverter<?>> paramConverters = new HashMap<>();
|
||||
|
||||
private final ArrayList<String> commandHelp = new ArrayList<>(); //Mainly needed by Discord
|
||||
private final CommandDispatcher<TP> dispatcher = new CommandDispatcher<>();
|
||||
|
||||
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) {
|
||||
for (int i = commandline.length(); i != -1; i = commandline.lastIndexOf(' ', i - 1)) {
|
||||
String subcommand = commandline.substring(0, i).toLowerCase();
|
||||
SubcommandData<TC> sd = subcommands.get(subcommand);
|
||||
if (sd == null) continue;
|
||||
var results = dispatcher.parse(commandline, sender);
|
||||
boolean sync = Bukkit.isPrimaryThread();
|
||||
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, () -> {
|
||||
try {
|
||||
handleCommandAsync(sender, commandline, sd, subcommand, sync);
|
||||
handleCommandAsync(sender, results, results.getContext().getNodes(), 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
|
||||
}
|
||||
return false;
|
||||
return true; //We found a method - TODO
|
||||
}
|
||||
|
||||
//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
|
||||
*
|
||||
* @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 subcommand The subcommand text
|
||||
* @param sync Whether the command was originally sync
|
||||
* @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
|
||||
sender.sendMessage(sd.helpText);
|
||||
return;
|
||||
|
@ -177,28 +176,12 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
|||
return;
|
||||
}
|
||||
val params = new ArrayList<Object>(sd.method.getParameterCount());
|
||||
int j = subcommand.length(), pj;
|
||||
Class<?>[] parameterTypes = sd.method.getParameterTypes();
|
||||
if (parameterTypes.length == 0)
|
||||
throw new Exception("No sender parameter for method '" + sd.method + "'");
|
||||
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;
|
||||
}
|
||||
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();
|
||||
for (int i1 = 1; i1 < parameterTypes.length; i1++) {
|
||||
Class<?> cl = parameterTypes[i1];
|
||||
pj = j + 1; //Start index
|
||||
|
@ -261,7 +244,7 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
|||
}
|
||||
params.add(cparam);
|
||||
}
|
||||
Runnable lol = () -> {
|
||||
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)
|
||||
|
@ -277,23 +260,76 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
|
|||
}
|
||||
};
|
||||
if (sync)
|
||||
Bukkit.getScheduler().runTask(MainPlugin.Instance, lol);
|
||||
Bukkit.getScheduler().runTask(MainPlugin.Instance, invokeCommand);
|
||||
else
|
||||
lol.run();
|
||||
invokeCommand.run();
|
||||
} //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);
|
||||
|
||||
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;
|
||||
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;
|
||||
boolean nosubs = true;
|
||||
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