From 28ff5d3003bdd5f8073b269f4036062b04aa005b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= <4096670+Citymonstret@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:23:41 +0200 Subject: [PATCH] feat(core): support root command deletion & standardize capabilities (#369) --- .../commandframework/CloudCapability.java | 85 ++++++++++ .../commandframework/CommandManager.java | 88 +++++++++++ .../cloud/commandframework/CommandTree.java | 29 +++- .../internal/CommandRegistrationHandler.java | 13 ++ .../commandframework/CommandDeletionTest.java | 149 ++++++++++++++++++ .../javacord/JavacordCommandManager.java | 3 + .../javacord/JavacordRegistrationHandler.java | 13 ++ .../jda/JDACommandManager.java | 4 + .../pircbotx/PircBotXCommandManager.java | 4 + .../bukkit/BukkitCommandManager.java | 14 +- .../bukkit/BukkitCommandPreprocessor.java | 8 +- .../bukkit/CloudBukkitCapabilities.java | 9 +- .../paper/PaperCommandManager.java | 7 +- .../examples/bukkit/ExamplePlugin.java | 6 +- 14 files changed, 416 insertions(+), 16 deletions(-) create mode 100644 cloud-core/src/main/java/cloud/commandframework/CloudCapability.java create mode 100644 cloud-core/src/test/java/cloud/commandframework/CommandDeletionTest.java diff --git a/cloud-core/src/main/java/cloud/commandframework/CloudCapability.java b/cloud-core/src/main/java/cloud/commandframework/CloudCapability.java new file mode 100644 index 00000000..98090007 --- /dev/null +++ b/cloud-core/src/main/java/cloud/commandframework/CloudCapability.java @@ -0,0 +1,85 @@ +// +// MIT License +// +// Copyright (c) 2021 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. +// +package cloud.commandframework; + +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Represents a capability that a cloud implementation may have. + * + * @since 1.7.0 + */ +@API(status = API.Status.STABLE, since = "1.7.0") +public interface CloudCapability { + + /** + * Returns the friendly name of this capability. + * + * @return the name of the capability + */ + @Override @NonNull String toString(); + + + /** + * Standard {@link CloudCapability capabilities}. + * + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + enum StandardCapabilities implements CloudCapability { + /** + * The capability to delete root commands using {@link CommandManager#deleteRootCommand(String)}. + */ + ROOT_COMMAND_DELETION; + + @Override + public @NonNull String toString() { + return name(); + } + } + + + /** + * Exception thrown when a {@link CloudCapability} is missing, when a method that requires the presence of that + * capability is invoked. + * + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + @SuppressWarnings("serial") + final class CloudCapabilityMissingException extends RuntimeException { + + private static final long serialVersionUID = 8961652857372971486L; + + /** + * Create a new cloud capability missing exception instance. + * + * @param capability the missing capability + */ + public CloudCapabilityMissingException(final @NonNull CloudCapability capability) { + super(String.format("Missing capability '%s'", capability)); + } + } +} diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java index b72eebf3..0387d7ce 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java @@ -29,6 +29,7 @@ import cloud.commandframework.arguments.CommandSuggestionEngine; import cloud.commandframework.arguments.CommandSyntaxFormatter; import cloud.commandframework.arguments.DelegatingCommandSuggestionEngineFactory; import cloud.commandframework.arguments.StandardCommandSyntaxFormatter; +import cloud.commandframework.arguments.StaticArgument; import cloud.commandframework.arguments.flags.CommandFlag; import cloud.commandframework.arguments.parser.ArgumentParser; import cloud.commandframework.arguments.parser.ParserParameter; @@ -66,15 +67,18 @@ import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -100,6 +104,7 @@ public abstract class CommandManager { private final CommandExecutionCoordinator commandExecutionCoordinator; private final CommandTree commandTree; private final CommandSuggestionEngine commandSuggestionEngine; + private final Set capabilities = new HashSet<>(); private CaptionVariableReplacementHandler captionVariableReplacementHandler = new SimpleCaptionVariableReplacementHandler(); private CommandSyntaxFormatter commandSyntaxFormatter = new StandardCommandSyntaxFormatter<>(); @@ -297,6 +302,40 @@ public abstract class CommandManager { return this.commandRegistrationHandler; } + /** + * Registers the given {@code capability}. + * + * @param capability the capability + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + protected final void registerCapability(final @NonNull CloudCapability capability) { + this.capabilities.add(capability); + } + + /** + * Checks whether the cloud implementation has the given {@code capability}. + * + * @param capability the capability + * @return {@code true} if the implementation has the {@code capability}, {@code false} if not + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + public boolean hasCapability(final @NonNull CloudCapability capability) { + return this.capabilities.contains(capability); + } + + /** + * Returns an unmodifiable snapshot of the currently registered {@link CloudCapability capabilities}. + * + * @return the currently registered capabilities + * @since 1.7.0 + */ + @API(status = API.Status.STABLE, since = "1.7.0") + public @NonNull Collection<@NonNull CloudCapability> capabilities() { + return Collections.unmodifiableSet(new HashSet<>(this.capabilities)); + } + protected final void setCommandRegistrationHandler(final @NonNull CommandRegistrationHandler commandRegistrationHandler) { this.requireState(RegistrationState.BEFORE_REGISTRATION); this.commandRegistrationHandler = commandRegistrationHandler; @@ -382,6 +421,55 @@ public abstract class CommandManager { */ public abstract boolean hasPermission(@NonNull C sender, @NonNull String permission); + /** + * Deletes the given {@code rootCommand}. + *

+ * This will delete all chains that originate at the root command. + * + * @param rootCommand The root command to delete + * @throws CloudCapability.CloudCapabilityMissingException If {@link CloudCapability.StandardCapabilities#ROOT_COMMAND_DELETION} is missing + * @since 1.7.0 + */ + @API(status = API.Status.EXPERIMENTAL, since = "1.7.0") + public void deleteRootCommand(final @NonNull String rootCommand) throws CloudCapability.CloudCapabilityMissingException { + if (!this.hasCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION)) { + throw new CloudCapability.CloudCapabilityMissingException(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION); + } + + // Mark the command for deletion. + final CommandTree.Node<@Nullable CommandArgument> node = this.commandTree.getNamedNode(rootCommand); + if (node == null) { + throw new IllegalArgumentException(String.format("No root command named '%s' exists", rootCommand)); + } + + // The registration handler gets to act before we destruct the command. + this.commandRegistrationHandler.unregisterRootCommand((StaticArgument) node.getValue()); + + // We then delete it from the tree. + this.commandTree.deleteRecursively(node); + + // And lastly we re-build the entire tree. + this.commandTree.verifyAndRegister(); + } + + /** + * Returns all root command names. + * + * @return Root command names. + * @since 1.7.0 + */ + @SuppressWarnings("unchecked") + @API(status = API.Status.STABLE, since = "1.7.0") + public @NonNull Collection<@NonNull String> rootCommands() { + return this.commandTree.getRootNodes() + .stream() + .map(CommandTree.Node::getValue) + .filter(arg -> arg instanceof StaticArgument) + .map(arg -> (StaticArgument) arg) + .map(StaticArgument::getName) + .collect(Collectors.toList()); + } + /** * Create a new command builder. This will also register the creating manager in the command * builder using {@link Command.Builder#manager(CommandManager)}, so that the command diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java index 2dce8b0f..8a5cb38b 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java @@ -953,6 +953,29 @@ public final class CommandTree { return null; } + void deleteRecursively(final @NonNull Node<@Nullable CommandArgument> node) { + for (final Node<@Nullable CommandArgument> child : new ArrayList<>(node.children)) { + this.deleteRecursively(child); + } + + // We need to remove it from the tree. + this.removeNode(node); + } + + private boolean removeNode(final @NonNull Node<@Nullable CommandArgument> node) { + if (node.isLeaf()) { + if (this.getRootNodes().contains(node)) { + this.internalTree.removeChild(node); + } else { + return node.getParent().removeChild(node); + } + } else { + throw new IllegalStateException(String.format("Cannot delete intermediate node '%s'", node)); + } + + return false; + } + /** * Get the command manager * @@ -971,7 +994,7 @@ public final class CommandTree { private final Map nodeMeta = new HashMap<>(); private final List> children = new LinkedList<>(); - private final T value; + private T value; private Node parent; private Node(final @Nullable T value) { @@ -1002,6 +1025,10 @@ public final class CommandTree { return null; } + private boolean removeChild(final @NonNull Node child) { + return this.children.remove(child); + } + /** * Check if the node is a leaf node * diff --git a/cloud-core/src/main/java/cloud/commandframework/internal/CommandRegistrationHandler.java b/cloud-core/src/main/java/cloud/commandframework/internal/CommandRegistrationHandler.java index 0f46e19a..c577f992 100644 --- a/cloud-core/src/main/java/cloud/commandframework/internal/CommandRegistrationHandler.java +++ b/cloud-core/src/main/java/cloud/commandframework/internal/CommandRegistrationHandler.java @@ -24,6 +24,7 @@ package cloud.commandframework.internal; import cloud.commandframework.Command; +import cloud.commandframework.arguments.StaticArgument; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; @@ -54,6 +55,15 @@ public interface CommandRegistrationHandler { */ boolean registerCommand(@NonNull Command command); + /** + * Requests that the given {@code rootCommand} should be unregistered. + * + * @param rootCommand The command to delete + * @since 1.7.0 + */ + default void unregisterRootCommand(final @NonNull StaticArgument rootCommand) { + } + @API(status = API.Status.INTERNAL, consumers = "cloud.commandframework.*") final class NullCommandRegistrationHandler implements CommandRegistrationHandler { @@ -66,6 +76,9 @@ public interface CommandRegistrationHandler { return true; } + @Override + public void unregisterRootCommand(final @NonNull StaticArgument rootCommand) { + } } } diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandDeletionTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandDeletionTest.java new file mode 100644 index 00000000..eecb35b8 --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/CommandDeletionTest.java @@ -0,0 +1,149 @@ +// +// MIT License +// +// Copyright (c) 2021 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. +// +package cloud.commandframework; + +import cloud.commandframework.arguments.standard.StringArgument; +import cloud.commandframework.exceptions.NoSuchCommandException; +import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.execution.CommandExecutionHandler; +import cloud.commandframework.internal.CommandRegistrationHandler; +import cloud.commandframework.meta.CommandMeta; +import cloud.commandframework.meta.SimpleCommandMeta; +import java.util.concurrent.CompletionException; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +class CommandDeletionTest { + + private CommandManager commandManager; + + @BeforeEach + void setup() { + this.commandManager = new CommandManager( + CommandExecutionCoordinator.simpleCoordinator(), + CommandRegistrationHandler.nullCommandRegistrationHandler() + ) { + { + this.registerCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION); + } + + @Override + public boolean hasPermission( + final @NonNull TestCommandSender sender, + final @NonNull String permission + ) { + return true; + } + + @Override + public @NonNull CommandMeta createDefaultCommandMeta() { + return SimpleCommandMeta.empty(); + } + }; + } + + @Test + void deleteSimpleCommand() { + // Arrange + this.commandManager.command(this.commandManager.commandBuilder("test").build()); + // Pre-assert. + this.commandManager.executeCommand(new TestCommandSender(), "test").join(); + + // Act + this.commandManager.deleteRootCommand("test"); + + // Assert + final CompletionException completionException = assertThrows( + CompletionException.class, + () -> this.commandManager.executeCommand(new TestCommandSender(), "test").join() + ); + assertThat(completionException).hasCauseThat().isInstanceOf(NoSuchCommandException.class); + + assertThat(this.commandManager.suggest(new TestCommandSender(), "")).isEmpty(); + assertThat(this.commandManager.getCommandTree().getRootNodes()).isEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + void deleteIntermediateCommand() { + // Arrange + final CommandExecutionHandler handler1 = mock(CommandExecutionHandler.class); + final Command command1 = this.commandManager + .commandBuilder("test") + .handler(handler1) + .build(); + this.commandManager.command(command1); + + final CommandExecutionHandler handler2 = mock(CommandExecutionHandler.class); + final Command command2 = this.commandManager + .commandBuilder("test") + .literal("literal") + .handler(handler2) + .build(); + this.commandManager.command(command2); + + final CommandExecutionHandler handler3 = mock(CommandExecutionHandler.class); + final Command command3 = this.commandManager + .commandBuilder("test") + .literal("literal") + .argument(StringArgument.of("string")) + .handler(handler3) + .build(); + this.commandManager.command(command3); + + // Act + this.commandManager.deleteRootCommand("test"); + + // Assert + final CompletionException completionException = assertThrows( + CompletionException.class, + () -> this.commandManager.executeCommand(new TestCommandSender(), "test").join() + ); + assertThat(completionException).hasCauseThat().isInstanceOf(NoSuchCommandException.class); + + final CompletionException completionException2 = assertThrows( + CompletionException.class, + () -> this.commandManager.executeCommand(new TestCommandSender(), "test literal").join() + ); + assertThat(completionException2).hasCauseThat().isInstanceOf(NoSuchCommandException.class); + + final CompletionException completionException3 = assertThrows( + CompletionException.class, + () -> this.commandManager.executeCommand(new TestCommandSender(), "test literal hm").join() + ); + assertThat(completionException3).hasCauseThat().isInstanceOf(NoSuchCommandException.class); + + verifyNoMoreInteractions(handler1); + verifyNoMoreInteractions(handler2); + verifyNoMoreInteractions(handler3); + + assertThat(this.commandManager.getCommandTree().getRootNodes()).isEmpty(); + } +} diff --git a/cloud-discord/cloud-javacord/src/main/java/cloud/commandframework/javacord/JavacordCommandManager.java b/cloud-discord/cloud-javacord/src/main/java/cloud/commandframework/javacord/JavacordCommandManager.java index 5257f0ee..da95f3df 100644 --- a/cloud-discord/cloud-javacord/src/main/java/cloud/commandframework/javacord/JavacordCommandManager.java +++ b/cloud-discord/cloud-javacord/src/main/java/cloud/commandframework/javacord/JavacordCommandManager.java @@ -23,6 +23,7 @@ // package cloud.commandframework.javacord; +import cloud.commandframework.CloudCapability; import cloud.commandframework.CommandManager; import cloud.commandframework.CommandTree; import cloud.commandframework.execution.CommandExecutionCoordinator; @@ -77,6 +78,8 @@ public class JavacordCommandManager extends CommandManager { this.commandPrefixMapper = commandPrefixMapper; this.commandPermissionMapper = commandPermissionMapper; + + this.registerCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION); } @Override diff --git a/cloud-discord/cloud-javacord/src/main/java/cloud/commandframework/javacord/JavacordRegistrationHandler.java b/cloud-discord/cloud-javacord/src/main/java/cloud/commandframework/javacord/JavacordRegistrationHandler.java index 8377ce9b..329a76d1 100644 --- a/cloud-discord/cloud-javacord/src/main/java/cloud/commandframework/javacord/JavacordRegistrationHandler.java +++ b/cloud-discord/cloud-javacord/src/main/java/cloud/commandframework/javacord/JavacordRegistrationHandler.java @@ -25,10 +25,12 @@ package cloud.commandframework.javacord; import cloud.commandframework.Command; import cloud.commandframework.arguments.CommandArgument; +import cloud.commandframework.arguments.StaticArgument; import cloud.commandframework.internal.CommandRegistrationHandler; import java.util.HashMap; import java.util.Map; import org.checkerframework.checker.nullness.qual.NonNull; +import org.javacord.api.listener.message.MessageCreateListener; final class JavacordRegistrationHandler implements CommandRegistrationHandler { @@ -59,4 +61,15 @@ final class JavacordRegistrationHandler implements CommandRegistrationHandler return true; } + @Override + public void unregisterRootCommand( + final @NonNull StaticArgument rootCommand + ) { + final JavacordCommand command = this.registeredCommands.get(rootCommand); + if (command == null) { + return; + } + + this.javacordCommandManager.getDiscordApi().removeListener(MessageCreateListener.class, command); + } } diff --git a/cloud-discord/cloud-jda/src/main/java/cloud/commandframework/jda/JDACommandManager.java b/cloud-discord/cloud-jda/src/main/java/cloud/commandframework/jda/JDACommandManager.java index 227c4be9..b5ff0ca0 100644 --- a/cloud-discord/cloud-jda/src/main/java/cloud/commandframework/jda/JDACommandManager.java +++ b/cloud-discord/cloud-jda/src/main/java/cloud/commandframework/jda/JDACommandManager.java @@ -23,6 +23,7 @@ // package cloud.commandframework.jda; +import cloud.commandframework.CloudCapability; import cloud.commandframework.CommandManager; import cloud.commandframework.CommandTree; import cloud.commandframework.execution.CommandExecutionCoordinator; @@ -117,6 +118,9 @@ public class JDACommandManager extends CommandManager { new RoleArgument.RoleParser<>( new HashSet<>(Arrays.asList(RoleArgument.ParserMode.values())) )); + + // No "native" command system means that we can delete commands just fine. + this.registerCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION); } /** diff --git a/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/PircBotXCommandManager.java b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/PircBotXCommandManager.java index 1022d655..f5ccf7c8 100644 --- a/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/PircBotXCommandManager.java +++ b/cloud-irc/cloud-pircbotx/src/main/java/cloud/commandframework/pircbotx/PircBotXCommandManager.java @@ -23,6 +23,7 @@ // package cloud.commandframework.pircbotx; +import cloud.commandframework.CloudCapability; import cloud.commandframework.CommandManager; import cloud.commandframework.CommandTree; import cloud.commandframework.captions.Caption; @@ -114,6 +115,9 @@ public class PircBotXCommandManager extends CommandManager { TypeToken.get(User.class), parameters -> new UserArgument.UserArgumentParser<>() ); + + // No "native" command system means that we can delete commands just fine. + this.registerCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION); } @Override diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java index 8d82b9a4..95511e81 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java @@ -23,6 +23,7 @@ // package cloud.commandframework.bukkit; +import cloud.commandframework.CloudCapability; import cloud.commandframework.CommandManager; import cloud.commandframework.CommandTree; import cloud.commandframework.brigadier.BrigadierManagerHolder; @@ -55,6 +56,7 @@ import java.lang.reflect.Method; import java.util.Set; import java.util.function.Function; import java.util.function.UnaryOperator; +import org.apiguardian.api.API; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.OfflinePlayer; @@ -126,6 +128,9 @@ public class BukkitCommandManager extends CommandManager implements Brigad final BukkitSynchronizer bukkitSynchronizer = new BukkitSynchronizer(owningPlugin); this.taskFactory = new TaskFactory(bukkitSynchronizer); + /* Register capabilities */ + CloudBukkitCapabilities.CAPABLE.forEach(this::registerCapability); + /* Register Bukkit Preprocessor */ this.registerCommandPreProcessor(new BukkitCommandPreprocessor<>(this)); @@ -157,7 +162,7 @@ public class BukkitCommandManager extends CommandManager implements Brigad new MultiplePlayerSelectorArgument.MultiplePlayerSelectorParser<>()); /* Register MC 1.13+ parsers */ - if (this.queryCapability(CloudBukkitCapabilities.BRIGADIER)) { + if (this.hasCapability(CloudBukkitCapabilities.BRIGADIER)) { this.registerParserSupplierFor(ItemStackPredicateArgument.class); this.registerParserSupplierFor(BlockPredicateArgument.class); } @@ -256,7 +261,7 @@ public class BukkitCommandManager extends CommandManager implements Brigad * will contain the reason for this. */ protected final void checkBrigadierCompatibility() throws BrigadierFailureException { - if (!this.queryCapability(CloudBukkitCapabilities.BRIGADIER)) { + if (!this.hasCapability(CloudBukkitCapabilities.BRIGADIER)) { throw new BrigadierFailureException( BrigadierFailureReason.VERSION_TOO_LOW, new IllegalArgumentException( @@ -271,7 +276,10 @@ public class BukkitCommandManager extends CommandManager implements Brigad * * @param capability Capability * @return {@code true} if the manager has the given capability, else {@code false} + * @deprecated for removal since 1.7.0. Use the new standard {@link #hasCapability(CloudCapability)} instead. */ + @Deprecated + @API(status = API.Status.DEPRECATED, since = "1.7.0") public final boolean queryCapability(final @NonNull CloudBukkitCapabilities capability) { return capability.capable(); } @@ -371,7 +379,7 @@ public class BukkitCommandManager extends CommandManager implements Brigad } final void lockIfBrigadierCapable() { - if (this.queryCapability(CloudBukkitCapabilities.BRIGADIER)) { + if (this.hasCapability(CloudBukkitCapabilities.BRIGADIER)) { this.lockRegistration(); } } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandPreprocessor.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandPreprocessor.java index 0b312513..e9deebbb 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandPreprocessor.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandPreprocessor.java @@ -27,7 +27,6 @@ import cloud.commandframework.brigadier.argument.WrappedBrigadierParser; import cloud.commandframework.bukkit.internal.BukkitBackwardsBrigadierSenderMapper; import cloud.commandframework.execution.preprocessor.CommandPreprocessingContext; import cloud.commandframework.execution.preprocessor.CommandPreprocessor; -import java.util.Set; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -40,7 +39,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; final class BukkitCommandPreprocessor implements CommandPreprocessor { private final BukkitCommandManager commandManager; - private final Set bukkitCapabilities; private final @Nullable BukkitBackwardsBrigadierSenderMapper mapper; /** @@ -50,8 +48,8 @@ final class BukkitCommandPreprocessor implements CommandPreprocessor { */ BukkitCommandPreprocessor(final @NonNull BukkitCommandManager commandManager) { this.commandManager = commandManager; - this.bukkitCapabilities = commandManager.queryCapabilities(); - if (this.bukkitCapabilities.contains(CloudBukkitCapabilities.BRIGADIER)) { + + if (this.commandManager.hasCapability(CloudBukkitCapabilities.BRIGADIER)) { this.mapper = new BukkitBackwardsBrigadierSenderMapper<>(this.commandManager); } else { this.mapper = null; @@ -76,7 +74,7 @@ final class BukkitCommandPreprocessor implements CommandPreprocessor { ); context.getCommandContext().store( BukkitCommandContextKeys.CLOUD_BUKKIT_CAPABILITIES, - this.bukkitCapabilities + this.commandManager.queryCapabilities() ); } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/CloudBukkitCapabilities.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/CloudBukkitCapabilities.java index 5dc7cb9e..a83cf907 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/CloudBukkitCapabilities.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/CloudBukkitCapabilities.java @@ -23,15 +23,17 @@ // package cloud.commandframework.bukkit; +import cloud.commandframework.CloudCapability; import cloud.commandframework.bukkit.internal.CraftBukkitReflection; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.NonNull; /** * Capabilities for the Bukkit module */ -public enum CloudBukkitCapabilities { +public enum CloudBukkitCapabilities implements CloudCapability { BRIGADIER(CraftBukkitReflection.classExists("com.mojang.brigadier.tree.CommandNode") && CraftBukkitReflection.findOBCClass("command.BukkitCommandWrapper") != null), @@ -56,4 +58,9 @@ public enum CloudBukkitCapabilities { boolean capable() { return this.capable; } + + @Override + public @NonNull String toString() { + return name(); + } } diff --git a/cloud-minecraft/cloud-paper/src/main/java/cloud/commandframework/paper/PaperCommandManager.java b/cloud-minecraft/cloud-paper/src/main/java/cloud/commandframework/paper/PaperCommandManager.java index a2d9654b..115aabcc 100644 --- a/cloud-minecraft/cloud-paper/src/main/java/cloud/commandframework/paper/PaperCommandManager.java +++ b/cloud-minecraft/cloud-paper/src/main/java/cloud/commandframework/paper/PaperCommandManager.java @@ -23,6 +23,7 @@ // package cloud.commandframework.paper; +import cloud.commandframework.CloudCapability; import cloud.commandframework.CommandTree; import cloud.commandframework.brigadier.CloudBrigadierManager; import cloud.commandframework.bukkit.BukkitCommandManager; @@ -120,7 +121,7 @@ public class PaperCommandManager extends BukkitCommandManager { public void registerBrigadier() throws BrigadierFailureException { this.requireState(RegistrationState.BEFORE_REGISTRATION); this.checkBrigadierCompatibility(); - if (!this.queryCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) { + if (!this.hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) { super.registerBrigadier(); } else { try { @@ -153,11 +154,11 @@ public class PaperCommandManager extends BukkitCommandManager { * is up to the caller to guarantee that such is the case * * @throws IllegalStateException when the server does not support asynchronous completions. - * @see #queryCapability(CloudBukkitCapabilities) Check if the capability is present + * @see #hasCapability(CloudCapability) Check if the capability is present */ public void registerAsynchronousCompletions() throws IllegalStateException { this.requireState(RegistrationState.BEFORE_REGISTRATION); - if (!this.queryCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) { + if (!this.hasCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) { throw new IllegalStateException("Failed to register asynchronous command completion listener."); } Bukkit.getServer().getPluginManager().registerEvents( diff --git a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java index 4ab5bed0..74eeb90a 100644 --- a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java +++ b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java @@ -160,13 +160,13 @@ public final class ExamplePlugin extends JavaPlugin { // // Register Brigadier mappings // - if (this.manager.queryCapability(CloudBukkitCapabilities.BRIGADIER)) { + if (this.manager.hasCapability(CloudBukkitCapabilities.BRIGADIER)) { this.manager.registerBrigadier(); } // // Register asynchronous completions // - if (this.manager.queryCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) { + if (this.manager.hasCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) { ((PaperCommandManager) this.manager).registerAsynchronousCompletions(); } // @@ -386,7 +386,7 @@ public final class ExamplePlugin extends JavaPlugin { ); // Commands using MC 1.13+ argument types - if (this.manager.queryCapability(CloudBukkitCapabilities.BRIGADIER)) { + if (this.manager.hasCapability(CloudBukkitCapabilities.BRIGADIER)) { new Mc113(this.manager).registerCommands(); }