Listing subcommands, other fixes

Member command converted
Now requiring the permissions plugin
Added support for optional arguments
Automatically sending an error if parameter conversion fails
Automatically sending the help text if there aren't enough args
Automatically converting the first line of the help text to a header
This commit is contained in:
Norbi Peti 2019-02-03 00:33:38 +01:00
parent ab24480f89
commit e32805c1fc
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
10 changed files with 137 additions and 90 deletions

View file

@ -2,6 +2,7 @@ package buttondevteam.core;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.Command2.Subcommand;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.ICommand2MC;
@ -13,29 +14,27 @@ import org.bukkit.plugin.Plugin;
import java.util.Optional;
@CommandClass(modOnly = true, helpText = {
"§6---- Component command ----",
"Component command",
"Can be used to enable/disable/list components"
})
public class ComponentCommand extends ICommand2MC {
public ComponentCommand() {
getManager().addParamConverter(Plugin.class, arg -> Bukkit.getPluginManager().getPlugin(arg));
getManager().addParamConverter(Plugin.class, arg -> Bukkit.getPluginManager().getPlugin(arg), "Plugin not found!");
}
@Subcommand
public boolean enable(CommandSender sender, Plugin plugin, String component) {
if (plugin == null) return respond(sender, "§cPlugin not found!");
plugin.reloadConfig(); //Reload config so the new config values are read - All changes are saved to disk on disable
return enable_disable(sender, plugin, component, true);
}
@Subcommand
public boolean disable(CommandSender sender, Plugin plugin, String component) {
if (plugin == null) return respond(sender, "§cPlugin not found!");
return enable_disable(sender, plugin, component, false);
}
@Subcommand
public boolean list(CommandSender sender, String plugin) {
public boolean list(CommandSender sender, @Command2.OptionalArg String plugin) {
sender.sendMessage("§6List of components:");
Component.getComponents().values().stream().filter(c -> plugin == null || c.getPlugin().getName().equalsIgnoreCase(plugin)) //If plugin is null, don't check
.map(c -> c.getPlugin().getName() + " - " + c.getClass().getSimpleName() + " - " + (c.isEnabled() ? "en" : "dis") + "abled").forEach(sender::sendMessage);

View file

@ -14,7 +14,6 @@ import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.chat.Color;
import buttondevteam.lib.chat.Command2MC;
import buttondevteam.lib.chat.TBMCChatAPI;
import buttondevteam.lib.player.ChromaGamerBase;
import buttondevteam.lib.player.TBMCPlayer;
@ -30,7 +29,6 @@ import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.RegisteredServiceProvider;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@ -42,7 +40,6 @@ import java.util.logging.Logger;
public class MainPlugin extends ButtonPlugin {
public static MainPlugin Instance;
@Nullable
public static Permission permission;
public static boolean Test;
public static Essentials ess;
@ -59,7 +56,8 @@ public class MainPlugin extends ButtonPlugin {
Instance = this;
PluginDescriptionFile pdf = getDescription();
logger = getLogger();
setupPermissions();
if (!setupPermissions())
throw new NullPointerException("No permission plugin found!");
Test = getConfig().getBoolean("test", true);
saveConfig();
Component.registerComponent(this, new PluginUpdaterComponent());
@ -69,7 +67,7 @@ public class MainPlugin extends ButtonPlugin {
Component.registerComponent(this, new MemberComponent());
Component.registerComponent(this, new TownyComponent());
ComponentManager.enableComponents();
Command2MC.registerCommand(new ComponentCommand());
getCommand2MC().registerCommand(new ComponentCommand());
TBMCCoreAPI.RegisterEventsForExceptions(new PlayerListener(), this);
ChromaGamerBase.addConverter(commandSender -> Optional.ofNullable(commandSender instanceof ConsoleCommandSender || commandSender instanceof BlockCommandSender
? TBMCPlayer.getPlayer(new UUID(0, 0), TBMCPlayer.class) : null)); //Console & cmdblocks

View file

@ -3,7 +3,7 @@ package buttondevteam.core;
import buttondevteam.lib.TBMCCommandPreprocessEvent;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.TBMCSystemChatEvent;
import buttondevteam.lib.chat.Command2MC;
import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.player.TBMCPlayerBase;
import lombok.val;
import org.bukkit.Bukkit;
@ -64,7 +64,7 @@ public class PlayerListener implements Listener {
public void onTBMCPreprocess(TBMCCommandPreprocessEvent event) {
if (event.isCancelled()) return;
try {
event.setCancelled(Command2MC.handleCommand(event.getSender(), event.getMessage()));
event.setCancelled(ButtonPlugin.getCommand2MC().handleCommand(event.getSender(), event.getMessage()));
} catch (Exception e) {
TBMCCoreAPI.SendException("Command processing failed for sender '" + event.getSender() + "' and message '" + event.getMessage() + "'", e);
}

View file

@ -1,56 +1,47 @@
package buttondevteam.core.component.members;
import buttondevteam.core.MainPlugin;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.TBMCCommandBase;
import lombok.val;
import buttondevteam.lib.chat.ICommand2MC;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
@CommandClass(modOnly = true, path = "member")
public class MemberCommand extends TBMCCommandBase {
@Override
public boolean OnCommand(CommandSender sender, String alias, String[] args) {
if (args.length < 2)
return false;
final boolean add;
if (args[0].equalsIgnoreCase("add"))
add = true;
else if (args[0].equalsIgnoreCase("remove"))
add = false;
else
return false;
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, () -> {
if (MainPlugin.permission == null) {
sender.sendMessage("§cError: No permission plugin found!");
return;
@CommandClass(modOnly = true, path = "member", helpText = { //
"Member command", //
"Add or remove server members.", //
})
public class MemberCommand extends ICommand2MC {
private final MemberComponent component;
public MemberCommand(MemberComponent component) {
getManager().addParamConverter(OfflinePlayer.class, Bukkit::getOfflinePlayer, "Player not found!");
this.component = component;
}
val op = Bukkit.getOfflinePlayer(args[1]);
@Command2.Subcommand
public boolean add(CommandSender sender, OfflinePlayer player) {
return addRemove(sender, player, true);
}
@Command2.Subcommand
public boolean remove(CommandSender sender, OfflinePlayer player) {
return addRemove(sender, player, false);
}
public boolean addRemove(CommandSender sender, OfflinePlayer op, boolean add) {
Bukkit.getScheduler().runTaskAsynchronously(MainPlugin.Instance, () -> {
if (!op.hasPlayedBefore()) {
sender.sendMessage("§cCannot find player or haven't played before.");
return;
}
if (add) {
if (MainPlugin.permission.playerAddGroup(null, op, "member"))
sender.sendMessage("§b" + op.getName() + " added as a member!");
if (add ? MainPlugin.permission.playerAddGroup(null, op, component.memberGroup().get())
: MainPlugin.permission.playerRemoveGroup(null, op, component.memberGroup().get()))
sender.sendMessage("§b" + op.getName() + " " + (add ? "added" : "removed") + " as a member!");
else
sender.sendMessage("§cFailed to add " + op.getName() + " as a member!");
} else {
if (MainPlugin.permission.playerRemoveGroup(null, op, "member"))
sender.sendMessage("§b" + op.getName() + " removed as a member!");
else
sender.sendMessage("§bFailed to remove " + op.getName() + " as a member!");
}
sender.sendMessage("§cFailed to " + (add ? "add" : "remove") + " " + op.getName() + " as a member!");
});
return true;
}
@Override
public String[] GetHelpText(String alias) {
return new String[]{ //
"06---- Member ----", //
"Add or remove server members.", //
"Usage: /member <add|remove> <player>" //
};
}
}

View file

@ -15,14 +15,14 @@ import java.util.Date;
import static buttondevteam.core.MainPlugin.permission;
public class MemberComponent extends Component implements Listener {
private ConfigData<String> memberGroup() {
ConfigData<String> memberGroup() {
return getConfig().getData("memberGroup", "member");
}
@Override
protected void enable() {
registerListener(this);
registerCommand(new MemberCommand());
registerCommand(new MemberCommand(this));
}
@Override

View file

@ -3,7 +3,7 @@ package buttondevteam.lib.architecture;
import buttondevteam.core.ComponentManager;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.exceptions.UnregisteredComponentException;
import buttondevteam.lib.chat.ICommand2;
import buttondevteam.lib.chat.ICommand2MC;
import buttondevteam.lib.chat.TBMCChatAPI;
import buttondevteam.lib.chat.TBMCCommandBase;
import lombok.Getter;
@ -197,7 +197,7 @@ public abstract class Component {
*
* @param commandBase Custom coded command class
*/
protected final void registerCommand(ICommand2 commandBase) {
protected final void registerCommand(ICommand2MC commandBase) {
ButtonPlugin.getCommand2MC().registerCommand(commandBase);
}

View file

@ -22,11 +22,13 @@ import java.util.stream.Collectors;
/**
* The method name is the subcommand, use underlines (_) to add further subcommands.
* The args may be null if the conversion failed.
* The args may be null if the conversion failed and it's optional.
*/
public abstract class Command2 {
public abstract class Command2<TC extends ICommand2> {
protected Command2() {
}
/**
* TODO: @CommandClass(helpText=...)
* Parameters annotated with this receive all of the remaining arguments
*/
@Target(ElementType.PARAMETER)
@ -46,12 +48,24 @@ public abstract class Command2 {
String[] helpText() default {};
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface OptionalArg {
}
@RequiredArgsConstructor
protected static class SubcommandData<T extends ICommand2> {
public final Method method;
public final T command;
public final String[] helpText;
}
@RequiredArgsConstructor
protected static class ParamConverter<T> {
public final Function<String, T> converter;
public final String errormsg;
}
/**
* Adds a param converter that obtains a specific object from a string parameter.
* The converter may return null.
@ -60,20 +74,29 @@ public abstract class Command2 {
* @param converter The converter to use
* @param <T> The type of the result
*/
public abstract <T> void addParamConverter(Class<T> cl, Function<String, T> converter);
public abstract <T> void addParamConverter(Class<T> cl, Function<String, T> converter, String errormsg);
protected <T> void addParamConverter(Class<T> cl, Function<String, T> converter, HashMap<Class<?>, Function<String, ?>> map) {
map.put(cl, converter);
protected <T> void addParamConverter(Class<T> cl, Function<String, T> converter, String errormsg, HashMap<Class<?>, ParamConverter<?>> map) {
map.put(cl, new ParamConverter<>(converter, errormsg));
}
public abstract boolean handleCommand(CommandSender sender, String commandLine) throws Exception;
protected <T extends ICommand2> boolean handleCommand(CommandSender sender, String commandline,
HashMap<String, SubcommandData<T>> subcommands, HashMap<Class<?>, Function<String, ?>> paramConverters) throws Exception {
protected boolean handleCommand(CommandSender sender, String commandline,
HashMap<String, SubcommandData<TC>> subcommands,
HashMap<Class<?>, ParamConverter<?>> paramConverters) throws Exception {
for (int i = commandline.length(); i != -1; i = commandline.lastIndexOf(' ', i - 1)) {
String subcommand = commandline.substring(0, i).toLowerCase();
SubcommandData sd = subcommands.get(subcommand); //O(1)
if (sd == null) continue; //TODO: This will run each time someone runs any command
SubcommandData<TC> sd = subcommands.get(subcommand); //O(1)
if (sd == null) continue;
if (sd.method == null || sd.command == null) { //Main command not registered, but we have subcommands
sender.sendMessage(sd.helpText);
return true;
}
if (!hasPermission(sender, sd.command)) {
sender.sendMessage("§cYou don't have permission to use this command");
return true;
}
val params = new ArrayList<Object>(sd.method.getParameterCount());
int j = subcommand.length(), pj;
Class<?>[] parameterTypes = sd.method.getParameterTypes();
@ -91,12 +114,18 @@ public abstract class Command2 {
sender.sendMessage("§cYou need to be a " + sendertype.getSimpleName() + " to use this command.");
return true;
}
val paramArr = sd.method.getParameters();
for (int i1 = 1; i1 < parameterTypes.length; i1++) {
Class<?> cl = parameterTypes[i1];
pj = j + 1; //Start index
if (pj == commandline.length() + 1) { //No param given
if (paramArr[i1].isAnnotationPresent(OptionalArg.class)) {
params.add(null);
continue; //Fill the remaining params with nulls
} else {
sender.sendMessage(sd.helpText); //Required param missing
return true;
}
}
j = commandline.indexOf(' ', j + 1); //End index
if (j == -1) //Last parameter
@ -109,7 +138,12 @@ public abstract class Command2 {
val conv = paramConverters.get(cl);
if (conv == null)
throw new Exception("No suitable converter found for parameter type '" + cl.getCanonicalName() + "' for command '" + sd.method.toString() + "'");
params.add(conv.apply(param));
val cparam = conv.converter.apply(param);
if (cparam == null) {
sender.sendMessage(conv.errormsg); //Param conversion failed - ex. plugin not found
return true;
}
params.add(cparam);
}
//System.out.println("Our params: "+params);
val ret = sd.method.invoke(sd.command, params.toArray()); //I FORGOT TO TURN IT INTO AN ARRAY (for a long time)
@ -123,21 +157,26 @@ public abstract class Command2 {
return false; //Didn't handle
} //TODO: Add to the help
public abstract void registerCommand(ICommand2 command);
public abstract void registerCommand(TC command);
protected <T extends ICommand2> void registerCommand(T command, HashMap<String, SubcommandData<T>> subcommands, char commandChar) {
protected void registerCommand(TC command, HashMap<String, SubcommandData<TC>> subcommands, char commandChar) {
val path = command.getCommandPath();
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;
try { //Register the default handler first so it can be reliably overwritten
val method = command.getClass().getMethod("def", CommandSender.class, String.class);
mainMethod = command.getClass().getMethod("def", CommandSender.class, String.class);
val cc = command.getClass().getAnnotation(CommandClass.class);
var ht = cc == null ? new String[0] : cc.helpText();
String[] both = Arrays.copyOf(ht, ht.length + 1);
both[ht.length] = "Usage: " + commandChar + path; //TODO: Print subcommands
ht = both;
subcommands.put(commandChar + path, new SubcommandData<>(method, command, ht)); //TODO: Disable components when the plugin is disabled
if (ht.length > 0)
ht[0] = "§6---- " + ht[0] + " ----";
scmdHelpList.addAll(Arrays.asList(ht));
scmdHelpList.add("§6Subcommands:");
} catch (Exception e) {
TBMCCoreAPI.SendException("Could not register default handler for command /" + path, e);
} //Continue on
}
for (val method : command.getClass().getMethods()) {
val ann = method.getAnnotation(Subcommand.class);
if (ann != null) {
@ -147,15 +186,22 @@ public abstract class Command2 {
(method.getName().equals("def") ? "" : " " + method.getName().replace('_', ' ').toLowerCase()); //Add method name, unless it's 'def'
ht = getHelpText(method, ht, subcommand);
subcommands.put(subcommand, new SubcommandData<>(method, command, ht)); //Result of the above (def) is that it will show the help text
scmdHelpList.add(subcommand);
}
}
if (mainMethod != null && !subcommands.containsKey(commandChar + path)) //Command specified by the class
subcommands.put(commandChar + path, new SubcommandData<>(mainMethod, command, scmdHelpList.toArray(new String[0])));
if (mainMethod != null && !subcommands.containsKey(mainPath)) //Main command, typically the same as the above
subcommands.put(mainPath, new SubcommandData<>(null, null, scmdHelpList.toArray(new String[0])));
}
private static String[] getHelpText(Method method, String[] ht, String subcommand) { //TODO: helpText[0]="§6---- "+helpText[0]+" ----";
private String[] getHelpText(Method method, String[] ht, String subcommand) {
val str = method.getDeclaringClass().getResourceAsStream("/commands.yml");
if (str == null)
TBMCCoreAPI.SendException("Error while getting command data!", new Exception("Resource not found!"));
else {
if (ht.length > 0)
ht[0] = "§6---- " + ht[0] + " ----";
YamlConfiguration yc = YamlConfiguration.loadConfiguration(new InputStreamReader(str)); //Generated by ButtonProcessor
val ccs = yc.getConfigurationSection(method.getDeclaringClass().getCanonicalName());
if (ccs != null) {
@ -166,7 +212,7 @@ public abstract class Command2 {
val goodname = method.getName() + "(" + Arrays.stream(method.getParameterTypes()).map(cl -> cl.getCanonicalName()).collect(Collectors.joining(",")) + ")";
if (goodname.equals(mname) && params != null) {
String[] both = Arrays.copyOf(ht, ht.length + 1);
both[ht.length] = "Usage: " + subcommand + " " + params;
both[ht.length] = "§6Usage:§r " + subcommand + " " + params;
ht = both;
} else
TBMCCoreAPI.SendException("Error while getting command data for " + method + "!", new Exception("Method '" + method.toString() + "' != " + mname + " or params is " + params));
@ -178,5 +224,5 @@ public abstract class Command2 {
return ht;
}
public abstract boolean hasPermission(CommandSender sender, ICommand2 command);
public abstract boolean hasPermission(CommandSender sender, TC command);
} //TODO: Test support of Player instead of CommandSender

View file

@ -1,24 +1,36 @@
package buttondevteam.lib.chat;
import buttondevteam.core.MainPlugin;
import org.bukkit.command.CommandSender;
import java.util.HashMap;
import java.util.function.Function;
public class Command2MC extends Command2 {
private HashMap<String, SubcommandData<ICommand2>> subcommands = new HashMap<>();
private HashMap<Class<?>, Function<String, ?>> paramConverters = new HashMap<>();
public class Command2MC extends Command2<ICommand2MC> {
private HashMap<String, SubcommandData<ICommand2MC>> subcommands = new HashMap<>();
private HashMap<Class<?>, ParamConverter<?>> paramConverters = new HashMap<>();
@Override
public boolean handleCommand(CommandSender sender, String commandLine) throws Exception {
return handleCommand(sender, commandLine, subcommands, paramConverters);
}
public void registerCommand(ICommand2 command) {
@Override
public void registerCommand(ICommand2MC command) {
registerCommand(command, subcommands, '/');
}
public <T> void addParamConverter(Class<T> cl, Function<String, T> converter) {
addParamConverter(cl, converter, paramConverters);
@Override
public boolean hasPermission(CommandSender sender, ICommand2MC command) {
return MainPlugin.permission.has(sender, "thorpe.command." + command.getCommandPath().replace(' ', '.'));
}
/**
* Automatically colors the message red.
* {@see super#addParamConverter}
*/
@Override
public <T> void addParamConverter(Class<T> cl, Function<String, T> converter, String errormsg) {
addParamConverter(cl, converter, "§c" + errormsg, paramConverters);
}
}

View file

@ -38,7 +38,8 @@ public @interface CommandClass {
boolean excludeFromPath() default false;
/**
* The help text to show for the players. A usage message will be also shown below it.
* The help text to show for the players. A usage message will be also shown below it.<br>
* <b>The fist line will be converted to a header.</b>
*
* @return The help text
*/

View file

@ -31,9 +31,9 @@ public abstract class ICommand2 {
private final String path;
@Getter
private final Command2 manager;
private final Command2<?> manager; //TIL that if I use a raw type on a variable then none of the type args will work (including what's defined on a method, not on the type)
public ICommand2(Command2 manager) {
public <T extends ICommand2> ICommand2(Command2<T> manager) {
path = getcmdpath();
this.manager = manager;
}