From 712f30a9c8072c11fb5c67eaad70d816140bf399 Mon Sep 17 00:00:00 2001 From: fullwall Date: Thu, 8 Sep 2022 03:01:24 +0800 Subject: [PATCH] Implement @Arg for commands --- .../net/citizensnpcs/api/CitizensAPI.java | 5 + .../net/citizensnpcs/api/CitizensPlugin.java | 3 + .../net/citizensnpcs/api/command/Arg.java | 33 ++- .../api/command/CommandContext.java | 8 +- .../api/command/CommandManager.java | 213 ++++++++++++------ .../net/citizensnpcs/api/command/Flag.java | 6 +- .../net/citizensnpcs/api/util/Messaging.java | 12 +- 7 files changed, 201 insertions(+), 79 deletions(-) diff --git a/src/main/java/net/citizensnpcs/api/CitizensAPI.java b/src/main/java/net/citizensnpcs/api/CitizensAPI.java index bdfd2043..95b4283e 100644 --- a/src/main/java/net/citizensnpcs/api/CitizensAPI.java +++ b/src/main/java/net/citizensnpcs/api/CitizensAPI.java @@ -7,6 +7,7 @@ import org.bukkit.plugin.Plugin; import net.citizensnpcs.api.ai.speech.SpeechFactory; +import net.citizensnpcs.api.command.CommandManager; import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.npc.NPCDataStore; import net.citizensnpcs.api.npc.NPCRegistry; @@ -60,6 +61,10 @@ public static NPCRegistry createNamedNPCRegistry(String name, NPCDataStore store return getImplementation().createNamedNPCRegistry(name, store); } + public static CommandManager getCommandManager() { + return getImplementation().getCommandManager(); + } + /** * @return The data folder of the current implementation */ diff --git a/src/main/java/net/citizensnpcs/api/CitizensPlugin.java b/src/main/java/net/citizensnpcs/api/CitizensPlugin.java index e0db5f33..44b81dfe 100644 --- a/src/main/java/net/citizensnpcs/api/CitizensPlugin.java +++ b/src/main/java/net/citizensnpcs/api/CitizensPlugin.java @@ -5,6 +5,7 @@ import org.bukkit.plugin.Plugin; import net.citizensnpcs.api.ai.speech.SpeechFactory; +import net.citizensnpcs.api.command.CommandManager; import net.citizensnpcs.api.npc.NPCDataStore; import net.citizensnpcs.api.npc.NPCRegistry; import net.citizensnpcs.api.npc.NPCSelector; @@ -36,6 +37,8 @@ public interface CitizensPlugin extends Plugin { */ public NPCRegistry createNamedNPCRegistry(String name, NPCDataStore store); + public CommandManager getCommandManager(); + /** * @return The default {@link NPCSelector} for managing player/server NPC selection */ diff --git a/src/main/java/net/citizensnpcs/api/command/Arg.java b/src/main/java/net/citizensnpcs/api/command/Arg.java index d8933dc0..2574fdd7 100644 --- a/src/main/java/net/citizensnpcs/api/command/Arg.java +++ b/src/main/java/net/citizensnpcs/api/command/Arg.java @@ -4,6 +4,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Collection; +import java.util.Collections; import org.bukkit.command.CommandSender; @@ -13,18 +15,35 @@ @Retention(RetentionPolicy.RUNTIME) @Target(value = { ElementType.PARAMETER }) public @interface Arg { - Class> validator() default Identity.class; + String[] completions() default {}; + + Class completionsProvider() default CompletionsProvider.Identity.class; + + Class> validator() default FlagValidator.Identity.class; int value(); - public static interface FlagValidator { - public T validate(CommandSender sender, NPC npc, String input) throws CommandException; + public static interface CompletionsProvider { + public Collection getCompletions(CommandContext args, CommandSender sender); + + public static class Identity implements CompletionsProvider { + @Override + public Collection getCompletions(CommandContext args, CommandSender sender) { + return Collections.emptyList(); + } + } } - public static class Identity implements FlagValidator { - @Override - public String validate(CommandSender sender, NPC npc, String input) throws CommandException { - return input; + public static interface FlagValidator { + public T validate(CommandContext args, CommandSender sender, NPC npc, String input) throws CommandException; + + public static class Identity implements FlagValidator { + @Override + public String validate(CommandContext args, CommandSender sender, NPC npc, String input) + throws CommandException { + return input; + } } } + } diff --git a/src/main/java/net/citizensnpcs/api/command/CommandContext.java b/src/main/java/net/citizensnpcs/api/command/CommandContext.java index a36e0f95..886f11ee 100644 --- a/src/main/java/net/citizensnpcs/api/command/CommandContext.java +++ b/src/main/java/net/citizensnpcs/api/command/CommandContext.java @@ -30,6 +30,7 @@ import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.bukkit.util.EulerAngle; import com.google.common.base.Joiner; import com.google.common.base.Splitter; @@ -310,6 +311,12 @@ public boolean matches(String command) { return args[0].equalsIgnoreCase(command); } + public EulerAngle parseEulerAngle(String input) { + List pose = Lists + .newArrayList(Iterables.transform(Splitter.on(',').split(input), (s) -> Double.parseDouble(s))); + return new EulerAngle(pose.get(0), pose.get(1), pose.get(2)); + } + public int parseTicks(String dur) { dur = dur.trim(); char last = Character.toLowerCase(dur.charAt(dur.length() - 1)); @@ -377,6 +384,5 @@ public static Location parseLocation(Location currentLocation, String flag) thro private static final Pattern FLAG = Pattern.compile("^-[a-zA-Z]+$"); private static final Splitter LOCATION_SPLITTER = Splitter.on(Pattern.compile("[,:]")).omitEmptyStrings(); - private static final Pattern VALUE_FLAG = Pattern.compile("^--[a-zA-Z0-9-]+$"); } diff --git a/src/main/java/net/citizensnpcs/api/command/CommandManager.java b/src/main/java/net/citizensnpcs/api/command/CommandManager.java index 58d1d859..75e5b307 100644 --- a/src/main/java/net/citizensnpcs/api/command/CommandManager.java +++ b/src/main/java/net/citizensnpcs/api/command/CommandManager.java @@ -14,13 +14,16 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.bukkit.ChatColor; import org.bukkit.Location; +import org.bukkit.Material; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.command.TabCompleter; @@ -33,8 +36,8 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; +import net.citizensnpcs.api.command.Arg.CompletionsProvider; import net.citizensnpcs.api.command.Arg.FlagValidator; -import net.citizensnpcs.api.command.Arg.Identity; import net.citizensnpcs.api.command.exception.CommandException; import net.citizensnpcs.api.command.exception.CommandUsageException; import net.citizensnpcs.api.command.exception.NoPermissionsException; @@ -44,6 +47,7 @@ import net.citizensnpcs.api.npc.NPC; import net.citizensnpcs.api.util.Messaging; import net.citizensnpcs.api.util.Paginator; +import net.citizensnpcs.api.util.SpigotUtil; public class CommandManager implements TabCompleter { private final Map, CommandAnnotationProcessor> annotationProcessors = Maps.newHashMap(); @@ -89,36 +93,25 @@ public void execute(org.bukkit.command.Command command, String[] args, CommandSe Object[] newMethodArgs = new Object[methodArgs.length + 1]; System.arraycopy(methodArgs, 0, newMethodArgs, 1, methodArgs.length); - executeMethod(newArgs, sender, newMethodArgs); + executeCommand(newArgs, sender, newMethodArgs); } - private void executeHelp(String[] args, CommandSender sender) throws CommandException { - if (!sender.hasPermission("citizens." + args[0] + ".help")) - throw new NoPermissionsException(); - int page = 1; - try { - page = args.length == 3 ? Integer.parseInt(args[2]) : page; - } catch (NumberFormatException e) { - sendSpecificHelp(sender, args[0], args[2]); - return; - } - sendHelp(sender, args[0], page); - } - - // Attempt to execute a command. - private void executeMethod(String[] args, CommandSender sender, Object[] methodArgs) throws CommandException { + private void executeCommand(String[] args, CommandSender sender, Object[] methodArgs) throws CommandException { String cmdName = args[0].toLowerCase(); String modifier = args.length > 1 ? args[1] : ""; boolean help = modifier.toLowerCase().equals("help"); - CommandInfo info = commands.get(cmdName + " " + modifier.toLowerCase()); - if ((info == null || info.method == null) && !help) { + CommandInfo info = getCommand(cmdName, modifier); + if ((info == null || info.method == null)) { + if (help) { + executeHelp(args, sender); + return; + } info = commands.get(cmdName + " *"); } - if ((info == null || info.method == null) && help) { - executeHelp(args, sender); - return; + if (info == null && args.length > 2) { + info = getCommand(cmdName, args[1], args[2]); } if (info == null) @@ -158,15 +151,18 @@ private void executeMethod(String[] args, CommandSender sender, Object[] methodA if (info.methodArguments.size() > 0) { methodArgs = Arrays.copyOf(methodArgs, methodArgs.length + info.methodArguments.size()); - for (Entry entry : info.methodArguments.entrySet()) { + for (Entry entry : info.methodArguments.entrySet()) { Class desiredType = entry.getValue().paramType; Object val = entry.getValue().getInput(context); if (val == null) { } else if (entry.getValue().validator != null) { - val = entry.getValue().validator.validate(sender, + val = entry.getValue().validator.validate(context, sender, methodArgs.length > 2 && methodArgs[2] instanceof NPC ? (NPC) methodArgs[2] : null, val.toString()); + } else if (desiredType == Material.class) { + val = SpigotUtil.isUsing1_13API() ? Material.matchMaterial(val.toString(), false) + : Material.matchMaterial(val.toString()); } else if (Enum.class.isAssignableFrom(desiredType)) { val = matchEnum((Enum[]) desiredType.getEnumConstants(), val.toString().toUpperCase()); } else if (desiredType == double.class || desiredType == Double.class) { @@ -204,12 +200,25 @@ private void executeMethod(String[] args, CommandSender sender, Object[] methodA } } + private void executeHelp(String[] args, CommandSender sender) throws CommandException { + if (!sender.hasPermission("citizens." + args[0] + ".help")) + throw new NoPermissionsException(); + int page = 1; + try { + page = args.length == 3 ? Integer.parseInt(args[2]) : page; + } catch (NumberFormatException e) { + sendSpecificHelp(sender, args[0], args[2]); + return; + } + sendHelp(sender, args[0], page); + } + /** - * A safe version of execute which catches and logs all errors that occur. Returns whether the command - * handler should print usage or not. + * A safe version of {@link #execute(org.bukkit.command.Command, String[], CommandSender, Object...)} which catches + * and logs all {@link Exception}s that occur. * * @see #execute(org.bukkit.command.Command, String[], CommandSender, Object...) - * @return Whether further usage should be printed + * @return Whether command usage should be printed */ public boolean executeSafe(org.bukkit.command.Command command, String[] args, CommandSender sender, Object... methodArgs) { @@ -272,32 +281,30 @@ public String getClosestCommandModifier(String command, String modifier) { } /** - * Gets the {@link CommandInfo} for the given top level command and modifier, or null if not found. + * Gets the {@link CommandInfo} for the given command parts, or null if not found. * - * @param rootCommand - * The top level command - * @param modifier - * The modifier (may be empty) + * @param commandParts + * The parts of the command * @return The command info for the command */ - public CommandInfo getCommand(String rootCommand, String modifier) { - return commands.get(Joiner.on(' ').join(rootCommand.toLowerCase(), modifier)); + public CommandInfo getCommand(String... commandParts) { + return commands.get(Joiner.on(' ').join(commandParts).toLowerCase()); } /** - * Gets all modified and root commands from the given root level command. For example, if /npc look and - * /npc jump were defined, calling getCommands("npc") would return {@link CommandInfo}s - * for both commands. + * Gets all modified and root commands from the given root level command. For example, if /npc look and + * /npc jump were defined, calling getCommands("npc") would return {@link CommandInfo}s for + * both commands. * - * @param command + * @param topLevelCommand * The root level command * @return The list of {@link CommandInfo}s */ - public List getCommands(String command) { - command = command.toLowerCase(); + public List getCommands(String topLevelCommand) { + topLevelCommand = topLevelCommand.toLowerCase(); List cmds = Lists.newArrayList(); for (Entry entry : commands.entrySet()) { - if (!entry.getKey().startsWith(command) || entry.getValue() == null) + if (!entry.getKey().startsWith(topLevelCommand) || entry.getValue() == null) continue; cmds.add(entry.getValue()); } @@ -336,21 +343,16 @@ private String getUsage(String[] args, Command cmd) { * The modifier to check (may be empty) * @return Whether the command is handled */ - public boolean hasCommand(org.bukkit.command.Command cmd, String modifier) { + public boolean hasCommand(org.bukkit.command.Command cmd, String... modifier) { String cmdName = cmd.getName().toLowerCase(); - return commands.containsKey(cmdName + " " + modifier.toLowerCase()) || commands.containsKey(cmdName + " *"); + return commands.containsKey(Joiner.on(' ').join(cmdName, modifier)) || commands.containsKey(cmdName + " *"); } - // Returns whether a player has access to a command. private boolean hasPermission(CommandInfo method, CommandSender sender) { Command cmd = method.commandAnnotation; - if (cmd.permission().isEmpty() || hasPermission(sender, cmd.permission()) || hasPermission(sender, "admin")) - return true; - - return false; + return cmd.permission().isEmpty() || hasPermission(sender, cmd.permission()) || hasPermission(sender, "admin"); } - // Returns whether a CommandSender has permission. private boolean hasPermission(CommandSender sender, String perm) { return sender.hasPermission(perm); } @@ -359,6 +361,13 @@ private boolean hasPermission(CommandSender sender, String perm) { public List onTabComplete(CommandSender sender, org.bukkit.command.Command command, String alias, String[] args) { List results = new ArrayList(); + if (args.length <= 2 && args[0].equalsIgnoreCase("help")) { + return getCommands(command.getName().toLowerCase()).stream() + .map((info) -> info.commandAnnotation.modifiers().length > 0 ? info.commandAnnotation.modifiers()[0] + : null) + .collect(Collectors.toList()); + } + if (args.length <= 1) { String search = args.length == 1 ? args[0] : ""; for (String base : commands.keySet()) { @@ -373,23 +382,31 @@ public List onTabComplete(CommandSender sender, org.bukkit.command.Comma } return results; } + CommandInfo cmd = getCommand(command.getName().toLowerCase(), args[0]); + if (cmd == null && args.length > 1) { + cmd = getCommand(command.getName().toLowerCase(), args[0], args[1]); + } + if (cmd == null) { return results; } + // partial parse String[] newArgs = new String[args.length + 1]; System.arraycopy(args, 0, newArgs, 1, args.length); newArgs[0] = command.getName().toLowerCase(); CommandContext context = new CommandContext(sender, newArgs); + results.addAll(cmd.getArgTabCompletions(context, sender, args.length - 1)); + Collection valueFlags = cmd.valueFlags(); String lastArg = (newArgs[newArgs.length - 1].isEmpty() && newArgs.length >= 2 ? newArgs[newArgs.length - 2] : newArgs[newArgs.length - 1]).toLowerCase(); String hyphenStrippedArg = lastArg.replaceFirst("--", ""); if (lastArg.startsWith("--") && valueFlags.contains(hyphenStrippedArg)) { - results.addAll(cmd.getFlagTabCompletions(hyphenStrippedArg)); + results.addAll(cmd.getFlagTabCompletions(context, sender, hyphenStrippedArg)); } else { for (String valueFlag : valueFlags) { if (!context.hasValueFlag(valueFlag)) { @@ -492,9 +509,11 @@ private void registerMethods(Class clazz, Method parent, Object obj) { Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameters.length; i++) { for (Annotation ann : parameters[i].getAnnotations()) { - if (!(ann instanceof Flag)) - continue; - info.addFlagAnnotation(i, parameterTypes[i], (Flag) ann); + if (ann instanceof Flag) { + info.addFlagAnnotation(i, parameterTypes[i], (Flag) ann); + } else if (ann instanceof Arg) { + info.addArgAnnotation(i, parameterTypes[i], (Arg) ann); + } } } @@ -550,7 +569,7 @@ public static class CommandInfo { private final Command commandAnnotation; public Object instance; private final Method method; - private final Map methodArguments = Maps.newHashMap(); + private final Map methodArguments = Maps.newHashMap(); public boolean serverCommand; private Collection valueFlags; @@ -559,14 +578,19 @@ public CommandInfo(Command commandAnnotation, Method method) { this.method = method; } + public void addArgAnnotation(int idx, Class paramType, Arg arg) { + this.methodArguments.put(idx, new InjectedCommandArgument(paramType, arg)); + } + public void addFlagAnnotation(int idx, Class paramType, Flag flag) { - this.methodArguments.put(idx, new FlagInstance(paramType, flag)); + this.methodArguments.put(idx, new InjectedCommandArgument(paramType, flag)); } private Collection calculateValueFlags() { valueFlags = new HashSet(); - for (FlagInstance instance : methodArguments.values()) { - valueFlags.add(instance.names[0]); + for (InjectedCommandArgument instance : methodArguments.values()) { + instance.getValueFlag().ifPresentOrElse((flag) -> valueFlags.add(flag), () -> { + }); } valueFlags.addAll(Arrays.asList(commandAnnotation.valueFlags())); return valueFlags; @@ -591,16 +615,25 @@ public boolean equals(Object obj) { return true; } + public Collection getArgTabCompletions(CommandContext args, CommandSender sender, int index) { + List completions = Lists.newArrayList(); + for (InjectedCommandArgument instance : methodArguments.values()) { + if (instance.matches(index)) { + completions.addAll(instance.getTabCompletions(args, sender)); + } + } + return completions; + } + public Command getCommandAnnotation() { return commandAnnotation; } - public Collection getFlagTabCompletions(String flag) { + public Collection getFlagTabCompletions(CommandContext args, CommandSender sender, String flag) { List completions = Lists.newArrayList(); - for (FlagInstance instance : methodArguments.values()) { - if (instance.names[0].equalsIgnoreCase(flag) - || (instance.names.length > 1 && instance.names[1].equalsIgnoreCase(flag))) { - completions.addAll(instance.getTabCompletions()); + for (InjectedCommandArgument instance : methodArguments.values()) { + if (instance.matches(flag)) { + completions.addAll(instance.getTabCompletions(args, sender)); } } return completions; @@ -616,15 +649,38 @@ public Collection valueFlags() { } } - private static class FlagInstance { + private static class InjectedCommandArgument { private final String[] completions; + private CompletionsProvider completionsProvider; private final String defaultValue; - private final int index = -1; + private int index = -1; private final String[] names; private final Class paramType; private FlagValidator validator; - public FlagInstance(Class paramType, Flag flag) { + public InjectedCommandArgument(Class paramType, Arg arg) { + this.defaultValue = ""; + this.names = new String[] {}; + this.paramType = paramType; + this.index = arg.value(); + this.completions = arg.completions(); + if (arg.validator() != FlagValidator.Identity.class) { + try { + this.validator = arg.validator().getConstructor().newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + } + if (arg.completionsProvider() != CompletionsProvider.Identity.class) { + try { + this.completionsProvider = arg.completionsProvider().getConstructor().newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public InjectedCommandArgument(Class paramType, Flag flag) { this.paramType = paramType; this.names = flag.value(); for (int i = 0; i < this.names.length; i++) { @@ -632,13 +688,20 @@ public FlagInstance(Class paramType, Flag flag) { } this.completions = flag.completions(); this.defaultValue = flag.defValue().isEmpty() ? null : flag.defValue(); - if (flag.validator() != Identity.class) { + if (flag.validator() != FlagValidator.Identity.class) { try { this.validator = flag.validator().getConstructor().newInstance(); } catch (Exception e) { e.printStackTrace(); } } + if (flag.completionsProvider() != CompletionsProvider.Identity.class) { + try { + this.completionsProvider = flag.completionsProvider().getConstructor().newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + } } public Object getInput(CommandContext context) { @@ -655,7 +718,10 @@ public Object getInput(CommandContext context) { } @SuppressWarnings("rawtypes") - public Collection getTabCompletions() { + private Collection getTabCompletions(CommandContext args, CommandSender sender) { + if (completionsProvider != null) { + return completionsProvider.getCompletions(args, sender); + } if (completions.length > 0) { return Arrays.asList(completions); } @@ -667,6 +733,19 @@ public Collection getTabCompletions() { } return Collections.emptyList(); } + + public Optional getValueFlag() { + return names.length == 0 ? Optional.empty() : Optional.of(names[0]); + } + + public boolean matches(int index) { + return this.index == index; + } + + public boolean matches(String flag) { + return names.length > 0 + && (names[0].equalsIgnoreCase(flag) || (names.length > 1 && names[1].equalsIgnoreCase(flag))); + } } private static String capitalize(Object string) { diff --git a/src/main/java/net/citizensnpcs/api/command/Flag.java b/src/main/java/net/citizensnpcs/api/command/Flag.java index 57ab6d00..5533bc30 100644 --- a/src/main/java/net/citizensnpcs/api/command/Flag.java +++ b/src/main/java/net/citizensnpcs/api/command/Flag.java @@ -5,17 +5,19 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import net.citizensnpcs.api.command.Arg.CompletionsProvider; import net.citizensnpcs.api.command.Arg.FlagValidator; -import net.citizensnpcs.api.command.Arg.Identity; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface Flag { String[] completions() default {}; + Class completionsProvider() default CompletionsProvider.Identity.class; + String defValue() default ""; - Class> validator() default Identity.class; + Class> validator() default FlagValidator.Identity.class; String[] value(); } diff --git a/src/main/java/net/citizensnpcs/api/util/Messaging.java b/src/main/java/net/citizensnpcs/api/util/Messaging.java index 001b6f3b..ecf2171e 100644 --- a/src/main/java/net/citizensnpcs/api/util/Messaging.java +++ b/src/main/java/net/citizensnpcs/api/util/Messaging.java @@ -68,7 +68,15 @@ public static void configure(File debugFile, boolean debug, String messageColour } if (CitizensAPI.getPlugin() != null) { - AUDIENCES = BukkitAudiences.create(CitizensAPI.getPlugin()); + try { + AUDIENCES = BukkitAudiences.create(CitizensAPI.getPlugin()); + } catch (Exception e) { + if (Messaging.isDebugging()) { + e.printStackTrace(); + } else { + Messaging.log("Unable to load Adventure, chat components will not work"); + } + } } if (debugFile != null) { @@ -200,7 +208,7 @@ private static void sendMessageTo(CommandSender sender, String rawMessage, boole String color = messageColor ? MESSAGE_COLOUR : ""; for (String message : CHAT_NEWLINE_SPLITTER.split(rawMessage)) { message = prettify(message); - if (hasComponents) { + if (hasComponents && AUDIENCES != null) { parseAndSendComponents(sender, message, color); } else { sender.sendMessage(message);