diff --git a/example/src/main/java/co/aikar/acfexample/ACFExample.java b/example/src/main/java/co/aikar/acfexample/ACFExample.java index 0fb71b0f..8ddc3116 100644 --- a/example/src/main/java/co/aikar/acfexample/ACFExample.java +++ b/example/src/main/java/co/aikar/acfexample/ACFExample.java @@ -41,7 +41,7 @@ public final class ACFExample extends JavaPlugin { private void registerCommands() { commandManager = ACF.createManager(this); commandManager.getCommandContexts().registerContext(SomeObject.class, SomeObject.getContextResolver()); - commandManager.getCommandCompletions().registerCompletion("test", (sender, completionConfig, input) -> ( + commandManager.getCommandCompletions().registerCompletion("test", (sender, config, input, c) -> ( Lists.newArrayList("foo", "bar", "baz") )); commandManager.registerCommand(new SomeCommand()); diff --git a/pom.xml b/pom.xml index 99515aef..99f4ee17 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ co.aikar acf-core - 0.3.0-SNAPSHOT + 0.4.0-SNAPSHOT ACF diff --git a/src/main/java/co/aikar/commands/BaseCommand.java b/src/main/java/co/aikar/commands/BaseCommand.java index 8de2523e..45232b6f 100644 --- a/src/main/java/co/aikar/commands/BaseCommand.java +++ b/src/main/java/co/aikar/commands/BaseCommand.java @@ -393,16 +393,9 @@ public abstract class BaseCommand extends Command { } String[] completions = ACFPatterns.SPACE.split(cmd.complete.value()); - final int argIndex = args.length - 1; - String input = args[argIndex]; - final String completion = argIndex < completions.length ? completions[argIndex] : null; - - List cmds = manager.getCommandCompletions().of(sender, completion, input); - if (cmds.isEmpty()) { - cmds = ImmutableList.of(input); - } - return filterTabComplete(args[(argIndex)], cmds); + List cmds = manager.getCommandCompletions().of(cmd, sender, completions, args); + return filterTabComplete(args[args.length-1], cmds); } private static List filterTabComplete(String arg, List cmds) { diff --git a/src/main/java/co/aikar/commands/BukkitCommandCompletions.java b/src/main/java/co/aikar/commands/BukkitCommandCompletions.java index ec4e6d26..69017039 100644 --- a/src/main/java/co/aikar/commands/BukkitCommandCompletions.java +++ b/src/main/java/co/aikar/commands/BukkitCommandCompletions.java @@ -38,16 +38,16 @@ import java.util.stream.Stream; public class BukkitCommandCompletions extends CommandCompletions { BukkitCommandCompletions() { super(); - registerCompletion("mobs", (sender, completionConfig, input) -> { + registerCompletion("mobs", (sender, config, input, c) -> { final Stream normal = Stream.of(EntityType.values()) .map(entityType -> ACFUtil.simplifyString(entityType.getName())); return normal.collect(Collectors.toList()); }); - registerCompletion("worlds", (sender, completionConfig, input) -> ( + registerCompletion("worlds", (sender, config, input, c) -> ( Bukkit.getWorlds().stream().map(World::getName).collect(Collectors.toList()) )); - registerCompletion("players", (sender, completionConfig, input) -> { + registerCompletion("players", (sender, config, input, c) -> { Validate.notNull(sender, "Sender cannot be null"); Player senderPlayer = sender instanceof Player ? (Player) sender : null; diff --git a/src/main/java/co/aikar/commands/CommandCompletions.java b/src/main/java/co/aikar/commands/CommandCompletions.java index 77bcfb2b..ab01ef08 100644 --- a/src/main/java/co/aikar/commands/CommandCompletions.java +++ b/src/main/java/co/aikar/commands/CommandCompletions.java @@ -26,7 +26,10 @@ package co.aikar.commands; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import java.lang.reflect.Parameter; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -39,11 +42,11 @@ public class CommandCompletions { private Map completionMap = new HashMap<>(); public CommandCompletions() { - registerCompletion("range", (sender, completionConfig, input) -> { - if (completionConfig == null) { + registerCompletion("range", (sender, config, input, c) -> { + if (config == null) { return ImmutableList.of(); } - final String[] ranges = ACFPatterns.DASH.split(completionConfig); + final String[] ranges = ACFPatterns.DASH.split(config); int start; int end; if (ranges.length != 2) { @@ -55,36 +58,131 @@ public class CommandCompletions { } return IntStream.rangeClosed(start, end).mapToObj(Integer::toString).collect(Collectors.toList()); }); - registerCompletion("timeunits", (sender, completionConfig, input) -> ImmutableList.of("minutes", "hours", "days", "weeks", "months", "years")); + registerCompletion("timeunits", (sender, config, input, c) -> ImmutableList.of("minutes", "hours", "days", "weeks", "months", "years")); } public CommandCompletionHandler registerCompletion(String id, CommandCompletionHandler handler) { return this.completionMap.put("@" + id.toLowerCase(), handler); } - public List of(CommandSender sender, String completion, String input) { + @NotNull + List of(RegisteredCommand command, CommandSender sender, String[] completionInfo, String[] args) { + final int argIndex = args.length - 1; + + String input = args[argIndex]; + final String completion = argIndex < completionInfo.length ? completionInfo[argIndex] : null; if (completion == null) { - return ImmutableList.of(); - } - if (input == null) { - input = ""; + return ImmutableList.of(input); } + return getCompletionValues(command, sender, completion, args); + } + + @NotNull + List getCompletionValues(RegisteredCommand command, CommandSender sender, String completion, String[] args) { + final int argIndex = args.length - 1; + + String input = args[argIndex]; String[] complete = ACFPatterns.COLON.split(completion, 2); CommandCompletionHandler handler = this.completionMap.get(complete[0].toLowerCase()); if (handler != null) { - List completions = handler.getCompletions(sender, complete.length == 1 ? null : complete[1], input); - if (completions == null) { - return ImmutableList.of(); - } else { - return completions; + String config = complete.length == 1 ? null : complete[1]; + CommandCompletionContext context = new CommandCompletionContext(command, sender, input, config, args); + + try { + Collection completions = handler.getCompletions(sender, config, input, context); + if (completions != null) { + return Lists.newArrayList(completions); + } + //noinspection ConstantIfStatement,ConstantConditions + if (false) { // Hack to fool compiler. since its sneakily thrown. + throw new CommandCompletionTextLookupException(); + } + } catch (CommandCompletionTextLookupException ignored) { + // This should only happen if some other feedback error occured. + } catch (Exception e) { + ACFLog.exception(e); } + // Something went wrong in lookup, fall back to input + return ImmutableList.of(input); } + // Plaintext values. return Lists.newArrayList(ACFPatterns.PIPE.split(completion)); } public interface CommandCompletionHandler { - List getCompletions(CommandSender sender, String config, String input); + Collection getCompletions(CommandSender sender, String config, String input, CommandCompletionContext context) throws InvalidCommandArgument; + } + + public class CommandCompletionContext { + private final RegisteredCommand command; + private final CommandSender sender; + private final String input; + private final String config; + private final List args; + + CommandCompletionContext(RegisteredCommand command, CommandSender sender, String input, String config, String[] args) { + this.command = command; + this.sender = sender; + this.input = input; + this.config = config; + this.args = Lists.newArrayList(args); + } + + public T getContextValue(Class clazz) throws InvalidCommandArgument { + return getContextValue(clazz, null); + } + + public T getContextValue(Class clazz, Integer paramIdx) throws InvalidCommandArgument { + String name = null; + if (paramIdx != null) { + if (paramIdx >= command.parameters.length) { + throw new IllegalArgumentException("Param index is higher than number of parameters"); + } + Parameter param = command.parameters[paramIdx]; + Class paramType = param.getType(); + if (!clazz.isAssignableFrom(paramType)) { + throw new IllegalArgumentException(param.getName() +":" + paramType.getName() + " can not satisfy " + clazz.getName()); + } + name = param.getName(); + } else { + Parameter[] parameters = command.parameters; + for (int i = 0; i < parameters.length; i++) { + Parameter param = parameters[i]; + if (clazz.isAssignableFrom(param.getType())) { + paramIdx = i; + name = param.getName(); + break; + } + } + if (paramIdx == null) { + throw new IllegalStateException("Can not find any parameter that can satisfy " + clazz.getName()); + } + } + Map resolved = command.resolveContexts(sender, args, args.size()); + if (resolved == null || paramIdx > resolved.size()) { + ACFLog.error("resolved: " + resolved + " paramIdx: " + paramIdx + " - size: " + (resolved != null ? resolved.size() : null )); + ACFUtil.sneaky(new CommandCompletionTextLookupException()); + } + + //noinspection unchecked + return (T) resolved.get(name); + } + + public CommandSender getSender() { + return sender; + } + + public String getInput() { + return input; + } + + public String getConfig() { + return config; + } + } + + private class CommandCompletionTextLookupException extends Throwable { } } diff --git a/src/main/java/co/aikar/commands/RegisteredCommand.java b/src/main/java/co/aikar/commands/RegisteredCommand.java index 10eeebc9..8b220e6e 100644 --- a/src/main/java/co/aikar/commands/RegisteredCommand.java +++ b/src/main/java/co/aikar/commands/RegisteredCommand.java @@ -32,12 +32,14 @@ import co.aikar.commands.annotation.Syntax; import co.aikar.commands.annotation.Values; import co.aikar.commands.contexts.ContextResolver; import co.aikar.commands.contexts.SenderAwareContextResolver; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -125,49 +127,8 @@ public class RegisteredCommand { return; } try { - Map passedArgs = Maps.newLinkedHashMap(); - for (int i = 0; i < parameters.length; i++) { - boolean isLast = i == parameters.length - 1; - final Parameter parameter = parameters[i]; - final String parameterName = parameter.getName(); - final Class type = parameter.getType(); - final ContextResolver resolver = resolvers[i]; - CommandExecutionContext context = new CommandExecutionContext(this, parameter, sender, args, i, passedArgs); - if (args.isEmpty() && !(isLast && type == String[].class)) { - Default def = parameter.getAnnotation(Default.class); - Optional opt = parameter.getAnnotation(Optional.class); - if (isLast && def != null) { - args.add(def.value()); - } else if (isLast && opt != null) { - passedArgs.put(parameterName, resolver instanceof SenderAwareContextResolver ? resolver.getContext(context) : null); - //noinspection UnnecessaryContinue - continue; - } else if (!(resolver instanceof SenderAwareContextResolver)) { - scope.showSyntax(sender, this); - return; - } - } - final Values values = parameter.getAnnotation(Values.class); - if (values != null) { - String arg = args.get(0); - - final String[] split = ACFPatterns.PIPE.split(values.value()); - Set possible = Sets.newHashSet(); - for (String s : split) { - List check = this.scope.manager.getCommandCompletions().of(sender, s, arg); - if (!check.isEmpty()) { - possible.addAll(check.stream().map(String::toLowerCase).collect(Collectors.toList())); - } else { - possible.add(s.toLowerCase()); - } - } - - if (!possible.contains(arg.toLowerCase())) { - throw new InvalidCommandArgument("Must be one of: " + ACFUtil.join(possible, ", ")); - } - } - passedArgs.put(parameterName, resolver.getContext(context)); - } + Map passedArgs = resolveContexts(sender, args); + if (passedArgs == null) return; method.invoke(scope, passedArgs.values().toArray()); } catch (Exception e) { @@ -175,7 +136,6 @@ public class RegisteredCommand { e = (Exception) e.getCause(); } if (e instanceof InvalidCommandArgument) { - if (e.getMessage() != null && !e.getMessage().isEmpty()) { ACFUtil.sendMsg(sender, "&cError: " + e.getMessage()); } @@ -189,6 +149,60 @@ public class RegisteredCommand { } } + @Nullable + Map resolveContexts(CommandSender sender, List args) throws InvalidCommandArgument { + return resolveContexts(sender, args, parameters.length); + } + @Nullable + Map resolveContexts(CommandSender sender, List args, int argLimit) throws InvalidCommandArgument { + args = Lists.newArrayList(args); + String[] origArgs = args.toArray(new String[args.size()]); + Map passedArgs = Maps.newLinkedHashMap(); + for (int i = 0; i < parameters.length && i < argLimit; i++) { + boolean isLast = i == parameters.length - 1; + final Parameter parameter = parameters[i]; + final String parameterName = parameter.getName(); + final Class type = parameter.getType(); + final ContextResolver resolver = resolvers[i]; + CommandExecutionContext context = new CommandExecutionContext(this, parameter, sender, args, i, passedArgs); + if (args.isEmpty() && !(isLast && type == String[].class)) { + Default def = parameter.getAnnotation(Default.class); + Optional opt = parameter.getAnnotation(Optional.class); + if (isLast && def != null) { + args.add(def.value()); + } else if (isLast && opt != null) { + passedArgs.put(parameterName, resolver instanceof SenderAwareContextResolver ? resolver.getContext(context) : null); + //noinspection UnnecessaryContinue + continue; + } else if (!(resolver instanceof SenderAwareContextResolver)) { + scope.showSyntax(sender, this); + return null; + } + } + final Values values = parameter.getAnnotation(Values.class); + if (values != null) { + String arg = args.get(0); + + final String[] split = ACFPatterns.PIPE.split(values.value()); + Set possible = Sets.newHashSet(); + for (String s : split) { + List check = this.scope.manager.getCommandCompletions().getCompletionValues(this, sender, s, origArgs); + if (!check.isEmpty()) { + possible.addAll(check.stream().map(String::toLowerCase).collect(Collectors.toList())); + } else { + possible.add(s.toLowerCase()); + } + } + + if (!possible.contains(arg.toLowerCase())) { + throw new InvalidCommandArgument("Must be one of: " + ACFUtil.join(possible, ", ")); + } + } + passedArgs.put(parameterName, resolver.getContext(context)); + } + return passedArgs; + } + CommandTiming getTiming() { if (this.timing == null) { this.timing = ACFUtil.getTiming(scope, command);