diff --git a/cloud-paper/src/main/java/org/incendo/cloud/paper/LegacyPaperBrigadier.java b/cloud-paper/src/main/java/org/incendo/cloud/paper/LegacyPaperBrigadier.java index 3eb7e4fa..614bc043 100644 --- a/cloud-paper/src/main/java/org/incendo/cloud/paper/LegacyPaperBrigadier.java +++ b/cloud-paper/src/main/java/org/incendo/cloud/paper/LegacyPaperBrigadier.java @@ -45,9 +45,9 @@ class LegacyPaperBrigadier implements Listener, BrigadierManagerHolder { private final CloudBrigadierManager brigadierManager; - private final PaperCommandManager paperCommandManager; + private final LegacyPaperCommandManager paperCommandManager; - LegacyPaperBrigadier(final @NonNull PaperCommandManager paperCommandManager) { + LegacyPaperBrigadier(final @NonNull LegacyPaperCommandManager paperCommandManager) { this.paperCommandManager = paperCommandManager; this.brigadierManager = new CloudBrigadierManager<>( this.paperCommandManager, diff --git a/cloud-paper/src/main/java/org/incendo/cloud/paper/LegacyPaperCommandManager.java b/cloud-paper/src/main/java/org/incendo/cloud/paper/LegacyPaperCommandManager.java new file mode 100644 index 00000000..43f0e099 --- /dev/null +++ b/cloud-paper/src/main/java/org/incendo/cloud/paper/LegacyPaperCommandManager.java @@ -0,0 +1,249 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.paper; + +import java.util.concurrent.Executor; +import java.util.function.Function; +import org.apiguardian.api.API; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.CloudCapability; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.SenderMapper; +import org.incendo.cloud.brigadier.BrigadierManagerHolder; +import org.incendo.cloud.brigadier.BrigadierSetting; +import org.incendo.cloud.brigadier.CloudBrigadierManager; +import org.incendo.cloud.bukkit.BukkitCommandManager; +import org.incendo.cloud.bukkit.CloudBukkitCapabilities; +import org.incendo.cloud.bukkit.internal.CraftBukkitReflection; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.paper.suggestion.SuggestionListener; +import org.incendo.cloud.paper.suggestion.SuggestionListenerFactory; +import org.incendo.cloud.state.RegistrationState; + +/** + * {@link CommandManager} implementation for Bukkit-based platforms (i.e. Spigot, Paper), + * with specific support for Paper features (gated behind {@link CloudBukkitCapabilities} for + * "backwards-compatibility"). + * + *

This command manager uses legacy Bukkit command APIs. It's recommended to use + * the {@link PaperCommandManager} instead when supporting Paper 1.20.6+ exclusively.

+ * + * @param command sender type + * @see LegacyPaperCommandManager#LegacyPaperCommandManager(Plugin, ExecutionCoordinator, SenderMapper) + * @see #createNative(Plugin, ExecutionCoordinator) + */ +public class LegacyPaperCommandManager extends BukkitCommandManager { + + private @Nullable BrigadierManagerHolder brigadierManagerHolder = null; + + /** + * Create a new Paper command manager. + * + * @param owningPlugin Plugin constructing the manager. Used when registering commands to the command map, + * registering event listeners, etc. + * @param commandExecutionCoordinator Execution coordinator instance. Due to Bukkit blocking the main thread for + * suggestion requests, it's potentially unsafe to use anything other than + * {@link ExecutionCoordinator#nonSchedulingExecutor()} for + * {@link ExecutionCoordinator.Builder#suggestionsExecutor(Executor)}. Once the + * coordinator, a suggestion provider, parser, or similar routes suggestion logic + * off of the calling (main) thread, it won't be possible to schedule further logic + * back to the main thread without a deadlock. When Brigadier support is active, this issue + * is avoided, as it allows for non-blocking suggestions. + * Paper's asynchronous completion API can also + * be used to avoid this issue: {@link #registerAsynchronousCompletions()} + * @param senderMapper Mapper between Bukkit's {@link CommandSender} and the command sender type {@code C}. + * @see #registerBrigadier() + * @throws InitializationException if construction of the manager fails + */ + @API(status = API.Status.STABLE, since = "2.0.0") + @SuppressWarnings("this-escape") + public LegacyPaperCommandManager( + final @NonNull Plugin owningPlugin, + final @NonNull ExecutionCoordinator commandExecutionCoordinator, + final @NonNull SenderMapper senderMapper + ) throws InitializationException { + super(owningPlugin, commandExecutionCoordinator, senderMapper); + + this.registerCommandPreProcessor(new PaperCommandPreprocessor<>( + this, + this.senderMapper(), + Function.identity() + )); + } + + /** + * Create a command manager using Bukkit's {@link CommandSender} as the sender type. + * + * @param owningPlugin plugin owning the command manager + * @param commandExecutionCoordinator execution coordinator instance + * @return a new command manager + * @throws InitializationException if the construction of the manager fails + * @see #LegacyPaperCommandManager(Plugin, ExecutionCoordinator, SenderMapper) for a more thorough explanation + * @since 1.5.0 + */ + @API(status = API.Status.STABLE, since = "2.0.0") + public static @NonNull LegacyPaperCommandManager<@NonNull CommandSender> createNative( + final @NonNull Plugin owningPlugin, + final @NonNull ExecutionCoordinator commandExecutionCoordinator + ) throws InitializationException { + return new LegacyPaperCommandManager<>( + owningPlugin, + commandExecutionCoordinator, + SenderMapper.identity() + ); + } + + /** + * Attempts to enable Brigadier command registration through the Paper API, falling + * back to {@link BukkitCommandManager#registerBrigadier()} if that fails. + * + *

Callers should check for {@link CloudBukkitCapabilities#NATIVE_BRIGADIER} first + * to avoid exceptions.

+ * + *

A check for {@link CloudBukkitCapabilities#NATIVE_BRIGADIER} {@code ||} {@link CloudBukkitCapabilities#COMMODORE_BRIGADIER} + * may also be appropriate for some use cases (because of the fallback behavior), but not most, as Commodore does not offer + * any functionality on modern + * versions (see the documentation for {@link CloudBukkitCapabilities#COMMODORE_BRIGADIER}).

+ * + * @see #hasCapability(CloudCapability) + * @throws BrigadierInitializationException when the prerequisite capabilities are not present or some other issue occurs + * during registration of Brigadier support + */ + @Override + public synchronized void registerBrigadier() throws BrigadierInitializationException { + this.registerBrigadier(true); + } + + /** + * Variant of {@link #registerBrigadier()} that only uses the old Paper-MojangAPI, even + * when the modern Paper commands API is present. This may be useful for debugging issues + * with the new Paper command system. + * + * @throws BrigadierInitializationException when the prerequisite capabilities are not present or some other issue occurs + * during registration of Brigadier support + * @deprecated This method will continue to work while the Paper commands API is still incubating, but will eventually no + * longer function when the old API is removed. + */ + @Deprecated + public synchronized void registerLegacyPaperBrigadier() throws BrigadierInitializationException { + this.registerBrigadier(false); + } + + private void registerBrigadier(final boolean allowModern) { + this.requireState(RegistrationState.BEFORE_REGISTRATION); + this.checkBrigadierCompatibility(); + + if (this.brigadierManagerHolder != null) { + throw new IllegalStateException("Brigadier is already registered! Holder: " + this.brigadierManagerHolder); + } + + if (!this.hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) { + super.registerBrigadier(); + } else if (allowModern && CraftBukkitReflection.classExists("io.papermc.paper.command.brigadier.CommandSourceStack")) { + try { + final ModernPaperBrigadier brig = new ModernPaperBrigadier<>( + CommandSender.class, + this, + this.senderMapper(), + this::lockRegistration + ); + this.brigadierManagerHolder = brig; + brig.registerPlugin(this.owningPlugin()); + this.commandRegistrationHandler(brig); + } catch (final Exception e) { + throw new BrigadierInitializationException("Failed to register ModernPaperBrigadier", e); + } + } else { + try { + this.brigadierManagerHolder = new LegacyPaperBrigadier<>(this); + Bukkit.getPluginManager().registerEvents((Listener) this.brigadierManagerHolder, this.owningPlugin()); + this.brigadierManagerHolder.brigadierManager().settings().set(BrigadierSetting.FORCE_EXECUTABLE, true); + } catch (final Exception e) { + throw new BrigadierInitializationException("Failed to register LegacyPaperBrigadier", e); + } + } + } + + /** + * {@inheritDoc} + * + * @return {@inheritDoc} + * @since 2.0.0 + */ + @API(status = API.Status.STABLE, since = "2.0.0") + @Override + public boolean hasBrigadierManager() { + return this.brigadierManagerHolder != null || super.hasBrigadierManager(); + } + + /** + * {@inheritDoc} + * + * @return {@inheritDoc} + * @throws BrigadierManagerNotPresent when {@link #hasBrigadierManager()} is false + * @since 1.2.0 + */ + @API(status = API.Status.STABLE, since = "2.0.0") + @Override + public @NonNull CloudBrigadierManager brigadierManager() { + if (this.brigadierManagerHolder != null) { + return this.brigadierManagerHolder.brigadierManager(); + } + return super.brigadierManager(); + } + + /** + * Registers asynchronous completions using the Paper API. This means the calling thread for suggestion queries will be a + * thread other than the {@link Server#isPrimaryThread() main server thread} (or, the sender's thread context on Folia). + * + *

Requires the {@link CloudBukkitCapabilities#ASYNCHRONOUS_COMPLETION} capability to be present.

+ * + *

It's not recommended to use this in combination with {@link #registerBrigadier()}, as Brigadier allows for + * non-blocking suggestions and the async completion API reduces the fidelity of suggestions compared to using Brigadier + * directly (see {@link LegacyPaperCommandManager#LegacyPaperCommandManager(Plugin, ExecutionCoordinator, SenderMapper)}).

+ * + * @throws IllegalStateException when the server does not support asynchronous completions + * @see #hasCapability(CloudCapability) + */ + public void registerAsynchronousCompletions() throws IllegalStateException { + this.requireState(RegistrationState.BEFORE_REGISTRATION); + if (!this.hasCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) { + throw new IllegalStateException("Failed to register asynchronous command completion listener."); + } + + final SuggestionListenerFactory suggestionListenerFactory = SuggestionListenerFactory.create(this); + final SuggestionListener suggestionListener = suggestionListenerFactory.createListener(); + + Bukkit.getServer().getPluginManager().registerEvents( + suggestionListener, + this.owningPlugin() + ); + } +} diff --git a/cloud-paper/src/main/java/org/incendo/cloud/paper/ModernPaperCommandManager.java b/cloud-paper/src/main/java/org/incendo/cloud/paper/ModernPaperCommandManager.java deleted file mode 100644 index 0b7bd3c5..00000000 --- a/cloud-paper/src/main/java/org/incendo/cloud/paper/ModernPaperCommandManager.java +++ /dev/null @@ -1,259 +0,0 @@ -// -// MIT License -// -// Copyright (c) 2024 Incendo -// -// 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 org.incendo.cloud.paper; - -import io.papermc.paper.command.brigadier.CommandSourceStack; -import io.papermc.paper.plugin.bootstrap.BootstrapContext; -import io.papermc.paper.plugin.configuration.PluginMeta; -import java.util.logging.Level; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import org.apiguardian.api.API; -import org.bukkit.plugin.Plugin; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.incendo.cloud.CloudCapability; -import org.incendo.cloud.CommandManager; -import org.incendo.cloud.SenderMapper; -import org.incendo.cloud.SenderMapperHolder; -import org.incendo.cloud.brigadier.BrigadierManagerHolder; -import org.incendo.cloud.brigadier.CloudBrigadierManager; -import org.incendo.cloud.bukkit.BukkitCommandContextKeys; -import org.incendo.cloud.bukkit.BukkitDefaultCaptionsProvider; -import org.incendo.cloud.bukkit.BukkitParsers; -import org.incendo.cloud.bukkit.CloudBukkitCapabilities; -import org.incendo.cloud.bukkit.PluginHolder; -import org.incendo.cloud.execution.ExecutionCoordinator; -import org.incendo.cloud.internal.CommandRegistrationHandler; - -/** - * A {@link CommandManager} implementation for modern Paper API, using {@link CommandSourceStack} as the base sender type. - * - *

This manager will only function on servers implementing Paper API 1.20.6 or newer.

- * - * @param command sender type - * @see #builder() - * @see #builder(SenderMapper) - */ -@API(status = API.Status.EXPERIMENTAL) -@SuppressWarnings("UnstableApiUsage") -public class ModernPaperCommandManager extends CommandManager implements SenderMapperHolder, - PluginMetaHolder, PluginHolder, BrigadierManagerHolder { - private final PluginMeta pluginMeta; - private final SenderMapper senderMapper; - - /** - * Creates a new {@link Builder} for a manager with sender type {@link C}. - * - * @param senderMapper sender mapper - * @param command sender type - * @return builder - */ - public static Builder builder(final SenderMapper senderMapper) { - return new Builder<>(senderMapper); - } - - /** - * Creates a new {@link Builder} using the native Paper {@link CommandSourceStack} sender type. - * - * @return builder - */ - public static Builder builder() { - return new Builder<>(SenderMapper.identity()); - } - - private ModernPaperCommandManager( - final @NonNull PluginMeta pluginMeta, - final @NonNull ExecutionCoordinator executionCoordinator, - final @NonNull SenderMapper senderMapper - ) { - super(executionCoordinator, CommandRegistrationHandler.nullCommandRegistrationHandler()); - this.pluginMeta = pluginMeta; - this.senderMapper = senderMapper; - - this.commandRegistrationHandler(new ModernPaperBrigadier<>( - CommandSourceStack.class, - this, - senderMapper, - this::lockRegistration - )); - - CloudBukkitCapabilities.CAPABLE.forEach(this::registerCapability); - this.registerCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION); - - BukkitParsers.register(this); - - this.registerDefaultExceptionHandlers(); - this.captionRegistry().registerProvider(new BukkitDefaultCaptionsProvider<>()); - - this.registerCommandPreProcessor(ctx -> ctx.commandContext().store( - BukkitCommandContextKeys.BUKKIT_COMMAND_SENDER, - this.senderMapper().reverse(ctx.commandContext().sender()).getSender() - )); - this.registerCommandPreProcessor(new PaperCommandPreprocessor<>( - this, - this.senderMapper(), - CommandSourceStack::getExecutor - )); - } - - @Override - public final boolean hasPermission(final @NonNull C sender, final @NonNull String permission) { - return this.senderMapper().reverse(sender).getSender().hasPermission(permission); - } - - @Override - public final @NonNull SenderMapper senderMapper() { - return this.senderMapper; - } - - private void registerDefaultExceptionHandlers() { - this.registerDefaultExceptionHandlers( - triplet -> this.senderMapper().reverse(triplet.first().sender()).getSender() - .sendMessage(Component.text( - triplet.first().formatCaption(triplet.second(), triplet.third()), - NamedTextColor.RED - )), - pair -> this.owningPlugin().getLogger().log(Level.SEVERE, pair.first(), pair.second()) - ); - } - - @Override - public final PluginMeta owningPluginMeta() { - return this.pluginMeta; - } - - @Override - public final boolean hasBrigadierManager() { - return true; - } - - @SuppressWarnings("unchecked") - @Override - public final @NonNull CloudBrigadierManager brigadierManager() { - return ((BrigadierManagerHolder) this.commandRegistrationHandler()) - .brigadierManager(); - } - - /** - * Variant of {@link ModernPaperCommandManager} created at - * {@link io.papermc.paper.plugin.bootstrap.PluginBootstrap bootstrap} time - * rather than in {@link Plugin#onEnable()}. This allows command registered at bootstrap time to be used by - * data pack functions. - * - * @param command sender type - */ - public static final class Bootstrapped extends ModernPaperCommandManager { - private Bootstrapped( - final @NonNull PluginMeta pluginMeta, - final @NonNull ExecutionCoordinator executionCoordinator, - final @NonNull SenderMapper senderMapper - ) { - super(pluginMeta, executionCoordinator, senderMapper); - } - - /** - * Runs the second phase of initialization for managers created at bootstrap time. - * - *

This method must be called in {@link Plugin#onEnable()} for some features to work.

- */ - public void onEnable() { - /* - ((ModernPaperBrigadier) this.commandRegistrationHandler()) - .registerPlugin(this.owningPlugin()); - */ - } - } - - /** - * First stage builder for {@link ModernPaperCommandManager}. - * - * @param command sender type - */ - public static final class Builder { - private final SenderMapper senderMapper; - - private Builder(final SenderMapper senderMapper) { - this.senderMapper = senderMapper; - } - - /** - * Configures the {@link ExecutionCoordinator} for the manager. - * - * @param executionCoordinator execution coordinator - * @return coordinated builder - */ - public CoordinatedBuilder executionCoordinator(final ExecutionCoordinator executionCoordinator) { - return new CoordinatedBuilder<>(this.senderMapper, executionCoordinator); - } - } - - /** - * Second stage builder for {@link ModernPaperCommandManager}. - * - * @param command sender type - */ - public static final class CoordinatedBuilder { - private final SenderMapper senderMapper; - private final ExecutionCoordinator executionCoordinator; - - private CoordinatedBuilder( - final SenderMapper senderMapper, - final ExecutionCoordinator executionCoordinator - ) { - this.senderMapper = senderMapper; - this.executionCoordinator = executionCoordinator; - } - - /** - * Creates a {@link ModernPaperCommandManager} from {@link Plugin#onEnable()}. - * - * @param plugin plugin instance - * @return manager - * @see Bootstrapped - * @see #buildBootstrapped(BootstrapContext) - */ - @SuppressWarnings("unchecked") - public @NonNull ModernPaperCommandManager buildOnEnable(final @NonNull Plugin plugin) { - final ModernPaperCommandManager mgr = - new ModernPaperCommandManager<>(plugin.getPluginMeta(), this.executionCoordinator, this.senderMapper); - ((ModernPaperBrigadier) mgr.commandRegistrationHandler()).registerPlugin(plugin); - return mgr; - } - - /** - * Creates a {@link ModernPaperCommandManager.Bootstrapped} during bootstrapping. - * - * @param context bootstrap context - * @return manager - * @see Bootstrapped#onEnable() - */ - @SuppressWarnings("unchecked") - public ModernPaperCommandManager.@NonNull Bootstrapped buildBootstrapped(final @NonNull BootstrapContext context) { - final ModernPaperCommandManager.Bootstrapped mgr = - new ModernPaperCommandManager.Bootstrapped<>(context.getPluginMeta(), this.executionCoordinator, this.senderMapper); - ((ModernPaperBrigadier) mgr.commandRegistrationHandler()).registerBootstrap(context); - return mgr; - } - } -} diff --git a/cloud-paper/src/main/java/org/incendo/cloud/paper/PaperCommandManager.java b/cloud-paper/src/main/java/org/incendo/cloud/paper/PaperCommandManager.java index 4318c0d5..a499476c 100644 --- a/cloud-paper/src/main/java/org/incendo/cloud/paper/PaperCommandManager.java +++ b/cloud-paper/src/main/java/org/incendo/cloud/paper/PaperCommandManager.java @@ -23,224 +23,237 @@ // package org.incendo.cloud.paper; -import java.util.concurrent.Executor; -import java.util.function.Function; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.plugin.bootstrap.BootstrapContext; +import io.papermc.paper.plugin.configuration.PluginMeta; +import java.util.logging.Level; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.apiguardian.api.API; -import org.bukkit.Bukkit; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.event.Listener; import org.bukkit.plugin.Plugin; import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; import org.incendo.cloud.CloudCapability; import org.incendo.cloud.CommandManager; import org.incendo.cloud.SenderMapper; +import org.incendo.cloud.SenderMapperHolder; import org.incendo.cloud.brigadier.BrigadierManagerHolder; -import org.incendo.cloud.brigadier.BrigadierSetting; import org.incendo.cloud.brigadier.CloudBrigadierManager; -import org.incendo.cloud.bukkit.BukkitCommandManager; +import org.incendo.cloud.bukkit.BukkitCommandContextKeys; +import org.incendo.cloud.bukkit.BukkitDefaultCaptionsProvider; +import org.incendo.cloud.bukkit.BukkitParsers; import org.incendo.cloud.bukkit.CloudBukkitCapabilities; -import org.incendo.cloud.bukkit.internal.CraftBukkitReflection; +import org.incendo.cloud.bukkit.PluginHolder; import org.incendo.cloud.execution.ExecutionCoordinator; -import org.incendo.cloud.paper.suggestion.SuggestionListener; -import org.incendo.cloud.paper.suggestion.SuggestionListenerFactory; -import org.incendo.cloud.state.RegistrationState; +import org.incendo.cloud.internal.CommandRegistrationHandler; /** - * {@link CommandManager} implementation for Bukkit-based platforms (i.e. Spigot, Paper), - * with specific support for Paper features (gated behind {@link CloudBukkitCapabilities} for - * "backwards-compatibility"). + * A {@link CommandManager} implementation for modern Paper API, using {@link CommandSourceStack} as the base sender type. + * + *

This manager will only function on servers implementing Paper API 1.20.6 or newer.

* * @param command sender type - * @see PaperCommandManager#PaperCommandManager(Plugin, ExecutionCoordinator, SenderMapper) - * @see #createNative(Plugin, ExecutionCoordinator) + * @see #builder() + * @see #builder(SenderMapper) */ -public class PaperCommandManager extends BukkitCommandManager { +@API(status = API.Status.EXPERIMENTAL) +@SuppressWarnings("UnstableApiUsage") +public class PaperCommandManager extends CommandManager implements SenderMapperHolder, + PluginMetaHolder, PluginHolder, BrigadierManagerHolder { + private final PluginMeta pluginMeta; + private final SenderMapper senderMapper; - private @Nullable BrigadierManagerHolder brigadierManagerHolder = null; + /** + * Creates a new {@link Builder} for a manager with sender type {@link C}. + * + * @param senderMapper sender mapper + * @param command sender type + * @return builder + */ + public static Builder builder(final SenderMapper senderMapper) { + return new Builder<>(senderMapper); + } /** - * Create a new Paper command manager. + * Creates a new {@link Builder} using the native Paper {@link CommandSourceStack} sender type. * - * @param owningPlugin Plugin constructing the manager. Used when registering commands to the command map, - * registering event listeners, etc. - * @param commandExecutionCoordinator Execution coordinator instance. Due to Bukkit blocking the main thread for - * suggestion requests, it's potentially unsafe to use anything other than - * {@link ExecutionCoordinator#nonSchedulingExecutor()} for - * {@link ExecutionCoordinator.Builder#suggestionsExecutor(Executor)}. Once the - * coordinator, a suggestion provider, parser, or similar routes suggestion logic - * off of the calling (main) thread, it won't be possible to schedule further logic - * back to the main thread without a deadlock. When Brigadier support is active, this issue - * is avoided, as it allows for non-blocking suggestions. - * Paper's asynchronous completion API can also - * be used to avoid this issue: {@link #registerAsynchronousCompletions()} - * @param senderMapper Mapper between Bukkit's {@link CommandSender} and the command sender type {@code C}. - * @see #registerBrigadier() - * @throws InitializationException if construction of the manager fails + * @return builder */ - @API(status = API.Status.STABLE, since = "2.0.0") - @SuppressWarnings("this-escape") - public PaperCommandManager( - final @NonNull Plugin owningPlugin, - final @NonNull ExecutionCoordinator commandExecutionCoordinator, - final @NonNull SenderMapper senderMapper - ) throws InitializationException { - super(owningPlugin, commandExecutionCoordinator, senderMapper); + public static Builder builder() { + return new Builder<>(SenderMapper.identity()); + } + + private PaperCommandManager( + final @NonNull PluginMeta pluginMeta, + final @NonNull ExecutionCoordinator executionCoordinator, + final @NonNull SenderMapper senderMapper + ) { + super(executionCoordinator, CommandRegistrationHandler.nullCommandRegistrationHandler()); + this.pluginMeta = pluginMeta; + this.senderMapper = senderMapper; + + this.commandRegistrationHandler(new ModernPaperBrigadier<>( + CommandSourceStack.class, + this, + senderMapper, + this::lockRegistration + )); + + CloudBukkitCapabilities.CAPABLE.forEach(this::registerCapability); + this.registerCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION); + + BukkitParsers.register(this); + + this.registerDefaultExceptionHandlers(); + this.captionRegistry().registerProvider(new BukkitDefaultCaptionsProvider<>()); + this.registerCommandPreProcessor(ctx -> ctx.commandContext().store( + BukkitCommandContextKeys.BUKKIT_COMMAND_SENDER, + this.senderMapper().reverse(ctx.commandContext().sender()).getSender() + )); this.registerCommandPreProcessor(new PaperCommandPreprocessor<>( this, this.senderMapper(), - Function.identity() + CommandSourceStack::getExecutor )); } - /** - * Create a command manager using Bukkit's {@link CommandSender} as the sender type. - * - * @param owningPlugin plugin owning the command manager - * @param commandExecutionCoordinator execution coordinator instance - * @return a new command manager - * @throws InitializationException if the construction of the manager fails - * @see #PaperCommandManager(Plugin, ExecutionCoordinator, SenderMapper) for a more thorough explanation - * @since 1.5.0 - */ - @API(status = API.Status.STABLE, since = "2.0.0") - public static @NonNull PaperCommandManager<@NonNull CommandSender> createNative( - final @NonNull Plugin owningPlugin, - final @NonNull ExecutionCoordinator commandExecutionCoordinator - ) throws InitializationException { - return new PaperCommandManager<>( - owningPlugin, - commandExecutionCoordinator, - SenderMapper.identity() - ); + @Override + public final boolean hasPermission(final @NonNull C sender, final @NonNull String permission) { + return this.senderMapper().reverse(sender).getSender().hasPermission(permission); } - /** - * Attempts to enable Brigadier command registration through the Paper API, falling - * back to {@link BukkitCommandManager#registerBrigadier()} if that fails. - * - *

Callers should check for {@link CloudBukkitCapabilities#NATIVE_BRIGADIER} first - * to avoid exceptions.

- * - *

A check for {@link CloudBukkitCapabilities#NATIVE_BRIGADIER} {@code ||} {@link CloudBukkitCapabilities#COMMODORE_BRIGADIER} - * may also be appropriate for some use cases (because of the fallback behavior), but not most, as Commodore does not offer - * any functionality on modern - * versions (see the documentation for {@link CloudBukkitCapabilities#COMMODORE_BRIGADIER}).

- * - * @see #hasCapability(CloudCapability) - * @throws BrigadierInitializationException when the prerequisite capabilities are not present or some other issue occurs - * during registration of Brigadier support - */ @Override - public synchronized void registerBrigadier() throws BrigadierInitializationException { - this.registerBrigadier(true); + public final @NonNull SenderMapper senderMapper() { + return this.senderMapper; } - /** - * Variant of {@link #registerBrigadier()} that only uses the old Paper-MojangAPI, even - * when the modern Paper commands API is present. This may be useful for debugging issues - * with the new Paper command system. - * - * @throws BrigadierInitializationException when the prerequisite capabilities are not present or some other issue occurs - * during registration of Brigadier support - * @deprecated This method will continue to work while the Paper commands API is still incubating, but will eventually no - * longer function when the old API is removed. - */ - @Deprecated - public synchronized void registerLegacyPaperBrigadier() throws BrigadierInitializationException { - this.registerBrigadier(false); + private void registerDefaultExceptionHandlers() { + this.registerDefaultExceptionHandlers( + triplet -> this.senderMapper().reverse(triplet.first().sender()).getSender() + .sendMessage(Component.text( + triplet.first().formatCaption(triplet.second(), triplet.third()), + NamedTextColor.RED + )), + pair -> this.owningPlugin().getLogger().log(Level.SEVERE, pair.first(), pair.second()) + ); } - private void registerBrigadier(final boolean allowModern) { - this.requireState(RegistrationState.BEFORE_REGISTRATION); - this.checkBrigadierCompatibility(); + @Override + public final PluginMeta owningPluginMeta() { + return this.pluginMeta; + } - if (this.brigadierManagerHolder != null) { - throw new IllegalStateException("Brigadier is already registered! Holder: " + this.brigadierManagerHolder); - } + @Override + public final boolean hasBrigadierManager() { + return true; + } - if (!this.hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) { - super.registerBrigadier(); - } else if (allowModern && CraftBukkitReflection.classExists("io.papermc.paper.command.brigadier.CommandSourceStack")) { - try { - final ModernPaperBrigadier brig = new ModernPaperBrigadier<>( - CommandSender.class, - this, - this.senderMapper(), - this::lockRegistration - ); - this.brigadierManagerHolder = brig; - brig.registerPlugin(this.owningPlugin()); - this.commandRegistrationHandler(brig); - } catch (final Exception e) { - throw new BrigadierInitializationException("Failed to register ModernPaperBrigadier", e); - } - } else { - try { - this.brigadierManagerHolder = new LegacyPaperBrigadier<>(this); - Bukkit.getPluginManager().registerEvents((Listener) this.brigadierManagerHolder, this.owningPlugin()); - this.brigadierManagerHolder.brigadierManager().settings().set(BrigadierSetting.FORCE_EXECUTABLE, true); - } catch (final Exception e) { - throw new BrigadierInitializationException("Failed to register LegacyPaperBrigadier", e); - } - } + @SuppressWarnings("unchecked") + @Override + public final @NonNull CloudBrigadierManager brigadierManager() { + return ((BrigadierManagerHolder) this.commandRegistrationHandler()) + .brigadierManager(); } /** - * {@inheritDoc} + * Variant of {@link PaperCommandManager} created at + * {@link io.papermc.paper.plugin.bootstrap.PluginBootstrap bootstrap} time + * rather than in {@link Plugin#onEnable()}. This allows command registered at bootstrap time to be used by + * data pack functions. * - * @return {@inheritDoc} - * @since 2.0.0 + * @param command sender type */ - @API(status = API.Status.STABLE, since = "2.0.0") - @Override - public boolean hasBrigadierManager() { - return this.brigadierManagerHolder != null || super.hasBrigadierManager(); + public static final class Bootstrapped extends PaperCommandManager { + private Bootstrapped( + final @NonNull PluginMeta pluginMeta, + final @NonNull ExecutionCoordinator executionCoordinator, + final @NonNull SenderMapper senderMapper + ) { + super(pluginMeta, executionCoordinator, senderMapper); + } + + /** + * Runs the second phase of initialization for managers created at bootstrap time. + * + *

This method must be called in {@link Plugin#onEnable()} for some features to work.

+ */ + public void onEnable() { + /* + ((ModernPaperBrigadier) this.commandRegistrationHandler()) + .registerPlugin(this.owningPlugin()); + */ + } } /** - * {@inheritDoc} + * First stage builder for {@link PaperCommandManager}. * - * @return {@inheritDoc} - * @throws BrigadierManagerNotPresent when {@link #hasBrigadierManager()} is false - * @since 1.2.0 + * @param command sender type */ - @API(status = API.Status.STABLE, since = "2.0.0") - @Override - public @NonNull CloudBrigadierManager brigadierManager() { - if (this.brigadierManagerHolder != null) { - return this.brigadierManagerHolder.brigadierManager(); + public static final class Builder { + private final SenderMapper senderMapper; + + private Builder(final SenderMapper senderMapper) { + this.senderMapper = senderMapper; + } + + /** + * Configures the {@link ExecutionCoordinator} for the manager. + * + * @param executionCoordinator execution coordinator + * @return coordinated builder + */ + public CoordinatedBuilder executionCoordinator(final ExecutionCoordinator executionCoordinator) { + return new CoordinatedBuilder<>(this.senderMapper, executionCoordinator); } - return super.brigadierManager(); } /** - * Registers asynchronous completions using the Paper API. This means the calling thread for suggestion queries will be a - * thread other than the {@link Server#isPrimaryThread() main server thread} (or, the sender's thread context on Folia). + * Second stage builder for {@link PaperCommandManager}. * - *

Requires the {@link CloudBukkitCapabilities#ASYNCHRONOUS_COMPLETION} capability to be present.

- * - *

It's not recommended to use this in combination with {@link #registerBrigadier()}, as Brigadier allows for - * non-blocking suggestions and the async completion API reduces the fidelity of suggestions compared to using Brigadier - * directly (see {@link PaperCommandManager#PaperCommandManager(Plugin, ExecutionCoordinator, SenderMapper)}).

- * - * @throws IllegalStateException when the server does not support asynchronous completions - * @see #hasCapability(CloudCapability) + * @param command sender type */ - public void registerAsynchronousCompletions() throws IllegalStateException { - this.requireState(RegistrationState.BEFORE_REGISTRATION); - if (!this.hasCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) { - throw new IllegalStateException("Failed to register asynchronous command completion listener."); + public static final class CoordinatedBuilder { + private final SenderMapper senderMapper; + private final ExecutionCoordinator executionCoordinator; + + private CoordinatedBuilder( + final SenderMapper senderMapper, + final ExecutionCoordinator executionCoordinator + ) { + this.senderMapper = senderMapper; + this.executionCoordinator = executionCoordinator; } - final SuggestionListenerFactory suggestionListenerFactory = SuggestionListenerFactory.create(this); - final SuggestionListener suggestionListener = suggestionListenerFactory.createListener(); + /** + * Creates a {@link PaperCommandManager} from {@link Plugin#onEnable()}. + * + * @param plugin plugin instance + * @return manager + * @see Bootstrapped + * @see #buildBootstrapped(BootstrapContext) + */ + @SuppressWarnings("unchecked") + public @NonNull PaperCommandManager buildOnEnable(final @NonNull Plugin plugin) { + final PaperCommandManager mgr = + new PaperCommandManager<>(plugin.getPluginMeta(), this.executionCoordinator, this.senderMapper); + ((ModernPaperBrigadier) mgr.commandRegistrationHandler()).registerPlugin(plugin); + return mgr; + } - Bukkit.getServer().getPluginManager().registerEvents( - suggestionListener, - this.owningPlugin() - ); + /** + * Creates a {@link PaperCommandManager.Bootstrapped} during bootstrapping. + * + * @param context bootstrap context + * @return manager + * @see Bootstrapped#onEnable() + */ + @SuppressWarnings("unchecked") + public PaperCommandManager.@NonNull Bootstrapped buildBootstrapped(final @NonNull BootstrapContext context) { + final PaperCommandManager.Bootstrapped mgr = + new PaperCommandManager.Bootstrapped<>(context.getPluginMeta(), this.executionCoordinator, this.senderMapper); + ((ModernPaperBrigadier) mgr.commandRegistrationHandler()).registerBootstrap(context); + return mgr; + } } } diff --git a/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/AsyncCommandSuggestionListener.java b/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/AsyncCommandSuggestionListener.java index 0f977e66..bc479164 100644 --- a/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/AsyncCommandSuggestionListener.java +++ b/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/AsyncCommandSuggestionListener.java @@ -30,16 +30,16 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.incendo.cloud.bukkit.BukkitPluginRegistrationHandler; import org.incendo.cloud.bukkit.internal.BukkitHelper; -import org.incendo.cloud.paper.PaperCommandManager; +import org.incendo.cloud.paper.LegacyPaperCommandManager; import org.incendo.cloud.suggestion.Suggestion; import org.incendo.cloud.suggestion.Suggestions; import org.incendo.cloud.util.StringUtils; class AsyncCommandSuggestionListener implements SuggestionListener { - private final PaperCommandManager paperCommandManager; + private final LegacyPaperCommandManager paperCommandManager; - AsyncCommandSuggestionListener(final @NonNull PaperCommandManager paperCommandManager) { + AsyncCommandSuggestionListener(final @NonNull LegacyPaperCommandManager paperCommandManager) { this.paperCommandManager = paperCommandManager; } diff --git a/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/BrigadierAsyncCommandSuggestionListener.java b/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/BrigadierAsyncCommandSuggestionListener.java index f20b2e91..7e4dc45a 100644 --- a/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/BrigadierAsyncCommandSuggestionListener.java +++ b/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/BrigadierAsyncCommandSuggestionListener.java @@ -30,7 +30,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.incendo.cloud.brigadier.suggestion.TooltipSuggestion; -import org.incendo.cloud.paper.PaperCommandManager; +import org.incendo.cloud.paper.LegacyPaperCommandManager; import org.incendo.cloud.paper.suggestion.tooltips.CompletionMapper; import org.incendo.cloud.paper.suggestion.tooltips.CompletionMapperFactory; import org.incendo.cloud.suggestion.SuggestionFactory; @@ -42,7 +42,7 @@ class BrigadierAsyncCommandSuggestionListener extends AsyncCommandSuggestionL private final CompletionMapperFactory completionMapperFactory = CompletionMapperFactory.detectingRelocation(); private final SuggestionFactory suggestionFactory; - BrigadierAsyncCommandSuggestionListener(final @NonNull PaperCommandManager paperCommandManager) { + BrigadierAsyncCommandSuggestionListener(final @NonNull LegacyPaperCommandManager paperCommandManager) { super(paperCommandManager); this.suggestionFactory = paperCommandManager.suggestionFactory().mapped(TooltipSuggestion::tooltipSuggestion); } diff --git a/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/SuggestionListenerFactory.java b/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/SuggestionListenerFactory.java index d7c5ebe7..300e1a43 100644 --- a/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/SuggestionListenerFactory.java +++ b/cloud-paper/src/main/java/org/incendo/cloud/paper/suggestion/SuggestionListenerFactory.java @@ -27,7 +27,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.incendo.cloud.bukkit.internal.CraftBukkitReflection; -import org.incendo.cloud.paper.PaperCommandManager; +import org.incendo.cloud.paper.LegacyPaperCommandManager; @API(status = API.Status.INTERNAL, since = "2.0.0") public interface SuggestionListenerFactory { @@ -39,7 +39,7 @@ public interface SuggestionListenerFactory { * @param commandManager the command manager * @return the suggestion listener factory */ - static @NonNull SuggestionListenerFactory create(final @NonNull PaperCommandManager commandManager) { + static @NonNull SuggestionListenerFactory create(final @NonNull LegacyPaperCommandManager commandManager) { return new SuggestionListenerFactoryImpl<>(commandManager); } @@ -53,9 +53,9 @@ public interface SuggestionListenerFactory { final class SuggestionListenerFactoryImpl implements SuggestionListenerFactory { - private final PaperCommandManager commandManager; + private final LegacyPaperCommandManager commandManager; - private SuggestionListenerFactoryImpl(final @NonNull PaperCommandManager commandManager) { + private SuggestionListenerFactoryImpl(final @NonNull LegacyPaperCommandManager commandManager) { this.commandManager = commandManager; } diff --git a/examples/example-bukkit/src/main/java/org/incendo/cloud/examples/bukkit/ExamplePlugin.java b/examples/example-bukkit/src/main/java/org/incendo/cloud/examples/bukkit/ExamplePlugin.java index d1f95068..f185e084 100644 --- a/examples/example-bukkit/src/main/java/org/incendo/cloud/examples/bukkit/ExamplePlugin.java +++ b/examples/example-bukkit/src/main/java/org/incendo/cloud/examples/bukkit/ExamplePlugin.java @@ -36,7 +36,7 @@ import org.incendo.cloud.minecraft.extras.MinecraftExceptionHandler; import org.incendo.cloud.minecraft.extras.MinecraftHelp; import org.incendo.cloud.minecraft.extras.caption.ComponentCaptionFormatter; -import org.incendo.cloud.paper.PaperCommandManager; +import org.incendo.cloud.paper.LegacyPaperCommandManager; import static net.kyori.adventure.text.Component.text; @@ -59,7 +59,7 @@ public void onEnable() { // (2) This function maps the Bukkit CommandSender to your custom sender type and back. If you're not using a custom // type, then SenderMapper.identity() maps CommandSender to itself. // - final PaperCommandManager manager = new PaperCommandManager<>( + final LegacyPaperCommandManager manager = new LegacyPaperCommandManager<>( /* Owning plugin */ this, /* (1) */ ExecutionCoordinator.simpleCoordinator(), /* (2) */ SenderMapper.identity() diff --git a/examples/example-paper/src/main/java/org/incendo/cloud/examples/paper/PaperPlugin.java b/examples/example-paper/src/main/java/org/incendo/cloud/examples/paper/PaperPlugin.java index 47dc1423..3ac7fe68 100644 --- a/examples/example-paper/src/main/java/org/incendo/cloud/examples/paper/PaperPlugin.java +++ b/examples/example-paper/src/main/java/org/incendo/cloud/examples/paper/PaperPlugin.java @@ -27,14 +27,14 @@ import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; -import org.incendo.cloud.paper.ModernPaperCommandManager; +import org.incendo.cloud.paper.PaperCommandManager; @SuppressWarnings("UnstableApiUsage") @DefaultQualifier(NonNull.class) public final class PaperPlugin extends JavaPlugin { - private final ModernPaperCommandManager.Bootstrapped commandManager; + private final PaperCommandManager.Bootstrapped commandManager; - public PaperPlugin(final ModernPaperCommandManager.Bootstrapped commandManager) { + public PaperPlugin(final PaperCommandManager.Bootstrapped commandManager) { this.commandManager = commandManager; } diff --git a/examples/example-paper/src/main/java/org/incendo/cloud/examples/paper/PluginBootstrap.java b/examples/example-paper/src/main/java/org/incendo/cloud/examples/paper/PluginBootstrap.java index e4d4a602..17c96d85 100644 --- a/examples/example-paper/src/main/java/org/incendo/cloud/examples/paper/PluginBootstrap.java +++ b/examples/example-paper/src/main/java/org/incendo/cloud/examples/paper/PluginBootstrap.java @@ -31,7 +31,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; import org.incendo.cloud.execution.ExecutionCoordinator; -import org.incendo.cloud.paper.ModernPaperCommandManager; +import org.incendo.cloud.paper.PaperCommandManager; import org.incendo.cloud.setting.ManagerSetting; import static org.incendo.cloud.parser.standard.StringParser.stringParser; @@ -39,12 +39,12 @@ @SuppressWarnings("UnstableApiUsage") @DefaultQualifier(NonNull.class) public final class PluginBootstrap implements io.papermc.paper.plugin.bootstrap.PluginBootstrap { - private ModernPaperCommandManager.@MonotonicNonNull Bootstrapped commandManager; + private PaperCommandManager.@MonotonicNonNull Bootstrapped commandManager; @Override public void bootstrap(final BootstrapContext context) { - final ModernPaperCommandManager.Bootstrapped mgr = - ModernPaperCommandManager.builder() + final PaperCommandManager.Bootstrapped mgr = + PaperCommandManager.builder() .executionCoordinator(ExecutionCoordinator.simpleCoordinator()) .buildBootstrapped(context); @@ -78,7 +78,7 @@ public JavaPlugin createPlugin(final PluginProviderContext context) { try { return Class.forName(context.getConfiguration().getMainClass()) .asSubclass(JavaPlugin.class) - .getDeclaredConstructor(ModernPaperCommandManager.Bootstrapped.class) + .getDeclaredConstructor(PaperCommandManager.Bootstrapped.class) .newInstance(this.commandManager); } catch (final ReflectiveOperationException e) { throw new RuntimeException(e);