cat.nyaa.nyaacore.cmdreceiver.CommandReceiver Maven / Gradle / Ivy
Show all versions of rpgitems Show documentation
package cat.nyaa.nyaacore.cmdreceiver;
import cat.nyaa.nyaacore.ILocalizer;
import cat.nyaa.nyaacore.LanguageRepository;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public abstract class CommandReceiver implements CommandExecutor, TabCompleter {
// Language class is passed in for message support
private final ILocalizer i18n;
// All subcommands
private final Map subCommands = new HashMap<>();
// Default subcommand
private SubCommandInfo defaultSubCommand = null;
/**
* @param plugin for logging purpose only
* @param _i18n
*/
@SuppressWarnings("rawtypes")
public CommandReceiver(Plugin plugin, ILocalizer _i18n) {
if (plugin == null) throw new IllegalArgumentException();
if (_i18n == null)
_i18n = new LanguageRepository.InternalOnlyRepository(plugin);
this.i18n = _i18n;
// Collect all methods
Class cls = getClass();
Set allMethods = new HashSet<>();
while (cls != null) {
allMethods.addAll(Arrays.asList(cls.getDeclaredMethods()));
cls = cls.getSuperclass();
}
// Collect all fields
cls = getClass();
Set allFields = new HashSet<>();
while (cls != null) {
allFields.addAll(Arrays.asList(cls.getDeclaredFields()));
cls = cls.getSuperclass();
}
Stream.concat(
allMethods.stream().map(m -> parseSubCommandAnnotation(plugin, m)),
allFields.stream().map(f -> parseSubCommandAnnotation(plugin, f))
).forEach(scInfo -> {
if (scInfo == null) return;
if (scInfo.name != null) {
if (subCommands.containsKey(scInfo.name)) {
// TODO dup sub command
}
subCommands.put(scInfo.name, scInfo);
}
if (scInfo.isDefault) {
if (defaultSubCommand != null) {
// TODO dup default subcommand
}
defaultSubCommand = scInfo;
}
});
}
public boolean registerCommand(SubCommandInfo command, boolean override) {
boolean contains = subCommands.containsKey(command.name);
if (!override && contains) return false;
subCommands.put(command.name, command);
return contains;
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static CommandReceiver newInstance(Class cls, Object arg1, Object arg2) throws ReflectiveOperationException {
for (Constructor c : cls.getConstructors()) {
if (c.getParameterCount() == 2 &&
c.getParameterTypes()[0].isAssignableFrom(arg1.getClass()) &&
c.getParameterTypes()[1].isAssignableFrom(arg2.getClass())) {
return (CommandReceiver) c.newInstance(arg1, arg2);
}
}
throw new NoSuchMethodException("no matching constructor found");
}
public static Player asPlayer(CommandSender target) {
if (target instanceof Player) {
return (Player) target;
} else {
throw new NotPlayerException();
}
}
public static ItemStack getItemInHand(CommandSender se) {
if (se instanceof Player p) {
if (p.getInventory() != null) {
ItemStack i = p.getInventory().getItemInMainHand();
if (i != null && i.getType() != Material.AIR) {
return i;
}
}
throw new NoItemInHandException(false);
} else {
throw new NotPlayerException();
}
}
public static ItemStack getItemInOffHand(CommandSender se) {
if (se instanceof Player p) {
if (p.getInventory() != null) {
ItemStack i = p.getInventory().getItemInOffHand();
if (i != null && i.getType() != Material.AIR) {
return i;
}
}
throw new NoItemInHandException(true);
} else {
throw new NotPlayerException();
}
}
// Scan recursively into parent class to find annotated methods when constructing
/**
* This prefix will be used to locate the correct manual item.
* If the class is registered to bukkit directly, you should return a empty string.
* If the class is registered through @SubCommand annotation, you should return the subcommand name.
* If it's a nested subcommand, separate the prefixes using dot.
*
* @return the prefix
*/
public abstract String getHelpPrefix();
/**
* @return should {@link CommandReceiver#acceptCommand(CommandSender, Arguments)} print the default "Success" message after executing a command
*/
protected boolean showCompleteMessage() {
return false;
}
/**
* @param plugin for logging purpose only
* @param m
* @return
*/
private SubCommandInfo parseSubCommandAnnotation(Plugin plugin, Method m) {
SubCommand scAnno = m.getAnnotation(SubCommand.class);
if (scAnno == null) return null;
Class[] params = m.getParameterTypes();
if (!(params.length == 2 &&
params[0] == CommandSender.class &&
params[1] == Arguments.class)) {
plugin.getLogger().warning(i18n.getFormatted("internal.error.bad_subcommand", m.toString()));
return null; // incorrect method signature
}
m.setAccessible(true);
Method tabm = null;
if (!scAnno.tabCompleter().isEmpty()) {
try {
tabm = m.getDeclaringClass().getDeclaredMethod(scAnno.tabCompleter(), CommandSender.class, Arguments.class);
tabm.setAccessible(true);
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
plugin.getLogger().warning(i18n.getFormatted("internal.error.bad_subcommand", m.toString()));
return null;
}
}
if (!scAnno.value().isEmpty() && scAnno.isDefaultCommand()) {
// cannot be both subcommand and default command
plugin.getLogger().warning(i18n.getFormatted("internal.error.bad_subcommand", m.toString()));
return null;
} else if (!scAnno.value().isEmpty()) {
// subcommand
String subCommandName = scAnno.value();
String perm = scAnno.permission().isEmpty() ? null : scAnno.permission();
return new SubCommandInfo(this, subCommandName, perm, false, m, null, null, false, tabm);
} else if (scAnno.isDefaultCommand()) {
// default command
String perm = scAnno.permission().isEmpty() ? null : scAnno.permission();
return new SubCommandInfo(this, null, perm, false, m, null, null, true, tabm);
} else {
// not subcommand nor default command, remove the annotation
plugin.getLogger().warning(i18n.getFormatted("internal.error.bad_subcommand", m.toString()));
return null;
}
}
//
// public List getSubcommands() {
// ArrayList ret = new ArrayList<>();
// ret.addAll(subCommands.keySet());
// if (defaultSubCommand != null && defaultSubCommand.name == null) {
// ret.add("");
// }
// ret.sort(String::compareTo);
// return ret;
// }
/**
* @param plugin for logging purpose only
* @param f
* @return
*/
private SubCommandInfo parseSubCommandAnnotation(Plugin plugin, Field f) {
SubCommand scAnno = f.getAnnotation(SubCommand.class);
if (scAnno == null) return null;
if (!CommandReceiver.class.isAssignableFrom(f.getType())) {
plugin.getLogger().warning(i18n.getFormatted("internal.error.bad_subcommand", f.toString()));
return null; // incorrect field type
}
if (!scAnno.tabCompleter().isEmpty()) {
plugin.getLogger().warning(i18n.getFormatted("internal.error.bad_subcommand", f.toString()));
return null; // field-based subcommand does not need method-based tabcompletion
}
// try to instantiate sub command receiver
CommandReceiver obj;
try {
obj = newInstance(f.getType(), plugin, i18n);
f.setAccessible(true);
f.set(this, obj);
} catch (ReflectiveOperationException ex) {
plugin.getLogger().warning(i18n.getFormatted("internal.error.bad_subcommand", f.toString()));
ex.printStackTrace();
return null;
}
if (!scAnno.value().isEmpty() && scAnno.isDefaultCommand()) {
// cannot be both subcommand and default command
plugin.getLogger().warning(i18n.getFormatted("internal.error.bad_subcommand", f.toString()));
return null;
} else if (!scAnno.value().isEmpty()) {
// subcommand
String subCommandName = scAnno.value();
String perm = scAnno.permission().isEmpty() ? null : scAnno.permission();
return new SubCommandInfo(this, subCommandName, perm, true, null, f, obj, false, null);
} else if (scAnno.isDefaultCommand()) {
// default command
String perm = scAnno.permission().isEmpty() ? null : scAnno.permission();
return new SubCommandInfo(this, null, perm, true, null, f, obj, true, null);
} else {
// not subcommand nor default command, remove the annotation
plugin.getLogger().warning(i18n.getFormatted("internal.error.bad_subcommand", f.toString()));
return null;
}
}
/*
* Code path looks like this:
* - Bukkit => CmdRecv:onCommand => CmdRecv:acceptCommand => SubCmdRecv:acceptCommand => SubCmdRecv:commandMethod
*
* Determine subcommand method or class and Exception collection.
* Can be override for finer subcommand routing
*
* Subcommand execution search order:
* 1. {@link CommandReceiver#subCommands}
* 2. {@link CommandReceiver#defaultSubCommand}
* 3. {@link CommandReceiver#printHelp(CommandSender, Arguments)}
*/
protected Set getSubCommands() {
return Collections.unmodifiableSet(subCommands.keySet());
}
// Only directly registered command handler need this
// acceptCommand() will be called directly in subcommand classes
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
Arguments cmd = Arguments.parse(args, sender);
if (cmd == null) return false;
acceptCommand(sender, cmd);
return true;
}
/*
* The code path looks like this:
* - Bukkit => CmdRecv:onTabComplete => CmdRecv:acceptTabComplete => SubCmdRecv:acceptTabComplete => SubCmdRecv:tabCompleteMethod
*
* Subcommand tab completion search order:
* 1. {@link CommandReceiver#subCommands}
* 2. {@link CommandReceiver#defaultSubCommand}.callTabCompletion
* 3. default builtin completion logic
*
*/
/**
* @param sender who run the command
* @param cmd the command, or part of the command
*/
public void acceptCommand(CommandSender sender, Arguments cmd) {
String subCommand = cmd.top();
try {
boolean subclass_may_print_success_msg;
try {
if (subCommand != null && subCommands.containsKey(subCommand)) {
cmd.next(); // drop the first parameter
subclass_may_print_success_msg = subCommands.get(subCommand).isField;
subCommands.get(subCommand).callCommand(sender, cmd);
} else if (defaultSubCommand != null) {
subclass_may_print_success_msg = defaultSubCommand.isField;
defaultSubCommand.callCommand(sender, cmd);
} else {
subclass_may_print_success_msg = true;
printHelp(sender, cmd);
}
if (!subclass_may_print_success_msg && showCompleteMessage()) {
msg(sender, "internal.info.command_complete");
}
} catch (ReflectiveOperationException ex) {
Throwable cause = ex.getCause();
if (cause instanceof RuntimeException)
throw (RuntimeException) cause;
else
throw new RuntimeException("Failed to invoke subcommand", ex);
}
} catch (NotPlayerException ex) {
msg(sender, "internal.error.not_player");
} catch (NoItemInHandException ex) {
msg(sender, ex.isOffHand ? "internal.error.no_item_offhand" : "internal.error.no_item_hand");
} catch (BadCommandException ex) {
String msg = ex.getMessage();
if (msg != null && !msg.equals("")) {
if (ex.objs == null) {
msg(sender, msg);
} else {
msg(sender, msg, ex.objs);
}
} else {
msg(sender, "internal.error.invalid_command_arg");
}
msg(sender, "internal.info.usage_prompt",
getHelpContent("usage", subCommand));
} catch (NoPermissionException ex) {
msg(sender, "internal.error.no_required_permission", ex.getMessage());
} catch (Exception ex) {
ex.printStackTrace();
msg(sender, "internal.error.command_exception");
}
}
// Only directly registered command handler need this
// acceptTabComplete() will be called directly in subcommand classes
@Override
public final List onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
try {
Arguments cmd = Arguments.parsePreserveLastBlank(args, sender);
if (cmd == null) return null;
return acceptTabComplete(sender, cmd);
} catch (Exception ex) {
return null;
}
}
/**
* @param sender who run the command
* @param args the command, or part of the command
* @return tab completion candidates
*/
public List acceptTabComplete(CommandSender sender, Arguments args) {
String cmd = args.top();
if (cmd == null) return null;
boolean isPartial = args.remains() == 1;
if (isPartial) {
// ask default command
// list all matching subcommands
List ret = null;
if (defaultSubCommand != null) ret = defaultSubCommand.callTabComplete(sender, args);
if (ret == null) ret = new ArrayList<>();
final String cmd_prefix = cmd;
List subcommands = subCommands.keySet().stream().filter(s -> s.startsWith(cmd_prefix)).sorted().toList();
ret.addAll(subcommands);
return ret;
} else {
// goto subcommand if exact match found
// otherwise ask default command
if (subCommands.containsKey(cmd)) {
args.next();
return subCommands.get(cmd).callTabComplete(sender, args);
} else if (defaultSubCommand != null) {
return defaultSubCommand.callTabComplete(sender, args);
} else {
return null;
}
}
}
private String getHelpContent(String type, String cmd) {
String prefix = getHelpPrefix().length() == 0 ? "" : (getHelpPrefix() + ".");
String key = "manual." + prefix + cmd + "." + type;
if (i18n.hasKey(key)) {
return i18n.getFormatted(key);
} else {
return i18n.getFormatted("manual.no_" + type, cmd);
}
}
@SubCommand("help")
public void printHelp(CommandSender sender, Arguments args) {
List cmds = new ArrayList<>(subCommands.keySet());
cmds.sort(Comparator.naturalOrder());
String format = i18n.getFormatted("manual.format");
StringBuilder tmp = new StringBuilder();
for (String cmd : cmds) {
if (!subCommands.get(cmd).hasPermission(sender)) continue;
String description = getHelpContent("description", cmd);
String usage = getHelpContent("usage", cmd);
tmp.append("\n").append(format
.replace("", description)
.replace("", usage));
}
if (defaultSubCommand != null && defaultSubCommand.hasPermission(sender)) {
String cmd = "";
String description = getHelpContent("description", cmd);
String usage = getHelpContent("usage", cmd);
tmp.append("\n").append(format
.replace("", description)
.replace("", usage));
}
sender.sendMessage(tmp.toString());
}
public void msg(CommandSender target, String template, Object... args) {
target.sendMessage(i18n.getFormatted(template, args));
}
public static class SubCommandInfo {
final Object instance;
final String name; // default command can have this be null
final String permission; // if none then no permission required
final Method tabCompleter;
final boolean isField; // isField? field : method;
final Method method;
final Field field;
final CommandReceiver fieldValue;
final boolean isDefault;
SubCommandInfo(Object instance, String name, String permission, boolean isField, Method method, Field field, CommandReceiver fieldValue, boolean isDefault, Method tabCompleter) {
if (name == null && !isDefault) throw new IllegalArgumentException();
if (isField && !(method == null && field != null && fieldValue != null))
throw new IllegalArgumentException();
if (!isField && !(method != null && field == null && fieldValue == null))
throw new IllegalArgumentException();
if (isField && tabCompleter != null) {
throw new IllegalArgumentException();
}
this.instance = instance;
this.name = name;
this.permission = permission;
this.isField = isField;
this.method = method;
this.field = field;
this.fieldValue = fieldValue;
this.isDefault = isDefault;
this.tabCompleter = tabCompleter;
}
void callCommand(CommandSender sender, Arguments args) throws IllegalAccessException, InvocationTargetException {
if (permission != null && !sender.hasPermission(permission)) {
throw new NoPermissionException(permission);
}
if (isField) {
fieldValue.acceptCommand(sender, args);
} else {
method.invoke(instance, sender, args);
}
}
@SuppressWarnings("unchecked")
List callTabComplete(CommandSender sender, Arguments args) {
if (permission != null && !sender.hasPermission(permission)) {
return null;
}
if (isField) {
return fieldValue.acceptTabComplete(sender, args);
} else if (tabCompleter != null) {
try {
return (List) tabCompleter.invoke(instance, sender, args);
} catch (ReflectiveOperationException ex) {
return null;
}
} else {
return null;
}
}
boolean hasPermission(CommandSender sender) {
if (permission == null) return true;
return sender.hasPermission(permission);
}
}
}