Added documentation and refactored commands.yml handling

- Added command argument help manager to read the arguments
- MC tab completion still needs to be fixed
This commit is contained in:
Norbi Peti 2023-02-04 02:30:44 +01:00
parent 47178e7f7c
commit db08d9baee
4 changed files with 103 additions and 70 deletions

View file

@ -3,6 +3,7 @@ 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.CommandArgumentHelpManager;
import buttondevteam.lib.chat.commands.NumberArg; 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;
@ -16,12 +17,10 @@ import com.mojang.brigadier.tree.LiteralCommandNode;
import lombok.RequiredArgsConstructor; 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.javatuples.Pair; import org.javatuples.Pair;
import org.javatuples.Triplet; import org.javatuples.Triplet;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.InputStreamReader;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -80,31 +79,6 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
public @interface OptionalArg { public @interface OptionalArg {
} }
/*protected static class SubcommandHelpData<T extends ICommand2> extends SubcommandData<T> {
private final TreeSet<String> ht = new TreeSet<>();
private BukkitTask task;
public SubcommandHelpData(Method method, T command, String[] helpText) {
super(method, command, helpText);
}
public void addSubcommand(String command) {
ht.add(command);
if (task == null)
task = Bukkit.getScheduler().runTask(MainPlugin.Instance, () -> {
helpText = new String[ht.size() + 1]; //This will only run after the server is started List<E> list = new ArrayList<E>(size());
helpText[0] = "§6---- Subcommands ----"; //TODO: There may be more to the help text
int i = 1;
for (Iterator<String> iterator = ht.iterator();
iterator.hasNext() && i < helpText.length; i++) {
String e = iterator.next();
helpText[i] = e;
}
task = null; //Run again, if needed
});
}
}*/
@RequiredArgsConstructor @RequiredArgsConstructor
protected static class ParamConverter<T> { protected static class ParamConverter<T> {
public final Function<String, T> converter; public final Function<String, T> converter;
@ -205,7 +179,7 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
if (ann == null) continue; if (ann == null) continue;
String methodPath = getCommandPath(meth.getName(), ' '); String methodPath = getCommandPath(meth.getName(), ' ');
val result = registerNodeFromPath(command.getCommandPath() + methodPath); val result = registerNodeFromPath(command.getCommandPath() + methodPath);
result.getValue0().addChild(getExecutableNode(meth, command, ann, result.getValue2())); result.getValue0().addChild(getExecutableNode(meth, command, ann, result.getValue2(), new CommandArgumentHelpManager<>(command)));
if (mainCommandNode == null) mainCommandNode = result.getValue1(); if (mainCommandNode == null) mainCommandNode = result.getValue1();
else if (!result.getValue1().getName().equals(mainCommandNode.getName())) { 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()); MainPlugin.Instance.getLogger().warning("Multiple commands are defined in the same class! This is not supported. Class: " + command.getClass().getSimpleName());
@ -225,8 +199,8 @@ 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, Subcommand ann, String path) { private LiteralCommandNode<TP> getExecutableNode(Method method, TC command, Subcommand ann, String path, CommandArgumentHelpManager<TC, TP> argHelpManager) {
val paramsAndSenderType = getCommandParameters(method); // Param order is important val paramsAndSenderType = getCommandParametersAndSender(method, argHelpManager); // Param order is important
val params = paramsAndSenderType.getValue0(); val params = paramsAndSenderType.getValue0();
val paramMap = new HashMap<String, CommandArgument>(); val paramMap = new HashMap<String, CommandArgument>();
for (val param : params) { for (val param : params) {
@ -237,7 +211,7 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
.executes(this::executeCommand); .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), param.optional)); parent.then(parent = CoreArgumentBuilder.argument(param.name, getArgumentType(param), param.optional));
} }
return node.build(); return node.build();
} }
@ -272,12 +246,12 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
* @return Parameter data objects and the sender type * @return Parameter data objects and the sender type
* @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 Pair<CommandArgument[], Class<?>> getCommandParameters(Method method) { private Pair<CommandArgument[], Class<?>> getCommandParametersAndSender(Method method, CommandArgumentHelpManager<TC, TP> argHelpManager) {
val parameters = method.getParameters(); 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 = argHelpManager.getParameterHelpForMethod(method);
val paramNames = usage != null ? usage.split(" ") : null; val paramNames = usage != null ? usage.split(" ") : null;
for (int i = 1; i < parameters.length; i++) { for (int i = 1; i < parameters.length; i++) {
val numAnn = parameters[i].getAnnotation(NumberArg.class); val numAnn = parameters[i].getAnnotation(NumberArg.class);
@ -290,7 +264,14 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
return new Pair<>(ret, parameters[0].getType()); return new Pair<>(ret, parameters[0].getType());
} }
private ArgumentType<?> getParameterType(CommandArgument arg) { /**
* Converts the Chroma representation of the argument declaration into Brigadier format.
* It does part of the command argument type processing.
*
* @param arg Our representation of the command argument
* @return The Brigadier representation of the command argument
*/
private ArgumentType<?> getArgumentType(CommandArgument arg) {
final Class<?> ptype = arg.type; final Class<?> ptype = arg.type;
Number lowerLimit = arg.limits.getValue0(), upperLimit = arg.limits.getValue1(); Number lowerLimit = arg.limits.getValue0(), upperLimit = arg.limits.getValue1();
if (arg.greedy) if (arg.greedy)
@ -316,11 +297,24 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
} }
} }
/**
* Displays the help text based on the executed command. Each command node might have a help text stored.
* The help text is displayed either because of incorrect usage or it's explicitly requested.
*
* @param context The command context
* @return Vanilla command success level (0)
*/
private int executeHelpText(CommandContext<TP> context) { private int executeHelpText(CommandContext<TP> context) {
System.out.println("Nodes:\n" + context.getNodes().stream().map(node -> node.getNode().getName() + "@" + node.getRange()).collect(Collectors.joining("\n"))); System.out.println("Nodes:\n" + context.getNodes().stream().map(node -> node.getNode().getName() + "@" + node.getRange()).collect(Collectors.joining("\n")));
return 0; return 0;
} }
/**
* Executes the command itself by calling the subcommand method associated with the input command node.
*
* @param context The command context
* @return Vanilla command success level (0)
*/
private int executeCommand(CommandContext<TP> context) { private int executeCommand(CommandContext<TP> context) {
System.out.println("Execute command"); System.out.println("Execute command");
System.out.println("Should be running sync: " + runOnPrimaryThread); System.out.println("Should be running sync: " + runOnPrimaryThread);
@ -378,31 +372,6 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
return 0; return 0;
} }
private String getParameterHelp(Method method) {
val str = method.getDeclaringClass().getResourceAsStream("/commands.yml");
if (str == null)
TBMCCoreAPI.SendException("Error while getting command data!", new Exception("Resource not found!"), MainPlugin.Instance);
else {
YamlConfiguration yc = YamlConfiguration.loadConfiguration(new InputStreamReader(str)); //Generated by ButtonProcessor
val ccs = yc.getConfigurationSection(method.getDeclaringClass().getCanonicalName().replace('$', '.'));
if (ccs != null) {
val cs = ccs.getConfigurationSection(method.getName());
if (cs != null) {
val mname = cs.getString("method");
val params = cs.getString("params");
int i = mname.indexOf('('); //Check only the name - the whole method is still stored for backwards compatibility and in case it may be useful
if (i != -1 && method.getName().equals(mname.substring(0, i)) && params != null) {
return params;
} else
TBMCCoreAPI.SendException("Error while getting command data for " + method + "!", new Exception("Method '" + method + "' != " + mname + " or params is " + params), MainPlugin.Instance);
} else
MainPlugin.Instance.getLogger().warning("Failed to get command data for " + method + " (cs is null)! Make sure to use 'clean install' when building the project.");
} else
MainPlugin.Instance.getLogger().warning("Failed to get command data for " + method + " (ccs is null)! Make sure to use 'clean install' when building the project.");
}
return null;
}
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() {

View file

@ -170,17 +170,6 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implemen
.map(comp -> component.getClass().getSimpleName().equals(comp.getClass().getSimpleName())).orElse(false), true); .map(comp -> component.getClass().getSimpleName().equals(comp.getClass().getSimpleName())).orElse(false), true);
} }
/*@EventHandler
public void onTabComplete(TabCompleteEvent event) {
try {
event.getCompletions().clear(); //Remove player names
} catch (UnsupportedOperationException e) {
//System.out.println("Tabcomplete: " + event.getBuffer());
//System.out.println("First completion: " + event.getCompletions().stream().findFirst().orElse("no completions"));
//System.out.println("Listeners: " + Arrays.toString(event.getHandlers().getRegisteredListeners()));
}
}*/
@Override @Override
public boolean handleCommand(Command2MCSender sender, String commandline) { public boolean handleCommand(Command2MCSender sender, String commandline) {
return handleCommand(sender, commandline, true); return handleCommand(sender, commandline, true);

View file

@ -7,6 +7,13 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.function.Function; import java.util.function.Function;
/**
* This class is used as a base class for all the specific command implementations.
* It primarily holds information about the command itself and how it should be run, ideally in a programmer-friendly way.
* Any inferred and processed information about this command will be stored in the command manager (Command2*).
*
* @param <TP> The sender's type
*/
public abstract class ICommand2<TP extends Command2Sender> { public abstract class ICommand2<TP extends Command2Sender> {
/** /**
* Default handler for commands, can be used to copy the args too. * Default handler for commands, can be used to copy the args too.
@ -14,6 +21,7 @@ public abstract class ICommand2<TP extends Command2Sender> {
* @param sender The sender which ran the command * @param sender The sender which ran the command
* @return The success of the command * @return The success of the command
*/ */
@SuppressWarnings("unused")
public boolean def(TP sender) { public boolean def(TP sender) {
return false; return false;
} }

View file

@ -0,0 +1,67 @@
package buttondevteam.lib.chat.commands;
import buttondevteam.core.MainPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.Command2Sender;
import buttondevteam.lib.chat.ICommand2;
import lombok.val;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
/**
* Deals with reading the commands.yml file from the plugin. The file is generated by ButtonProcessor at compile-time.
* Only used when registering commands.
*/
public class CommandArgumentHelpManager<TC extends ICommand2<TP>, TP extends Command2Sender> {
private ConfigurationSection commandConfig;
/**
* Read the yaml file for the given command class.
*
* @param command The command object to use
*/
public CommandArgumentHelpManager(TC command) {
val commandClass = command.getClass();
// It will load it for each class, but it would be complicated to solve that
// Most plugins don't have a lot of command classes anyway
try (val str = commandClass.getResourceAsStream("/commands.yml")) {
if (str == null) {
TBMCCoreAPI.SendException("Error while getting command data!", new Exception("Resource not found!"), MainPlugin.Instance);
return;
}
val config = YamlConfiguration.loadConfiguration(new InputStreamReader(str));
commandConfig = config.getConfigurationSection(commandClass.getCanonicalName().replace('$', '.'));
if (commandConfig == null) {
MainPlugin.Instance.getLogger().warning("Failed to get command data for " + commandClass + "! Make sure to use 'clean install' when building the project.");
}
} catch (IOException e) {
TBMCCoreAPI.SendException("Error while getting command data!", e, MainPlugin.Instance);
}
}
/**
* Returns a parameter help string for the given subcommand method by reading it from the plugin.
*
* @param method The subcommand method
* @return The parameter part of the usage string for the command
*/
public String getParameterHelpForMethod(Method method) {
val cs = commandConfig.getConfigurationSection(method.getName());
if (cs == null) {
MainPlugin.Instance.getLogger().warning("Failed to get command data for " + method + "! Make sure to use 'clean install' when building the project.");
return null;
}
val mname = cs.getString("method");
val params = cs.getString("params");
int i = mname.indexOf('('); //Check only the name - the whole method is still stored for backwards compatibility and in case it may be useful
if (i != -1 && method.getName().equals(mname.substring(0, i)) && params != null) {
return params;
} else
TBMCCoreAPI.SendException("Error while getting command data for " + method + "!", new Exception("Method '" + method + "' != " + mname + " or params is " + params), MainPlugin.Instance);
return null;
}
}