From 782f3023fc8875ad9ac6c837f3b2292f7f04457f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Thu, 1 Oct 2020 23:41:17 +0200 Subject: [PATCH] :sparkles: Finalize command flags They're currently quite ugly in the help menu. this should probably be fixed, but it's not a priority issue. --- .../java/cloud/commandframework/Command.java | 116 +++++++++-- .../commandframework/CommandManager.java | 11 ++ .../cloud/commandframework/CommandTree.java | 10 + .../arguments/compound/FlagArgument.java | 182 ++++++++++++++++-- .../arguments/flags/CommandFlag.java | 20 +- .../arguments/flags/FlagContext.java | 2 +- .../arguments/flags/package-info.java | 28 +++ .../context/CommandContext.java | 14 +- .../types/tuples/DynamicTuple.java | 2 +- .../CommandSuggestionsTest.java | 23 +++ .../commandframework/CommandTreeTest.java | 29 +++ .../brigadier/CloudBrigadierManager.java | 78 +++++--- .../cloud/commandframework/BukkitTest.java | 10 + config/checkstyle/checkstyle.xml | 4 - 14 files changed, 453 insertions(+), 76 deletions(-) create mode 100644 cloud-core/src/main/java/cloud/commandframework/arguments/flags/package-info.java diff --git a/cloud-core/src/main/java/cloud/commandframework/Command.java b/cloud-core/src/main/java/cloud/commandframework/Command.java index 18ababfb..b84e3a65 100644 --- a/cloud-core/src/main/java/cloud/commandframework/Command.java +++ b/cloud-core/src/main/java/cloud/commandframework/Command.java @@ -27,6 +27,8 @@ import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.StaticArgument; import cloud.commandframework.arguments.compound.ArgumentPair; import cloud.commandframework.arguments.compound.ArgumentTriplet; +import cloud.commandframework.arguments.compound.FlagArgument; +import cloud.commandframework.arguments.flags.CommandFlag; import cloud.commandframework.execution.CommandExecutionHandler; import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.SimpleCommandMeta; @@ -39,6 +41,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -146,8 +149,13 @@ public class Command { @NonNull final String... aliases) { final Map<@NonNull CommandArgument, @NonNull Description> map = new LinkedHashMap<>(); map.put(StaticArgument.of(commandName, aliases), description); - return new Builder<>(null, commandMeta, null, map, - new CommandExecutionHandler.NullCommandExecutionHandler<>(), Permission.empty()); + return new Builder<>(null, + commandMeta, + null, + map, + new CommandExecutionHandler.NullCommandExecutionHandler<>(), + Permission.empty(), + Collections.emptyList()); } /** @@ -165,8 +173,13 @@ public class Command { @NonNull final String... aliases) { final Map, Description> map = new LinkedHashMap<>(); map.put(StaticArgument.of(commandName, aliases), Description.empty()); - return new Builder<>(null, commandMeta, null, map, - new CommandExecutionHandler.NullCommandExecutionHandler<>(), Permission.empty()); + return new Builder<>(null, + commandMeta, + null, + map, + new CommandExecutionHandler.NullCommandExecutionHandler<>(), + Permission.empty(), + Collections.emptyList()); } /** @@ -258,19 +271,22 @@ public class Command { private final Class senderType; private final CommandPermission commandPermission; private final CommandManager commandManager; + private final Collection> flags; private Builder(@Nullable final CommandManager commandManager, @NonNull final CommandMeta commandMeta, @Nullable final Class senderType, @NonNull final Map<@NonNull CommandArgument, @NonNull Description> commandArguments, @NonNull final CommandExecutionHandler<@NonNull C> commandExecutionHandler, - @NonNull final CommandPermission commandPermission) { + @NonNull final CommandPermission commandPermission, + @NonNull final Collection> flags) { this.commandManager = commandManager; this.senderType = senderType; this.commandArguments = Objects.requireNonNull(commandArguments, "Arguments may not be null"); this.commandExecutionHandler = Objects.requireNonNull(commandExecutionHandler, "Execution handler may not be null"); this.commandPermission = Objects.requireNonNull(commandPermission, "Permission may not be null"); this.commandMeta = Objects.requireNonNull(commandMeta, "Meta may not be null"); + this.flags = Objects.requireNonNull(flags, "Flags may not be null"); } /** @@ -282,8 +298,13 @@ public class Command { */ public @NonNull Builder meta(@NonNull final String key, @NonNull final String value) { final CommandMeta commandMeta = SimpleCommandMeta.builder().with(this.commandMeta).with(key, value).build(); - return new Builder<>(this.commandManager, commandMeta, this.senderType, this.commandArguments, - this.commandExecutionHandler, this.commandPermission); + return new Builder<>(this.commandManager, + commandMeta, + this.senderType, + this.commandArguments, + this.commandExecutionHandler, + this.commandPermission, + this.flags); } /** @@ -295,8 +316,13 @@ public class Command { * @return New builder instance using the provided command manager */ public @NonNull Builder manager(@Nullable final CommandManager commandManager) { - return new Builder<>(commandManager, this.commandMeta, this.senderType, this.commandArguments, - this.commandExecutionHandler, this.commandPermission); + return new Builder<>(commandManager, + this.commandMeta, + this.senderType, + this.commandArguments, + this.commandExecutionHandler, + this.commandPermission, + this.flags); } /** @@ -360,8 +386,13 @@ public class Command { @NonNull final Description description) { final Map, Description> commandArgumentMap = new LinkedHashMap<>(this.commandArguments); commandArgumentMap.put(argument, description); - return new Builder<>(this.commandManager, this.commandMeta, this.senderType, commandArgumentMap, - this.commandExecutionHandler, this.commandPermission); + return new Builder<>(this.commandManager, + this.commandMeta, + this.senderType, + commandArgumentMap, + this.commandExecutionHandler, + this.commandPermission, + this.flags); } /** @@ -537,8 +568,13 @@ public class Command { * @return New builder instance using the command execution handler */ public @NonNull Builder handler(@NonNull final CommandExecutionHandler commandExecutionHandler) { - return new Builder<>(this.commandManager, this.commandMeta, this.senderType, this.commandArguments, - commandExecutionHandler, this.commandPermission); + return new Builder<>(this.commandManager, + this.commandMeta, + this.senderType, + this.commandArguments, + commandExecutionHandler, + this.commandPermission, + this.flags); } /** @@ -548,8 +584,13 @@ public class Command { * @return New builder instance using the command execution handler */ public @NonNull Builder withSenderType(@NonNull final Class senderType) { - return new Builder<>(this.commandManager, this.commandMeta, senderType, this.commandArguments, - this.commandExecutionHandler, this.commandPermission); + return new Builder<>(this.commandManager, + this.commandMeta, + senderType, + this.commandArguments, + this.commandExecutionHandler, + this.commandPermission, + this.flags); } /** @@ -559,8 +600,13 @@ public class Command { * @return New builder instance using the command permission */ public @NonNull Builder withPermission(@NonNull final CommandPermission permission) { - return new Builder<>(this.commandManager, this.commandMeta, this.senderType, this.commandArguments, - this.commandExecutionHandler, permission); + return new Builder<>(this.commandManager, + this.commandMeta, + this.senderType, + this.commandArguments, + this.commandExecutionHandler, + permission, + this.flags); } /** @@ -570,8 +616,13 @@ public class Command { * @return New builder instance using the command permission */ public @NonNull Builder withPermission(@NonNull final String permission) { - return new Builder<>(this.commandManager, this.commandMeta, this.senderType, this.commandArguments, - this.commandExecutionHandler, Permission.of(permission)); + return new Builder<>(this.commandManager, + this.commandMeta, + this.senderType, + this.commandArguments, + this.commandExecutionHandler, + Permission.of(permission), + this.flags); } /** @@ -610,13 +661,38 @@ public class Command { return this.meta("hidden", "true"); } + /** + * Register a new command flag + * + * @param flag Flag + * @param Flag value type + * @return New builder instance that uses the provided flag + */ + public @NonNull Builder flag(@NonNull final CommandFlag flag) { + final List> flags = new ArrayList<>(this.flags); + flags.add(flag); + return new Builder<>(this.commandManager, + this.commandMeta, + this.senderType, + this.commandArguments, + this.commandExecutionHandler, + this.commandPermission, + Collections.unmodifiableList(flags)); + } + /** * Build a command using the builder instance * * @return Built command */ public @NonNull Command build() { - return new Command<>(Collections.unmodifiableMap(this.commandArguments), + final LinkedHashMap, Description> commandArguments = new LinkedHashMap<>(this.commandArguments); + /* Construct flag node */ + if (!flags.isEmpty()) { + final FlagArgument flagArgument = new FlagArgument<>(this.flags); + commandArguments.put(flagArgument, Description.of("Command flags")); + } + return new Command<>(Collections.unmodifiableMap(commandArguments), this.commandExecutionHandler, this.senderType, this.commandPermission, diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java index ac5044de..04e3d3d0 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java @@ -26,6 +26,7 @@ package cloud.commandframework; import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.CommandSyntaxFormatter; import cloud.commandframework.arguments.StandardCommandSyntaxFormatter; +import cloud.commandframework.arguments.flags.CommandFlag; import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.parser.ParserParameter; import cloud.commandframework.arguments.parser.ParserRegistry; @@ -363,6 +364,16 @@ public abstract class CommandManager { return CommandArgument.ofType(type, name).manager(this); } + /** + * Create a new command flag builder + * + * @param name Flag name + * @return Flag builder + */ + public CommandFlag.@NonNull Builder flagBuilder(@NonNull final String name) { + return CommandFlag.newBuilder(name); + } + /** * Get the internal command tree. This should not be accessed unless you know what you * are doing diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java index 1abde9f9..6261a99e 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java @@ -26,6 +26,7 @@ package cloud.commandframework; import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.StaticArgument; import cloud.commandframework.arguments.compound.CompoundArgument; +import cloud.commandframework.arguments.compound.FlagArgument; import cloud.commandframework.arguments.parser.ArgumentParseResult; import cloud.commandframework.context.CommandContext; import cloud.commandframework.exceptions.AmbiguousNodeException; @@ -378,6 +379,15 @@ public final class CommandTree { } // END: Compound arguments + // START: Flags + if (child.getValue() instanceof FlagArgument) { + /* Remove all but last */ + while (commandQueue.size() > 1) { + commandContext.store(FlagArgument.FLAG_META, commandQueue.remove()); + } + } + // END: Flags + if (child.getValue() != null) { if (commandQueue.isEmpty()) { return Collections.emptyList(); diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java b/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java index 3cd71547..94237d8c 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java @@ -23,12 +23,21 @@ // package cloud.commandframework.arguments.compound; -import cloud.commandframework.types.tuples.DynamicTuple; -import cloud.commandframework.types.tuples.Tuple; -import io.leangen.geantyref.TypeToken; +import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.flags.CommandFlag; +import cloud.commandframework.arguments.parser.ArgumentParseResult; +import cloud.commandframework.arguments.parser.ArgumentParser; +import cloud.commandframework.context.CommandContext; import org.checkerframework.checker.nullness.qual.NonNull; -import java.util.function.Function; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.function.BiFunction; /** * Container for flag parsing logic. This should not be be used directly. @@ -36,21 +45,160 @@ import java.util.function.Function; * * @param Command sender type */ -public class FlagArgument extends CompoundArgument { +public class FlagArgument extends CommandArgument { - FlagArgument(final @NonNull Tuple names, - final @NonNull Tuple parserTuple, - final @NonNull Tuple types, - final @NonNull Function<@NonNull DynamicTuple, @NonNull DynamicTuple> mapper, - final @NonNull TypeToken valueType) { + /** + * Dummy object that indicates that flags were parsed successfully + */ + public static final Object FLAG_PARSE_RESULT_OBJECT = new Object(); + /** + * Meta data for the last argument that was suggested + */ + public static final String FLAG_META = "__last_flag__"; + + private static final String FLAG_ARGUMENT_NAME = "flags"; + + /** + * Construct a new flag argument + * + * @param flags Flags + */ + public FlagArgument(final Collection> flags) { super(false, - "flags", - names, - parserTuple, - types, - mapper, - DynamicTuple::of, - valueType); + FLAG_ARGUMENT_NAME, + new FlagArgumentParser<>(flags.toArray(new CommandFlag[0])), + Object.class); + } + + public static final class FlagArgumentParser implements ArgumentParser { + + private final CommandFlag[] flags; + + private FlagArgumentParser(@NonNull final CommandFlag[] flags) { + this.flags = flags; + } + + @Override + public @NonNull ArgumentParseResult<@NonNull Object> parse(@NonNull final CommandContext<@NonNull C> commandContext, + @NonNull final Queue<@NonNull String> inputQueue) { + /* + This argument must necessarily be the last so we can just consume all remaining input. This argument type + is similar to a greedy string in that sense. But, we need to keep all flag logic contained to the parser + */ + final Set> parsedFlags = new HashSet<>(); + CommandFlag currentFlag = null; + + for (@NonNull final String string : inputQueue) { + if (string.startsWith("-")) { + if (currentFlag != null && currentFlag.getCommandArgument() != null) { + return ArgumentParseResult.failure( + new IllegalArgumentException(String.format("Missing argument for '%s'", currentFlag.getName()))); + } + if (string.startsWith("--")) { + final String flagName = string.substring(2); + for (final CommandFlag flag : this.flags) { + if (flagName.equalsIgnoreCase(flag.getName())) { + currentFlag = flag; + break; + } + } + } else { + final String flagName = string.substring(1); + for (final CommandFlag flag : this.flags) { + for (final String alias : flag.getAliases()) { + if (alias.equalsIgnoreCase(flagName)) { + currentFlag = flag; + break; + } + } + } + } + if (currentFlag == null) { + return ArgumentParseResult.failure( + new IllegalArgumentException(String.format("Unknown flag '%s'", string))); + } else if (parsedFlags.contains(currentFlag)) { + return ArgumentParseResult.failure( + new IllegalArgumentException(String.format("Duplicate flag '%s'", string))); + } + parsedFlags.add(currentFlag); + if (currentFlag.getCommandArgument() == null) { + /* It's a presence flag */ + commandContext.flags().addPresenceFlag(currentFlag); + /* We don't want to parse a value for this flag */ + currentFlag = null; + } + } else { + if (currentFlag == null) { + return ArgumentParseResult.failure( + new IllegalArgumentException(String.format("No flag started. Don't" + + " know what to do with '%s'", string))); + } else { + final ArgumentParseResult result = + ((CommandArgument) currentFlag.getCommandArgument()) + .getParser() + .parse(commandContext, + new LinkedList<>(Collections.singletonList(string))); + if (result.getFailure().isPresent()) { + return ArgumentParseResult.failure(result.getFailure().get()); + } else { + final CommandFlag erasedFlag = currentFlag; + final Object value = result.getParsedValue().get(); + commandContext.flags().addValueFlag(erasedFlag, value); + } + } + } + } + /* We've consumed everything */ + inputQueue.clear(); + return ArgumentParseResult.success(FLAG_PARSE_RESULT_OBJECT); + } + + @Override + public @NonNull List<@NonNull String> suggestions(final @NonNull CommandContext commandContext, + final @NonNull String input) { + /* Check if we have a last flag stored */ + final String lastArg = commandContext.getOrDefault(FLAG_META, ""); + if (lastArg.isEmpty() || !lastArg.startsWith("-")) { + /* We don't care about the last value and so we expect a flag */ + final List strings = new LinkedList<>(); + for (final CommandFlag flag : this.flags) { + strings.add(String.format("--%s", flag.getName())); + for (final String alias : flag.getAliases()) { + strings.add(String.format("-%s", alias)); + } + } + return strings; + } else { + CommandFlag currentFlag = null; + if (lastArg.startsWith("--")) { + final String flagName = lastArg.substring(2); + for (final CommandFlag flag : this.flags) { + if (flagName.equalsIgnoreCase(flag.getName())) { + currentFlag = flag; + break; + } + } + } else if (lastArg.startsWith("-")) { + final String flagName = lastArg.substring(1); + for (final CommandFlag flag : this.flags) { + for (final String alias : flag.getAliases()) { + if (alias.equalsIgnoreCase(flagName)) { + currentFlag = flag; + break; + } + } + } + } + if (currentFlag != null && currentFlag.getCommandArgument() != null) { + // noinspection all + return (List) ((BiFunction) currentFlag.getCommandArgument().getSuggestionsProvider()) + .apply(commandContext, input); + } + } + commandContext.store(FLAG_META, ""); + return suggestions(commandContext, input); + } + } } diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java index ea934f90..635d94de 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java @@ -30,6 +30,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Arrays; import java.util.Collection; +import java.util.Objects; /** * A flag is an optional command argument that may have an associated parser, @@ -88,7 +89,7 @@ public final class CommandFlag { /** * Get the flag description *

- * Flag description + * @return Flag description */ public @NonNull Description getDescription() { return this.description; @@ -108,6 +109,23 @@ public final class CommandFlag { return String.format("--%s", this.name); } + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final CommandFlag that = (CommandFlag) o; + return getName().equals(that.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } + public static final class Builder { diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java index 964b4adc..459311d7 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/FlagContext.java @@ -31,7 +31,7 @@ import java.util.Map; /** * Flag value mappings */ -public class FlagContext { +public final class FlagContext { /** * Dummy object stored as a flag value when the flag has no associated parser diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/package-info.java b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/package-info.java new file mode 100644 index 00000000..87c06697 --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/package-info.java @@ -0,0 +1,28 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +/** + * Cloud flag system + */ +package cloud.commandframework.arguments.flags; diff --git a/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java b/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java index 750b1dca..1cb12a01 100644 --- a/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java +++ b/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java @@ -26,6 +26,7 @@ package cloud.commandframework.context; import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.flags.FlagContext; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Collections; import java.util.HashMap; @@ -114,6 +115,15 @@ public final class CommandContext { } } + /** + * Remove a stored value from the context + * + * @param key Key to remove + */ + public void remove(@NonNull final String key) { + this.internalStorage.remove(key); + } + /** * Get a required argument from the context. This will thrown an exception * if there's no value associated with the given key @@ -140,8 +150,8 @@ public final class CommandContext { * @param Argument type * @return Argument, or supplied default value */ - public @NonNull T getOrDefault(@NonNull final String key, - @NonNull final T defaultValue) { + public @Nullable T getOrDefault(@NonNull final String key, + @Nullable final T defaultValue) { return this.getOptional(key).orElse(defaultValue); } diff --git a/cloud-core/src/main/java/cloud/commandframework/types/tuples/DynamicTuple.java b/cloud-core/src/main/java/cloud/commandframework/types/tuples/DynamicTuple.java index 7e0a69d8..b9417476 100644 --- a/cloud-core/src/main/java/cloud/commandframework/types/tuples/DynamicTuple.java +++ b/cloud-core/src/main/java/cloud/commandframework/types/tuples/DynamicTuple.java @@ -47,7 +47,7 @@ public final class DynamicTuple implements Tuple { } @Override - public final int getSize() { + public int getSize() { return this.internalArray.length; } diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java index a73f6f1b..dc6c4c6b 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java @@ -68,6 +68,15 @@ public class CommandSuggestionsTest { manager.command(manager.commandBuilder("com2") .argumentPair("com", Pair.of("x", "enum"), Pair.of(Integer.class, TestEnum.class), Description.empty())); + + manager.command(manager.commandBuilder("flags") + .argument(IntegerArgument.of("num")) + .flag(manager.flagBuilder("enum") + .withArgument(EnumArgument.of(TestEnum.class, "enum")) + .build()) + .flag(manager.flagBuilder("static") + .build()) + .build()); } @Test @@ -142,6 +151,20 @@ public class CommandSuggestionsTest { Assertions.assertEquals(Arrays.asList("foo", "bar"), suggestions4); } + @Test + void testFlags() { + final String input = "flags 10 "; + final List suggestions = manager.suggest(new TestCommandSender(), input); + Assertions.assertEquals(Arrays.asList("--enum", "--static"), suggestions); + final String input2 = "flags 10 --enum "; + final List suggestions2 = manager.suggest(new TestCommandSender(), input2); + Assertions.assertEquals(Arrays.asList("foo", "bar"), suggestions2); + final String input3 = "flags 10 --enum foo "; + final List suggestions3 = manager.suggest(new TestCommandSender(), input3); + Assertions.assertEquals(Arrays.asList("--enum", "--static"), suggestions3); + } + + public enum TestEnum { FOO, BAR diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java index b7b15d6b..1386ca7b 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java @@ -101,6 +101,21 @@ class CommandTreeTest { final Vector2 vector2 = c.get("vec"); System.out.printf("X: %f | Y: %f\n", vector2.getX(), vector2.getY()); })); + + /* Build command for testing flags */ + manager.command(manager.commandBuilder("flags") + .flag(manager.flagBuilder("test") + .withAliases("t") + .build()) + .flag(manager.flagBuilder("test2") + .build()) + .flag(manager.flagBuilder("num") + .withArgument(IntegerArgument.of("num")).build()) + .handler(c -> { + System.out.println("Flag present? " + c.flags().isPresent("test")); + System.out.println("Numerical flag: " + c.flags().getValue("num", -10)); + }) + .build()); } @Test @@ -182,6 +197,20 @@ class CommandTreeTest { manager.executeCommand(new TestCommandSender(), "vec 1 1").join(); } + @Test + void testFlags() { + manager.executeCommand(new TestCommandSender(), "flags").join(); + manager.executeCommand(new TestCommandSender(), "flags --test").join(); + manager.executeCommand(new TestCommandSender(), "flags -t").join(); + Assertions.assertThrows(CompletionException.class, () -> + manager.executeCommand(new TestCommandSender(), "flags --test --nonexistant").join()); + Assertions.assertThrows(CompletionException.class, () -> + manager.executeCommand(new TestCommandSender(), "flags --test --duplicate").join()); + manager.executeCommand(new TestCommandSender(), "flags --test --test2").join(); + Assertions.assertThrows(CompletionException.class, () -> + manager.executeCommand(new TestCommandSender(), "flags --test test2").join()); + manager.executeCommand(new TestCommandSender(), "flags --num 500"); + } public static final class SpecificCommandSender extends TestCommandSender { } diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java index f1c5e7e2..48a5425c 100644 --- a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java @@ -23,14 +23,13 @@ // package cloud.commandframework.brigadier; -import io.leangen.geantyref.GenericTypeReflector; -import io.leangen.geantyref.TypeToken; import cloud.commandframework.Command; import cloud.commandframework.CommandManager; import cloud.commandframework.CommandTree; import cloud.commandframework.arguments.CommandArgument; import cloud.commandframework.arguments.StaticArgument; import cloud.commandframework.arguments.compound.CompoundArgument; +import cloud.commandframework.arguments.compound.FlagArgument; import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.standard.BooleanArgument; import cloud.commandframework.arguments.standard.ByteArgument; @@ -57,6 +56,8 @@ import com.mojang.brigadier.suggestion.SuggestionProvider; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.tree.LiteralCommandNode; +import io.leangen.geantyref.GenericTypeReflector; +import io.leangen.geantyref.TypeToken; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -82,8 +83,8 @@ import java.util.function.Supplier; */ public final class CloudBrigadierManager { - private final Map, Function, - ? extends ArgumentType>> mappers; + private final Map, cloud.commandframework.types.tuples.Pair, + ? extends ArgumentType>, Boolean>> mappers; private final Map, Supplier>> defaultArgumentTypeSuppliers; private final Supplier> dummyContextProvider; private final CommandManager commandManager; @@ -107,7 +108,7 @@ public final class CloudBrigadierManager { private void registerInternalMappings() { /* Map byte, short and int to IntegerArgumentType */ this.registerMapping(new TypeToken>() { - }, argument -> { + }, true, argument -> { final boolean hasMin = argument.getMin() != Byte.MIN_VALUE; final boolean hasMax = argument.getMax() != Byte.MAX_VALUE; if (hasMin) { @@ -119,7 +120,7 @@ public final class CloudBrigadierManager { } }); this.registerMapping(new TypeToken>() { - }, argument -> { + }, true, argument -> { final boolean hasMin = argument.getMin() != Short.MIN_VALUE; final boolean hasMax = argument.getMax() != Short.MAX_VALUE; if (hasMin) { @@ -131,7 +132,7 @@ public final class CloudBrigadierManager { } }); this.registerMapping(new TypeToken>() { - }, argument -> { + }, true, argument -> { final boolean hasMin = argument.getMin() != Integer.MIN_VALUE; final boolean hasMax = argument.getMax() != Integer.MAX_VALUE; if (hasMin) { @@ -144,7 +145,7 @@ public final class CloudBrigadierManager { }); /* Map float to FloatArgumentType */ this.registerMapping(new TypeToken>() { - }, argument -> { + }, true, argument -> { final boolean hasMin = argument.getMin() != Float.MIN_VALUE; final boolean hasMax = argument.getMax() != Float.MAX_VALUE; if (hasMin) { @@ -157,7 +158,7 @@ public final class CloudBrigadierManager { }); /* Map double to DoubleArgumentType */ this.registerMapping(new TypeToken>() { - }, argument -> { + }, true, argument -> { final boolean hasMin = argument.getMin() != Double.MIN_VALUE; final boolean hasMax = argument.getMax() != Double.MAX_VALUE; if (hasMin) { @@ -170,10 +171,10 @@ public final class CloudBrigadierManager { }); /* Map boolean to BoolArgumentType */ this.registerMapping(new TypeToken>() { - }, argument -> BoolArgumentType.bool()); + }, true, argument -> BoolArgumentType.bool()); /* Map String properly to StringArgumentType */ this.registerMapping(new TypeToken>() { - }, argument -> { + }, false, argument -> { switch (argument.getStringMode()) { case QUOTED: return StringArgumentType.string(); @@ -183,21 +184,27 @@ public final class CloudBrigadierManager { return StringArgumentType.word(); } }); + /* Map flags to a greedy string */ + this.registerMapping(new TypeToken>() { + }, false, argument -> StringArgumentType.greedyString()); } /** * Register a cloud-Brigadier mapping * - * @param argumentType cloud argument type - * @param mapper mapper function - * @param cloud argument value type - * @param cloud argument type - * @param Brigadier argument type value + * @param argumentType cloud argument type + * @param nativeSuggestions Whether or not Brigadier suggestions should be used + * @param mapper mapper function + * @param cloud argument value type + * @param cloud argument type + * @param Brigadier argument type value */ public , O> void registerMapping(@NonNull final TypeToken argumentType, + final boolean nativeSuggestions, @NonNull final Function<@NonNull ? extends K, @NonNull ? extends ArgumentType> mapper) { - this.mappers.put(GenericTypeReflector.erase(argumentType.getType()), mapper); + this.mappers.put(GenericTypeReflector.erase(argumentType.getType()), + cloud.commandframework.types.tuples.Pair.of(mapper, nativeSuggestions)); } /** @@ -217,11 +224,13 @@ public final class CloudBrigadierManager { @NonNull final TypeToken argumentType, @NonNull final K argument) { final ArgumentParser commandArgument = (ArgumentParser) argument; - Function function = this.mappers.get(GenericTypeReflector.erase(argumentType.getType())); - if (function == null) { + final cloud.commandframework.types.tuples.Pair pair + = this.mappers.get(GenericTypeReflector.erase(argumentType.getType())); + if (pair == null || pair.getFirst() == null) { return this.createDefaultMapper(valueType, commandArgument); } - return new Pair<>((ArgumentType) function.apply(commandArgument), !(argument instanceof StringArgument.StringParser)); + return new Pair<>((ArgumentType) ((Function) pair.getFirst()).apply(commandArgument), + (boolean) pair.getSecond()); } private > @NonNull Pair<@NonNull ArgumentType, @NonNull Boolean> createDefaultMapper( @@ -255,7 +264,8 @@ public final class CloudBrigadierManager { final LiteralArgumentBuilder literalArgumentBuilder = LiteralArgumentBuilder .literal(label) .requires(sender -> permissionChecker.test(sender, (CommandPermission) node.getNodeMeta() - .getOrDefault("permission", Permission.empty()))); + .getOrDefault("permission", + Permission.empty()))); literalArgumentBuilder.executes(executor); final LiteralCommandNode constructedRoot = literalArgumentBuilder.build(); for (final CommandTree.Node> child : node.getChildren()) { @@ -282,8 +292,10 @@ public final class CloudBrigadierManager { final com.mojang.brigadier.@NonNull Command executor, @NonNull final BiPredicate<@NonNull S, @NonNull CommandPermission> permissionChecker) { final LiteralArgumentBuilder literalArgumentBuilder = LiteralArgumentBuilder.literal(root.getLiteral()) - .requires(sender -> permissionChecker.test(sender, (CommandPermission) cloudCommand.getNodeMeta() - .getOrDefault("permission", Permission.empty()))); + .requires(sender -> permissionChecker.test(sender, + (CommandPermission) cloudCommand.getNodeMeta() + .getOrDefault("permission", + Permission.empty()))); if (cloudCommand.getValue() != null && cloudCommand.getValue().getOwningCommand() != null) { literalArgumentBuilder.executes(executor); } @@ -312,18 +324,19 @@ public final class CloudBrigadierManager { final ArgumentBuilder[] argumentBuilders = new ArgumentBuilder[parsers.length]; for (int i = parsers.length - 1; i >= 0; i--) { - @SuppressWarnings("unchecked") - final ArgumentParser parser = (ArgumentParser) parsers[i]; + @SuppressWarnings("unchecked") final ArgumentParser parser = (ArgumentParser) parsers[i]; final Pair, Boolean> pair = this.getArgument(TypeToken.get((Class) types[i]), TypeToken.get(parser.getClass()), parser); final SuggestionProvider provider = pair.getRight() ? null : suggestionProvider; + final ArgumentBuilder fragmentBuilder = RequiredArgumentBuilder .argument((String) names[i], (ArgumentType) pair.getLeft()) .suggests(provider) .requires(sender -> permissionChecker.test(sender, - (CommandPermission) root.getNodeMeta() - .getOrDefault("permission", Permission.empty()))); + (CommandPermission) root.getNodeMeta() + .getOrDefault("permission", + Permission.empty()))); argumentBuilders[i] = fragmentBuilder; if (forceExecutor || (i == parsers.length - 1) && (root.isLeaf() || !root.getValue().isRequired())) { @@ -354,12 +367,17 @@ public final class CloudBrigadierManager { final Pair, Boolean> pair = this.getArgument(root.getValue().getValueType(), TypeToken.get(root.getValue().getParser().getClass()), root.getValue().getParser()); - final SuggestionProvider provider = pair.getRight() ? null : suggestionProvider; + final SuggestionProvider provider = pair.getRight() + ? null + : (context, builder) -> this.buildSuggestions(root.getValue(), + context, builder); argumentBuilder = RequiredArgumentBuilder .argument(root.getValue().getName(), (ArgumentType) pair.getLeft()) .suggests(provider) - .requires(sender -> permissionChecker.test(sender, (CommandPermission) root.getNodeMeta() - .getOrDefault("permission", Permission.empty()))); + .requires(sender -> permissionChecker.test(sender, + (CommandPermission) root.getNodeMeta() + .getOrDefault("permission", + Permission.empty()))); } if (forceExecutor || root.isLeaf() || !root.getValue().isRequired()) { argumentBuilder.executes(executor); diff --git a/cloud-minecraft/cloud-bukkit-test/src/main/java/cloud/commandframework/BukkitTest.java b/cloud-minecraft/cloud-bukkit-test/src/main/java/cloud/commandframework/BukkitTest.java index 52785795..9790ada2 100644 --- a/cloud-minecraft/cloud-bukkit-test/src/main/java/cloud/commandframework/BukkitTest.java +++ b/cloud-minecraft/cloud-bukkit-test/src/main/java/cloud/commandframework/BukkitTest.java @@ -59,6 +59,7 @@ import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.util.Vector; import org.checkerframework.checker.nullness.qual.NonNull; @@ -168,12 +169,21 @@ public final class BukkitTest extends JavaPlugin { .sendMessage(String.format("UUID: %s\n", c.getOptional("uuid").orElse(null))))) .command(mgr.commandBuilder("give") .withSenderType(Player.class) + .flag(mgr.flagBuilder("color") + .withArgument(EnumArgument.of(ChatColor.class, "color")) + .build()) .argument(EnumArgument.of(Material.class, "material")) .argument(IntegerArgument.of("amount")) .handler(c -> { final Material material = c.get("material"); final int amount = c.get("amount"); final ItemStack itemStack = new ItemStack(material, amount); + + final ChatColor color = c.flags().getValue("color", ChatColor.GOLD); + final ItemMeta itemMeta = itemStack.getItemMeta(); + itemMeta.setDisplayName(color + String.format("%s's item", c.getSender().getName())); + itemStack.setItemMeta(itemMeta); + ((Player) c.getSender()).getInventory().addItem(itemStack); c.getSender().sendMessage("You've been given stuff, bro."); })) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 51351e15..ec754c81 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -156,10 +156,6 @@ - - - -