diff --git a/README.md b/README.md index aea96de7..167f8beb 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ intuitive entry point for newcomers to the project. ## Features 🔨 **Easy-to-use** Tools -⏱ **Multi-Version** support from `1.18` - `26.x` +⏱ **Multi-Version** support from `1.20.6` - `26.x`
✔️ **User-friendly** GUIs and Commands 📆 **Automatic** Updates 🔌 **Easy setup** and configuration @@ -82,7 +82,7 @@ outdated.* 💻 **Requirements:** -- A Server running Paper 1.18 - **26.x**. +- A Server running Paper 1.20.6 - **26.x**. 🚩 **How to install:** @@ -102,6 +102,11 @@ outdated.* **For more help, please see the [wiki](https://resources.buildtheearth.net/s/btt).** +Use `/btt help` for in-game command help. See the +[Commands & Permissions](https://resources.buildtheearth.net/s/btt/doc/commands-permissions-wbqTdIlhWx) +and [Configuration](https://resources.buildtheearth.net/s/btt/doc/configuration-tpEHSZ6Zt2) +pages for the maintained command and configuration documentation. + ## Contributors Thanks to all our contributors for contributing towards the plugin development and improvement! diff --git a/build.gradle.kts b/build.gradle.kts index a3c44598..082b3e3a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,6 +48,9 @@ dependencies { implementation(libs.anvilgui) implementation(libs.clipper2) implementation(libs.dazzleconf) + implementation(libs.cloud.core) + implementation(libs.cloud.paper) + implementation(libs.cloud.minecraft.extras) implementation(libs.json) implementation(libs.googlecode.gson) implementation(libs.okhttp.jvm) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fa184903..f6309f4f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,10 @@ bstats = "3.2.1" buildtheearth-projection = "1.0.5" # @pin Waiting for dependency update in alps - lib - Used in AlpsLib-Utils#GeneratorUtils https://github.com/micycle1/Clipper2-java/releases clipper2 = "1.5.4" +# https://github.com/Incendo/cloud +cloud-core = "2.1.0" +# https://github.com/Incendo/cloud-minecraft +cloud-minecraft = "2.0.1" # Ref https://github.com/A248/DazzleConf/releases dazzleconf = "2.0.0-M3" # Ref: https://github.com/IntellectualSites/bom @@ -61,6 +65,9 @@ bluemap-api = { module = "de.bluecolored:bluemap-api", version.ref = "bluemap-ap bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" } buildtheearth-projection = { module = "net.buildtheearth:projection", version.ref = "buildtheearth-projection" } clipper2 = { module = "com.github.micycle1:Clipper2-java", version.ref = "clipper2" } +cloud-core = { module = "org.incendo:cloud-core", version.ref = "cloud-core" } +cloud-paper = { module = "org.incendo:cloud-paper", version.ref = "cloud-minecraft" } +cloud-minecraft-extras = { module = "org.incendo:cloud-minecraft-extras", version.ref = "cloud-minecraft" } dazzleconf = { module = "space.arim.dazzleconf:dazzleconf-yaml", version.ref = "dazzleconf" } com-alpsbte-alpslib-alpslib-libpsterra = { module = "com.alpsbte.alpslib:alpslib-libpsterra", version.ref = "alpslib-libpsterra" } fawe-bom = { module = "com.intellectualsites.bom:bom-newest", version.ref = "fawe-bom" } diff --git a/src/main/java/net/buildtheearth/buildteamtools/BuildTeamTools.java b/src/main/java/net/buildtheearth/buildteamtools/BuildTeamTools.java index 21d44fdc..e9314e42 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/BuildTeamTools.java +++ b/src/main/java/net/buildtheearth/buildteamtools/BuildTeamTools.java @@ -4,6 +4,7 @@ import lombok.Getter; import net.buildtheearth.buildteamtools.modules.ModuleHandler; import net.buildtheearth.buildteamtools.modules.common.CommonModule; +import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; import net.buildtheearth.buildteamtools.modules.miscellaneous.MiscModule; import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule; @@ -38,6 +39,8 @@ public class BuildTeamTools extends JavaPlugin { private ConfigurationLoader navigationConfigLoader; private ConfigurationLoader generatorConfigLoader; private ConfigurationLoader plotSystemConfigLoader; + @Getter + private BttCommandManager commandManager; @Override public void onEnable() { @@ -53,6 +56,7 @@ public void onEnable() { NavigationConfig navigationConfig = navigationConfigLoader.load(); GeneratorConfig generatorConfig = generatorConfigLoader.load(); plotSystemConfigLoader.load(); + commandManager = new BttCommandManager(this); // Register Modules ModuleHandler.getInstance().replaceModules( @@ -65,6 +69,7 @@ public void onEnable() { PlotSystemModule.getInstance() ); ModuleHandler.getInstance().enableAll(null); + commandManager.registerEnabledCommands(); } @Override diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/Module.java b/src/main/java/net/buildtheearth/buildteamtools/modules/Module.java index adc437b2..5993d5a5 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/Module.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/Module.java @@ -3,18 +3,13 @@ import com.alpsbte.alpslib.utils.ChatHelper; import com.alpsbte.alpslib.utils.WikiDocumented; import lombok.Getter; -import lombok.NonNull; import net.buildtheearth.buildteamtools.BuildTeamTools; import org.bukkit.Bukkit; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.PluginCommand; -import org.bukkit.command.TabCompleter; import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; /** @@ -37,12 +32,6 @@ public abstract class Module implements WikiDocumented { @Getter private final List listeners = new ArrayList<>(); - @Getter - private final HashMap commands = new HashMap<>(); - - @Getter - private final HashMap tabCompleter = new HashMap<>(); - @Getter private final List dependsOnModules = new ArrayList<>(); @@ -74,10 +63,12 @@ protected Module(String moduleName, String wikiPage, Module... dependsOnModules) public void enable() { checkForModuleDependencies(); - loadCommands(); loadListeners(); enabled = true; + BuildTeamTools plugin = BuildTeamTools.getInstance(); + if (plugin != null && plugin.getCommandManager() != null) + plugin.getCommandManager().registerEnabledCommands(this); } /** @@ -113,48 +104,6 @@ public void shutdown(String reason) { protected void registerCommands() { } - /** - * Registers a command for the module. - * Note that this method will only register the command in the module, but it won't load it in Bukkit. - * To load the command, use the loadCommands() method. - * - * @param command The command to register - * @param executor The executor for the command. If the executor is also a TabCompleter, the TabCompleter will be - * registered as well. - */ - protected void registerCommand(@NonNull String command, @NonNull CommandExecutor executor) { - this.commands.put(BuildTeamTools.getInstance().getCommand(command), executor); - - if (executor instanceof TabCompleter tc) - this.tabCompleter.put(BuildTeamTools.getInstance().getCommand(command), tc); - } - - /** - * Registers a command for the module. - * Note that this method will only register the command in the module, but it won't load it in Bukkit. - * To load the command, use the loadCommands() method. - * - * @param command The command to register - * @param executor The executor for the command. - * @param tabCompleter The tab completer for the command. Only needed to specify if the TabCompleter is in a different - * class than the executor. - */ - protected void registerCommand(@NonNull String command, @NonNull CommandExecutor executor, - @NonNull TabCompleter tabCompleter) { - this.commands.put(BuildTeamTools.getInstance().getCommand(command), executor); - this.tabCompleter.put(BuildTeamTools.getInstance().getCommand(command), tabCompleter); - } - - /** - * Loads the commands for the module into Bukkit - */ - private void loadCommands() { - for (var command : commands.entrySet()) - command.getKey().setExecutor(command.getValue()); - - for (var command : tabCompleter.entrySet()) - command.getKey().setTabCompleter(command.getValue()); - } /** diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/CommonModule.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/CommonModule.java index aeb0808a..6526c6ab 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/CommonModule.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/CommonModule.java @@ -8,7 +8,6 @@ import net.buildtheearth.buildteamtools.modules.common.components.dependency.DependencyComponent; import net.buildtheearth.buildteamtools.modules.common.components.pluginmessaging.PluginMessagingComponent; import net.buildtheearth.buildteamtools.modules.common.components.updater.UpdaterComponent; -import net.buildtheearth.buildteamtools.modules.common.listeners.CommandListener; import net.buildtheearth.buildteamtools.modules.common.listeners.ExceptionListener; import net.buildtheearth.buildteamtools.modules.common.metrics.MetricsManager; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; @@ -72,12 +71,12 @@ public void enable() { @Override public void registerCommands() { - registerCommand("buildteamtools", new BuildTeamToolsCommand()); + new BuildTeamToolsCommand().register(this); } @Override public void registerListeners() { - super.registerListeners(new MenuFunctionListener(), new CommandListener(), new ExceptionListener()); + super.registerListeners(new MenuFunctionListener(), new ExceptionListener()); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java new file mode 100644 index 00000000..c44a4f32 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java @@ -0,0 +1,535 @@ +package net.buildtheearth.buildteamtools.modules.common.commands; + +import com.alpsbte.alpslib.utils.ChatHelper; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.Module; +import net.buildtheearth.buildteamtools.modules.network.model.Permissions; +import net.buildtheearth.buildteamtools.utils.io.MainConfig; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.component.DefaultValue; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.exception.ArgumentParseException; +import org.incendo.cloud.exception.InvalidCommandSenderException; +import org.incendo.cloud.exception.InvalidSyntaxException; +import org.incendo.cloud.exception.NoPermissionException; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.minecraft.extras.MinecraftHelp; +import org.incendo.cloud.minecraft.extras.RichDescription; +import org.incendo.cloud.paper.PaperCommandManager; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.parser.standard.StringParser; +import org.incendo.cloud.permission.Permission; +import org.incendo.cloud.setting.ManagerSetting; +import org.incendo.cloud.suggestion.SuggestionProvider; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class BttCommandManager { + + private static final Set COMMAND_NAMES = Set.of( + "btt", "generate", "kml", "geopoints", "geopath", "georing", "geosurface", + "navigator", "buildteam", "btwarps", "explore", "address", "warp", "blockpalette", + "help", "communicators", "cache", "debug", "checkforupdates", "reload-config", "update" + ); + private static final ParserDescriptor ARGUMENT_PARSER = + StringParser.greedyStringParser(); + + private final Logger logger; + private final MainConfig config; + private final CommandManager manager; + private final MinecraftHelp help; + private final Set claimedLabels = new HashSet<>(COMMAND_NAMES); + private final Map> aliasesByCommand = new HashMap<>(); + private final List pendingRegistrations = new ArrayList<>(); + + public BttCommandManager(BuildTeamTools plugin) { + this(PaperCommandManager.builder() + .executionCoordinator(ExecutionCoordinator.simpleCoordinator()) + .buildOnEnable(plugin), plugin.getMainConfig(), plugin.getLogger()); + } + + BttCommandManager(CommandManager manager, MainConfig config, Logger logger) { + this.manager = manager; + this.config = config; + this.logger = logger; + manager.settings().set(ManagerSetting.ALLOW_UNSAFE_REGISTRATION, true); + help = MinecraftHelp.create("/btt help", manager, CommandSourceStack::getSender); + + manager.exceptionController().registerHandler(InvalidSyntaxException.class, context -> + context.context().sender().getSender().sendMessage(ChatHelper.getErrorString( + "Invalid command syntax. Usage: %s", context.exception().correctSyntax()))); + manager.exceptionController().registerHandler(ArgumentParseException.class, context -> + context.context().sender().getSender().sendMessage(ChatHelper.getErrorString( + "Invalid command argument: %s", context.exception().getMessage()))); + manager.exceptionController().registerHandler(NoPermissionException.class, context -> + context.context().sender().getSender().sendMessage(ChatHelper.getErrorString( + "You don't have permission to use this command."))); + manager.exceptionController().registerHandler(InvalidCommandSenderException.class, context -> + context.context().sender().getSender().sendMessage(ChatHelper.getErrorString( + "This command cannot be used by your sender type."))); + } + + public void registerRoot(Module owner, Consumer handler) { + if (isConfiguredDisabled(owner)) return; + + List aliases = validatedAliases( + "btt", + config.commands().labels().btt(), + true + ); + aliasesByCommand.put("btt", aliases); + registerWhenEnabled(owner, () -> { + try { + manager.command(manager.commandBuilder("btt", aliases.toArray(String[]::new)) + .commandDescription(description("Show BuildTeamTools status and connection information.")) + .permission(Permissions.BUILD_TEAM_TOOLS) + .handler(context -> execute(owner, context.sender().getSender(), "btt", + () -> handler.accept(context.sender().getSender())))); + } catch (RuntimeException exception) { + logger.log(Level.SEVERE, "Failed to register the BTT command root.", exception); + throw exception; + } + }); + } + + public void registerRootSubcommand( + Module owner, + String name, + String description, + String permission, + CommandHandler handler + ) { + if (isConfiguredDisabled(owner)) return; + + registerWhenEnabled(owner, () -> manager.command(rootBuilder() + .literal(name, description(description)) + .commandDescription(description(description)) + .permission(Permission.allOf( + Permission.of(Permissions.BUILD_TEAM_TOOLS), + Permission.of(permission) + )) + .handler(context -> execute(owner, context.sender().getSender(), name, () -> + handler.execute(context.sender().getSender(), name, new String[0]))))); + } + + public void registerRootNestedSubcommand( + Module owner, + String parent, + String name, + String description, + String permission, + Consumer> handler + ) { + if (isConfiguredDisabled(owner)) return; + + RichDescription parentDescription = description(rootSubcommandDescription(parent)); + registerWhenEnabled(owner, () -> manager.command(rootBuilder() + .literal(parent, parentDescription) + .literal(name, description(description)) + .commandDescription(description(description)) + .permission(Permission.allOf( + Permission.of(Permissions.BUILD_TEAM_TOOLS), + Permission.of(permission) + )) + .handler(context -> execute(owner, context.sender().getSender(), + parent + " " + name, () -> handler.accept(context))))); + } + + public void registerRootHelp(Module owner) { + if (isConfiguredDisabled(owner)) return; + + registerWhenEnabled(owner, () -> manager.command(rootBuilder() + .literal("help", description("Show available commands or details for a command.")) + .optional("query", ARGUMENT_PARSER, DefaultValue.constant("")) + .commandDescription(description("Show available commands or details for a command.")) + .permission(Permissions.BUILD_TEAM_TOOLS) + .handler(context -> execute(owner, context.sender().getSender(), "btt help", () -> + help.queryCommands(context.optional("query").orElse(""), context.sender()))))); + } + + public void register(Module owner, String name, BiConsumer handler) { + registerAliased(owner, name, configuredAliases(name), + (sender, label, arguments) -> handler.accept(sender, arguments)); + } + + public void register( + Module owner, + String name, + String permission, + BiConsumer handler, + BiFunction> suggestions + ) { + registerAliased(owner, name, configuredAliases(name), + (sender, label, arguments) -> handler.accept(sender, arguments), + (sender, label, input) -> suggestions.apply(sender, input), + Permission.of(permission)); + } + + public void register( + Module owner, + String name, + Permission permission, + BiConsumer handler, + BiFunction> suggestions + ) { + registerAliased(owner, name, configuredAliases(name), + (sender, label, arguments) -> handler.accept(sender, arguments), + (sender, label, input) -> suggestions.apply(sender, input), + permission); + } + + public void register( + Module owner, + String name, + BiConsumer handler, + BiFunction> suggestions + ) { + registerAliased(owner, name, configuredAliases(name), + (sender, label, arguments) -> handler.accept(sender, arguments), suggestions); + } + + public void registerAliased(Module owner, String name, List aliases, CommandHandler handler) { + registerAliased(owner, name, aliases, handler, (sender, label, input) -> List.of()); + } + + public void registerAliased( + Module owner, + String name, + List aliases, + CommandHandler handler, + BiFunction> suggestions + ) { + registerAliased(owner, name, aliases, handler, + (sender, label, input) -> suggestions.apply(sender, input)); + } + + public void registerAliased( + Module owner, + String name, + List aliases, + CommandHandler handler, + SuggestionHandler suggestions + ) { + registerAliased(owner, name, aliases, handler, suggestions, null); + } + + public void registerAliased( + Module owner, + String name, + List aliases, + CommandHandler handler, + SuggestionHandler suggestions, + Permission permission + ) { + registerAliasedInternal(owner, name, aliases, handler, suggestions, permission); + } + + private void registerAliasedInternal( + Module owner, + String name, + List aliases, + CommandHandler handler, + SuggestionHandler suggestions, + Permission permission + ) { + if (isConfiguredDisabled(owner)) return; + + List validAliases = validatedAliases(name, aliases, false); + aliasesByCommand.put(name, validAliases); + registerWhenEnabled(owner, () -> { + try { + for (var base : featureBuilders(name)) { + var builder = base + .commandDescription(descriptionFor(name)) + .optional("arguments", ARGUMENT_PARSER, + suggestionProvider((sender, input) -> + suggestions.suggest(sender.getSender(), name, input))); + if (permission != null) builder = builder.permission(permission); + manager.command(builder.handler(context -> + execute(owner, context.sender().getSender(), name, () -> + handler.execute(context.sender().getSender(), name, + splitArguments(context.optional("arguments").orElse("")))))); + } + } catch (RuntimeException exception) { + logger.log(Level.SEVERE, "Failed to register command '" + name + "'.", exception); + throw exception; + } + }); + } + + public void registerNoArguments(Module owner, String name, Consumer handler) { + registerNoArguments(owner, name, null, handler); + } + + public void registerNoArguments( + Module owner, + String name, + String permission, + Consumer handler + ) { + if (isConfiguredDisabled(owner)) return; + + List aliases = validatedAliases(name, configuredAliases(name), false); + aliasesByCommand.put(name, aliases); + registerWhenEnabled(owner, () -> { + for (var base : featureBuilders(name)) { + var builder = base.commandDescription(descriptionFor(name)); + if (permission != null) builder = builder.permission(permission); + manager.command(builder.handler(context -> execute(owner, context.sender().getSender(), name, + () -> handler.accept(context.sender().getSender())))); + } + }); + } + + public void registerRequiredArgument( + Module owner, + String name, + String argumentName, + String argumentDescription, + ParserDescriptor parser, + BiConsumer handler + ) { + registerRequiredArgument(owner, name, argumentName, argumentDescription, parser, + null, (sender, input) -> List.of(), handler); + } + + public void registerRequiredArgument( + Module owner, + String name, + String argumentName, + String argumentDescription, + ParserDescriptor parser, + String permission, + BiFunction> suggestions, + BiConsumer handler + ) { + if (isConfiguredDisabled(owner)) return; + + List aliases = validatedAliases(name, configuredAliases(name), false); + aliasesByCommand.put(name, aliases); + registerWhenEnabled(owner, () -> { + for (var base : featureBuilders(name)) { + var builder = base.commandDescription(descriptionFor(name)) + .required(argumentName, parser, description(argumentDescription), + suggestionProvider((sender, input) -> suggestions.apply(sender.getSender(), input))); + if (permission != null) builder = builder.permission(permission); + manager.command(builder.handler(context -> execute(owner, context.sender().getSender(), name, () -> + handler.accept(context.sender().getSender(), context.get(argumentName))))); + } + }); + } + + public void registerSubcommand( + Module owner, + String commandName, + String subcommand, + String description, + UnaryOperator> argumentBuilder, + Consumer> handler + ) { + if (isConfiguredDisabled(owner)) return; + + RichDescription richDescription = description(description); + registerWhenEnabled(owner, () -> { + for (var base : featureBuilders(commandName)) { + var builder = base + .literal(subcommand, richDescription) + .commandDescription(richDescription); + manager.command(argumentBuilder.apply(builder) + .handler(context -> execute(owner, context.sender().getSender(), + commandName + " " + subcommand, () -> handler.accept(context)))); + } + }); + } + + private boolean isConfiguredDisabled(Module owner) { + return owner != null + && !owner.getModuleName().equalsIgnoreCase("Common") + && config.disabledModules().stream() + .anyMatch(owner.getModuleName()::equalsIgnoreCase); + } + + public void registerEnabledCommands() { + registerPending(null, false); + } + + public void registerEnabledCommands(Module owner) { + if (owner.isEnabled()) { + registerPending(owner, true); + } + } + + private void registerPending(Module owner, boolean matchOwner) { + var iterator = pendingRegistrations.iterator(); + while (iterator.hasNext()) { + PendingCommandRegistration pending = iterator.next(); + boolean matches = matchOwner + ? pending.owner() == owner + : pending.owner() == null || pending.owner().isEnabled(); + if (matches) { + pending.registration().run(); + iterator.remove(); + } + } + } + + private void registerWhenEnabled(Module owner, Runnable registration) { + if (owner != null && owner.isEnabled()) { + registration.run(); + } else { + pendingRegistrations.add(new PendingCommandRegistration(owner, registration)); + } + } + + private Command.Builder rootBuilder() { + return manager.commandBuilder("btt", aliasesByCommand.getOrDefault("btt", List.of()).toArray(String[]::new)); + } + + private List> featureBuilders(String name) { + List labels = aliasesByCommand.get(name); + if (labels == null) { + throw new IllegalStateException("Register command '" + name + "' before its subcommands."); + } + List> builders = new ArrayList<>(); + // The list is the standalone labels, so an empty list means no standalone command. + if (!labels.isEmpty()) { + builders.add(manager.commandBuilder(labels.getFirst(), + labels.subList(1, labels.size()).toArray(String[]::new))); + } + String[] subcommandAliases = labels.stream().filter(label -> !label.equals(name)).toArray(String[]::new); + builders.add(rootBuilder().literal(name, descriptionFor(name), subcommandAliases)); + return builders; + } + + private List configuredAliases(String name) { + MainConfig.Labels labels = config.commands().labels(); + return switch (name) { + case "generate" -> labels.generate(); + case "kml" -> labels.kml(); + case "geopoints" -> labels.geopoints(); + case "geopath" -> labels.geopath(); + case "georing" -> labels.georing(); + case "geosurface" -> labels.geosurface(); + case "navigator" -> labels.navigator(); + case "buildteam" -> labels.buildteam(); + case "btwarps" -> labels.btwarps(); + case "explore" -> labels.explore(); + case "address" -> labels.address(); + case "warp" -> labels.warp(); + case "blockpalette" -> labels.blockpalette(); + default -> List.of(); + }; + } + + private List validatedAliases(String command, List aliases, boolean rootCommand) { + List accepted = new ArrayList<>(); + if (aliases == null) { + logger.warning("Ignoring null aliases configured for " + command + "."); + return accepted; + } + for (String alias : aliases) { + String normalized = alias == null ? "" : alias.toLowerCase(Locale.ROOT); + boolean validLabel = normalized.matches("[a-z0-9_-]+"); + boolean configuredRootAlias = rootCommand && normalized.equals("buildteamtools"); + boolean canonicalLabel = !rootCommand && normalized.equals(command); + if (!validLabel || (!configuredRootAlias && !canonicalLabel && claimedLabels.contains(normalized))) { + logger.warning("Ignoring invalid alias '" + alias + "' configured for " + command + "."); + continue; + } + if (accepted.contains(normalized) || (!canonicalLabel && !claimedLabels.add(normalized))) { + logger.warning("Ignoring duplicate alias '" + alias + "' configured for " + command + "."); + continue; + } + accepted.add(normalized); + } + return List.copyOf(accepted); + } + + private static SuggestionProvider suggestionProvider( + BiFunction> suggestions + ) { + return SuggestionProvider.blockingStrings((context, input) -> + suggestions.apply(context.sender(), input.remainingInput())); + } + + private static RichDescription description(String description) { + return RichDescription.of(Component.text(description)); + } + + private static RichDescription descriptionFor(String name) { + String description = switch (name) { + case "btt" -> "Show BuildTeamTools status and connection information."; + case "generate" -> "Generate geographic data for a build team."; + case "kml", "geopoints", "geopath", "georing", "geosurface" -> + "Create KML geometry from selected map points."; + case "navigator" -> "Open the build team navigation menu."; + case "buildteam" -> "Switch to a build team by name or tag."; + case "btwarps" -> "Open the warp menu for a build team."; + case "explore" -> "Explore nearby build team regions."; + case "address" -> "Look up the address at your current location."; + case "warp" -> "Warp to a saved location."; + case "blockpalette" -> "Open the block palette menu."; + default -> "BuildTeamTools command."; + }; + return description(description); + } + + private static String[] splitArguments(String arguments) { + return arguments.isBlank() ? new String[0] : arguments.trim().split("\\s+"); + } + + private static String rootSubcommandDescription(String name) { + return switch (name) { + case "cache" -> "View the network cache or upload and refresh it."; + case "debug" -> "Show or change debug mode."; + default -> "BuildTeamTools administrative subcommand."; + }; + } + + public static List matchingSuggestions(List options, String input) { + String partial = input.substring(input.lastIndexOf(' ') + 1); + return options.stream() + .filter(value -> value.toLowerCase(Locale.ROOT).startsWith(partial.toLowerCase(Locale.ROOT))) + .toList(); + } + + private static void execute(Module owner, CommandSender sender, String label, Runnable handler) { + if (!owner.isEnabled()) { + String reason = owner.getError() == null || owner.getError().isBlank() + ? "" + : " Reason: " + owner.getError(); + sender.sendMessage(ChatHelper.getErrorString("The Module %s is currently disabled.%s", + owner.getModuleName(), reason)); + return; + } + try { + handler.run(); + } catch (RuntimeException exception) { + BuildTeamTools.getInstance().getSLF4JLogger().error("Command '{}' failed", label, exception); + sender.sendMessage(ChatHelper.getErrorString("An error occurred while executing this command.")); + } + } + + @FunctionalInterface + public interface CommandHandler { + void execute(CommandSender sender, String label, String[] arguments); + } + + @FunctionalInterface + public interface SuggestionHandler { + List suggest(CommandSender sender, String label, String input); + } + + private record PendingCommandRegistration(Module owner, Runnable registration) { + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BuildTeamToolsCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BuildTeamToolsCommand.java index 1286a6e8..6b79f8f4 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BuildTeamToolsCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BuildTeamToolsCommand.java @@ -10,95 +10,107 @@ import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.buildtheearth.buildteamtools.modules.network.model.Region; import net.buildtheearth.buildteamtools.modules.stats.StatsModule; -import net.buildtheearth.buildteamtools.utils.Utils; import net.md_5.bungee.api.chat.ComponentBuilder; import net.md_5.bungee.api.chat.HoverEvent; import net.md_5.bungee.api.chat.TextComponent; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; import org.jetbrains.annotations.NotNull; import org.jspecify.annotations.NonNull; -import java.util.*; - -public class BuildTeamToolsCommand implements CommandExecutor, TabCompleter { - - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String @NotNull [] args) { - - if (!sender.hasPermission(Permissions.BUILD_TEAM_TOOLS)) { - Permissions.sendNoPermissionMessage(sender, Permissions.BUILD_TEAM_TOOLS); - return true; - } - - if (args.length == 0) { - sendBuildTeamToolsInfo(sender); - return true; - } - - switch (args[0].toLowerCase()) { - case "communicators" -> { - if (!sender.hasPermission(Permissions.BUILD_TEAM_TOOLS_COMMUNICATORS)) { - Permissions.sendNoPermissionMessage(sender, Permissions.BUILD_TEAM_TOOLS_COMMUNICATORS); - return true; - } +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class BuildTeamToolsCommand { + + public void register(Module owner) { + var commandManager = BuildTeamTools.getInstance().getCommandManager(); + commandManager.registerRoot(owner, BuildTeamToolsCommand::sendBuildTeamToolsInfo); + commandManager.registerRootHelp(owner); + commandManager.registerRootSubcommand(owner, "communicators", + "List players currently communicating with the network.", + Permissions.BUILD_TEAM_TOOLS_COMMUNICATORS, + (sender, label, args) -> communicatorsCommand(sender, args)); + commandManager.registerRootSubcommand(owner, "cache", + "View the network cache or upload and refresh it.", + Permissions.BUILD_TEAM_TOOLS_CACHE, + (sender, label, args) -> cacheCommand(sender)); + commandManager.registerRootNestedSubcommand(owner, "cache", "update", + "Upload and refresh the network cache.", + Permissions.BUILD_TEAM_TOOLS_CACHE, context -> updateCacheCommand(context.sender().getSender())); + commandManager.registerRootSubcommand(owner, "debug", + "Show or change debug mode.", + Permissions.BUILD_TEAM_TOOLS_DEBUG, + (sender, label, args) -> debugCommand(sender)); + commandManager.registerRootNestedSubcommand(owner, "debug", "on", + "Enable debug mode.", + Permissions.BUILD_TEAM_TOOLS_DEBUG, context -> setDebugMode(context.sender().getSender(), true)); + commandManager.registerRootNestedSubcommand(owner, "debug", "off", + "Disable debug mode.", + Permissions.BUILD_TEAM_TOOLS_DEBUG, context -> setDebugMode(context.sender().getSender(), false)); + commandManager.registerRootSubcommand(owner, "checkforupdates", + "Check whether a plugin update is available.", + Permissions.BUILD_TEAM_TOOLS_CHECK_FOR_UPDATES, + (sender, label, args) -> checkForUpdatesCommand(sender, args)); + commandManager.registerRootSubcommand(owner, "reload-config", + "Reload module configuration files.", + Permissions.BUILD_TEAM_TOOLS_RELOAD, + (sender, label, args) -> reloadCommand(sender)); + commandManager.registerRootSubcommand(owner, "update", + "Update the plugin to the latest version.", + Permissions.BUILD_TEAM_TOOLS_UPDATE, + (sender, label, args) -> updateCommand(sender)); + } - ChatHelper.sendMessageBox(sender, "Build Team Communicators", () -> { - for (UUID uuid : NetworkModule.getInstance().getCommunicators()) - sender.sendMessage("§7- §e" + uuid.toString()); - }); - } - case "cache" -> cacheCommand(sender, args); - case "debug" -> debugCommand(sender, args); - case "checkforupdates" -> CommonModule.getInstance().getUpdaterComponent().checkForUpdates(sender); - case "reload-config", "reload" -> reloadCommand(sender); - case "update" -> updateCommand(sender); - default -> ChatHelper.sendMessageBox(sender, "Build Team Help", () -> { - sender.sendMessage("§e/btt cache [update] §8- §7View the cache or upload it to the network & update it locally."); - sender.sendMessage("§e/btt checkForUpdates §8- §7Check for updates."); - sender.sendMessage("§e/btt communicators §8- §7List of players who communicate with the network."); - sender.sendMessage("§e/btt debug §8- §7Enable or disable debug mode."); - sender.sendMessage("§e/btt help §8- §7List of all sub commands."); - sender.sendMessage("§e/btt reload-config §8- §7Reload all configs for the modules"); - sender.sendMessage("§e/btt update §8- §7Update the plugin to the latest version."); - }); - } + private void communicatorsCommand(@NotNull CommandSender sender, String @NotNull [] args) { + ChatHelper.sendMessageBox(sender, "Build Team Communicators", () -> { + for (UUID uuid : NetworkModule.getInstance().getCommunicators()) + sender.sendMessage("§7- §e" + uuid); + }); + } - return true; + private void checkForUpdatesCommand(@NotNull CommandSender sender, String @NotNull [] args) { + CommonModule.getInstance().getUpdaterComponent().checkForUpdates(sender); } - private static void debugCommand(@NonNull CommandSender sender, String @NonNull [] args) { + private static void debugCommand(@NonNull CommandSender sender) { if (!sender.hasPermission(Permissions.BUILD_TEAM_TOOLS_DEBUG)) { Permissions.sendNoPermissionMessage(sender, Permissions.BUILD_TEAM_TOOLS_DEBUG); return; } - if (args.length == 1 || (!args[1].equalsIgnoreCase("on") && !args[1].equalsIgnoreCase("off"))) { - sender.sendMessage(ChatHelper.getStandardComponent(true, "Current Debug Mode: %s. You need to add a value: " + - "on/off to change it", BuildTeamTools.getInstance().isDebug() ? "ON" : "OFF")); - return; - } - - boolean debug = args[1].equalsIgnoreCase("on"); + sender.sendMessage(ChatHelper.getStandardComponent(true, "Current Debug Mode: %s.", + BuildTeamTools.getInstance().isDebug() ? "ON" : "OFF")); + } + private static void setDebugMode(@NonNull CommandSender sender, boolean debug) { BuildTeamTools.getInstance().setDebug(debug); sender.sendMessage(ChatHelper.getStandardComponent(true, "Debug Mode was set to: %s", debug)); } - private static void cacheCommand(@NonNull CommandSender sender, String @NonNull [] args) { + private static void cacheCommand(@NonNull CommandSender sender) { if (!sender.hasPermission(Permissions.BUILD_TEAM_TOOLS_CACHE)) { Permissions.sendNoPermissionMessage(sender, Permissions.BUILD_TEAM_TOOLS_CACHE); return; } - if (args.length > 1 && args[1].equalsIgnoreCase("update")) { - NetworkModule.getInstance().updateCache(); - if (NetworkModule.getInstance().getBuildTeam() != null) NetworkModule.getInstance().enableDisabledModules(); - StatsModule.getInstance().updateAndSave(); - sender.sendMessage(ChatHelper.getSuccessComponent("Cache successfully updated.")); + displayCache(sender); + } + + private static void updateCacheCommand(@NonNull CommandSender sender) { + if (!sender.hasPermission(Permissions.BUILD_TEAM_TOOLS_CACHE)) { + Permissions.sendNoPermissionMessage(sender, Permissions.BUILD_TEAM_TOOLS_CACHE); + return; } + NetworkModule.getInstance().updateCache(); + if (NetworkModule.getInstance().getBuildTeam() != null) NetworkModule.getInstance().enableDisabledModules(); + StatsModule.getInstance().updateAndSave(); + sender.sendMessage(ChatHelper.getSuccessComponent("Cache successfully updated.")); + displayCache(sender); + } + + private static void displayCache(CommandSender sender) { if (StatsModule.getInstance().isEnabled()) { ChatHelper.sendMessageBox(sender, "Build Team Cache", () -> sender.sendMessage(StatsModule.getInstance().getCurrentCache().toJSONString())); @@ -134,18 +146,6 @@ private void updateCommand(@NotNull CommandSender sender) { CommonModule.getInstance().getUpdaterComponent().update(sender, true); } - @Override - public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String @NotNull [] args) { - if (args.length == 1) - return Arrays.asList("help", "communicators", "checkForUpdates", "cache", "debug", "reload-config", "update"); - - List debugSuggestions = Utils.getTabCompleterArgs(args, "debug", 2, Arrays.asList("on", "off")); - if (debugSuggestions != null) - return debugSuggestions; - - return Utils.getTabCompleterArgs(args, "cache", 2, Collections.singletonList("update")); - } - public static void sendBuildTeamToolsInfo(CommandSender sender) { ChatHelper.sendMessageBox(sender, "Build Team Tools", () -> { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/common/listeners/CommandListener.java b/src/main/java/net/buildtheearth/buildteamtools/modules/common/listeners/CommandListener.java deleted file mode 100644 index ec08c78d..00000000 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/listeners/CommandListener.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.buildtheearth.buildteamtools.modules.common.listeners; - -import com.alpsbte.alpslib.utils.ChatHelper; -import net.buildtheearth.buildteamtools.modules.Module; -import net.buildtheearth.buildteamtools.modules.ModuleHandler; -import org.bukkit.command.PluginCommand; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; - -import java.util.List; - -public class CommandListener implements Listener { - - @EventHandler - public void onCommand(PlayerCommandPreprocessEvent e) { - String message = e.getMessage(); - - - for (Module module : ModuleHandler.getInstance().getModules()) - if (!module.isEnabled()) - for (PluginCommand command : module.getCommands().keySet()) { - if (command == null) continue; - List commands = command.getAliases(); - commands.add(command.getLabel()); - - for (String alias : commands) { - if (message.toLowerCase().equalsIgnoreCase("/" + alias.toLowerCase())) { - String reason = ""; - - if (module.getError() != null && !module.getError().isEmpty()) - reason = " Reason: " + module.getError(); - - ChatHelper.sendErrorMessage(e.getPlayer(), "The Module " + module.getModuleName() + " is currently " + - "disabled." + reason); - e.setCancelled(true); - return; - } - } - } - } -} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/GeneratorModule.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/GeneratorModule.java index 12bbee25..be1cb016 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/GeneratorModule.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/GeneratorModule.java @@ -10,7 +10,6 @@ import net.buildtheearth.buildteamtools.modules.generator.components.field.Field; import net.buildtheearth.buildteamtools.modules.generator.components.house.House; import net.buildtheearth.buildteamtools.modules.generator.components.kml.KmlCommand; -import net.buildtheearth.buildteamtools.modules.generator.components.kml.KmlTabCompleter; import net.buildtheearth.buildteamtools.modules.generator.components.rail.Rail; import net.buildtheearth.buildteamtools.modules.generator.components.road.Road; import net.buildtheearth.buildteamtools.modules.generator.components.tree.Tree; @@ -87,8 +86,8 @@ public void enable() { @Override public void registerCommands() { - registerCommand("generate", new GeneratorCommand()); - registerCommand("kml", new KmlCommand(), new KmlTabCompleter()); + new GeneratorCommand().register(this); + new KmlCommand().register(this); } @Override diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/commands/GeneratorCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/commands/GeneratorCommand.java index 13df92f6..09eb7159 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/commands/GeneratorCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/commands/GeneratorCommand.java @@ -1,86 +1,71 @@ package net.buildtheearth.buildteamtools.modules.generator.commands; import com.alpsbte.alpslib.utils.ChatHelper; +import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; +import net.buildtheearth.buildteamtools.modules.Module; +import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; import net.buildtheearth.buildteamtools.modules.generator.menu.GeneratorMenu; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorType; import net.buildtheearth.buildteamtools.modules.generator.model.HistoryEntry; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.buildtheearth.buildteamtools.utils.Utils; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; +import net.kyori.adventure.text.Component; +import org.incendo.cloud.component.DefaultValue; +import org.incendo.cloud.parser.standard.StringParser; +import org.incendo.cloud.suggestion.SuggestionProvider; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.ArrayList; import java.util.List; -public class GeneratorCommand implements CommandExecutor, TabCompleter { +public class GeneratorCommand { private static final List HISTORY_COMMANDS = List.of("history", "undo", "redo"); - private static final List SUB_COMMANDS = createSubCommands(); private static final List HELP_ARGUMENTS = List.of("help", "info", "?"); - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String cmdLabel, String @NotNull [] args) { - if (!(sender instanceof Player p)) { - sender.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( - "Only players can execute this command." - ))); - return true; + public void register(Module owner) { + var commandManager = BuildTeamTools.getInstance().getCommandManager(); + commandManager.register(owner, "generate", Permissions.GENERATOR_USE, this::execute, this::suggestions); + commandManager.registerSubcommand(owner, "generate", "history", "Show your recent generation history.", + builder -> builder.permission(Permissions.GENERATOR_USE), + context -> handleHistoryCommand(context.sender().getSender())); + commandManager.registerSubcommand(owner, "generate", "undo", "Undo your last generation.", + builder -> builder.permission(Permissions.GENERATOR_USE), + context -> handleHistoryAction(context.sender().getSender(), true)); + commandManager.registerSubcommand(owner, "generate", "redo", "Redo your last undone generation.", + builder -> builder.permission(Permissions.GENERATOR_USE), + context -> handleHistoryAction(context.sender().getSender(), false)); + + for (GeneratorType generatorType : GeneratorType.values()) { + String generatorName = generatorType.getCommandName(); + commandManager.registerSubcommand(owner, "generate", generatorName, + "Generate " + generatorType.getName().toLowerCase() + " structures.", + builder -> builder.permission(Permissions.GENERATOR_USE) + .optional("arguments", StringParser.greedyStringParser(), + DefaultValue.constant(""), + org.incendo.cloud.minecraft.extras.RichDescription.of( + Component.text("Generator flags and values")), + SuggestionProvider.blockingStrings((context, input) -> + BttCommandManager.matchingSuggestions( + HELP_ARGUMENTS, input.remainingInput()))), + context -> runGeneratorCommand(context.sender().getSender(), generatorType, + context.optional("arguments").orElse(""))); } + } - if (!p.hasPermission(Permissions.GENERATOR_USE)) { - p.sendMessage(ChatHelper.getErrorString("You don't have permission to use this command!")); - return true; - } + private void execute(@NotNull CommandSender sender, String @NotNull [] args) { + Player player = playerWithPermission(sender); + if (player == null) return; if (args.length == 0) { - new GeneratorMenu(p, true); - return true; - } - - GeneratorType generatorType = GeneratorType.fromCommandName(args[0]); - if (generatorType != null) - return runGeneratorCommand(p, args, generatorType); - - switch (args[0].toLowerCase()) { - case "history": - if (GeneratorModule.getInstance().getPlayerHistory(p).getHistoryEntries().isEmpty()) { - p.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( - "You didn't generate any structures yet. Use /gen to create one." - ))); - return true; - } - - ChatHelper.sendMessageBox(sender, "Generator History for " + p.getName(), () -> { - for (HistoryEntry history : GeneratorModule.getInstance().getPlayerHistory(p).getHistoryEntries()) { - long timeDifference = System.currentTimeMillis() - history.getTimeCreated(); - p.sendMessage(ChatHelper.getStandardComponent( - false, - "- %s - %s ago - %s Commands executed", - history.getGeneratorType().name(), - Utils.toDate(timeDifference), - history.getWorldEditCommandCount() - )); - } - }); - return true; - - case "undo": - GeneratorModule.getInstance().getPlayerHistory(p).undoCommand(p); - return true; - - case "redo": - GeneratorModule.getInstance().getPlayerHistory(p).redoCommand(p); - return true; - - default: - sendHelp(p); - return true; + new GeneratorMenu(player, true); + } else { + sendHelp(player); } } @@ -96,72 +81,104 @@ public static void sendHelp(CommandSender sender) { .append(ChatHelper.getStandardComponent( false, "History: %s, %s, %s", - "/gen history", - "/gen undo", - "/gen redo"))) + "/btt generate history", + "/btt generate undo", + "/btt generate redo"))) ); } - @Override - public List onTabComplete( - @NotNull CommandSender sender, - @NotNull Command command, - @NotNull String label, - String @NotNull [] args - ) { - if (!sender.hasPermission(Permissions.GENERATOR_USE)) - return List.of(); + private void handleHistoryCommand(CommandSender sender) { + Player player = playerWithPermission(sender); + if (player == null) return; - if (args.length == 1) - return getMatchingCompletions(SUB_COMMANDS, args[0]); + List entries = GeneratorModule.getInstance().getPlayerHistory(player).getHistoryEntries(); + if (entries.isEmpty()) { + player.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "You didn't generate any structures yet. Use /btt generate to create one." + ))); + return; + } - if (args.length == 2 && isGeneratorSubCommand(args[0])) - return getMatchingCompletions(HELP_ARGUMENTS, args[1]); + ChatHelper.sendMessageBox(sender, "Generator History for " + player.getName(), () -> { + for (HistoryEntry history : entries) { + long timeDifference = System.currentTimeMillis() - history.getTimeCreated(); + player.sendMessage(ChatHelper.getStandardComponent( + false, + "- %s - %s ago - %s Commands executed", + history.getGeneratorType().name(), + Utils.toDate(timeDifference), + history.getWorldEditCommandCount() + )); + } + }); + } - return List.of(); + private void handleHistoryAction(CommandSender sender, boolean undo) { + Player player = playerWithPermission(sender); + if (player == null) return; + if (undo) { + GeneratorModule.getInstance().getPlayerHistory(player).undoCommand(player); + } else { + GeneratorModule.getInstance().getPlayerHistory(player).redoCommand(player); + } } - private boolean runGeneratorCommand(Player player, String[] args, GeneratorType generatorType) { + private void runGeneratorCommand(CommandSender sender, GeneratorType generatorType, String arguments) { + Player player = playerWithPermission(sender); + if (player == null) return; + if (generatorType == GeneratorType.FIELD) { player.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( "This generator has serious issues and is currently disabled." ))); - return true; + return; } - generatorType.getComponent(GeneratorModule.getInstance()).analyzeCommand(player, args); - return true; + generatorType.getComponent(GeneratorModule.getInstance()).analyzeCommand( + player, prependGeneratorName(generatorType.getCommandName(), arguments)); } - private boolean isGeneratorSubCommand(String value) { - return GeneratorType.fromCommandName(value) != null; + private static Player playerWithPermission(CommandSender sender) { + if (!(sender instanceof Player player)) { + sender.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "Only players can execute this command." + ))); + return null; + } + if (!player.hasPermission(Permissions.GENERATOR_USE)) { + player.sendMessage(ChatHelper.getErrorString("You don't have permission to use this command!")); + return null; + } + return player; } - private List getMatchingCompletions(List options, String input) { - String normalizedInput = input.toLowerCase(); - List completions = new ArrayList<>(); - - for (String option : options) - if (option.startsWith(normalizedInput)) - completions.add(option); - - return completions; + private static String[] prependGeneratorName(String generatorName, String arguments) { + String[] remaining = arguments.isBlank() ? new String[0] : arguments.trim().split("\\s+"); + String[] commandArguments = new String[remaining.length + 1]; + commandArguments[0] = generatorName; + System.arraycopy(remaining, 0, commandArguments, 1, remaining.length); + return commandArguments; } - private static List createSubCommands() { - List commands = new ArrayList<>(); - - Arrays.stream(GeneratorType.values()) - .map(GeneratorType::getCommandName) - .forEach(commands::add); - - commands.addAll(HISTORY_COMMANDS); - return List.copyOf(commands); + private List suggestions(CommandSender sender, String input) { + if (!sender.hasPermission(Permissions.GENERATOR_USE)) return List.of(); + List topLevel = new ArrayList<>(); + Arrays.stream(GeneratorType.values()).map(GeneratorType::getCommandName).forEach(topLevel::add); + topLevel.addAll(HISTORY_COMMANDS); + + String[] arguments = input.trim().split("\\s+"); + if (input.isBlank() || (arguments.length == 1 && !input.endsWith(" "))) + return BttCommandManager.matchingSuggestions(topLevel, input); + if (arguments.length == 1 && GeneratorType.fromCommandName(arguments[0]) != null) + return HELP_ARGUMENTS; + if (arguments.length == 2 && GeneratorType.fromCommandName(arguments[0]) != null) + return BttCommandManager.matchingSuggestions(HELP_ARGUMENTS, arguments[1]); + return List.of(); } private static String[] getGeneratorHelpCommands() { return Arrays.stream(GeneratorType.values()) - .map(type -> "/gen " + type.getCommandName() + " help") + .map(type -> "/btt generate " + type.getCommandName() + " help") .toArray(String[]::new); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java index 34015026..26c16eb6 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlCommand.java @@ -4,11 +4,14 @@ import com.cryptomorin.xseries.XMaterial; import de.micromata.opengis.kml.v_2_2_0.Coordinate; import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.Module; +import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; import net.buildtheearth.buildteamtools.modules.navigation.NavUtils; import net.buildtheearth.buildteamtools.utils.BlockLocation; import net.buildtheearth.buildteamtools.utils.LineRasterization; import net.buildtheearth.buildteamtools.utils.PolygonTools; import net.buildtheearth.model.GeographicalCoordinate; +import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -17,11 +20,11 @@ import org.bukkit.block.BlockState; import org.bukkit.block.CommandBlock; import org.bukkit.command.BlockCommandSender; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.metadata.FixedMetadataValue; +import org.incendo.cloud.minecraft.extras.RichDescription; +import org.incendo.cloud.parser.standard.StringParser; import org.jspecify.annotations.NonNull; import java.util.*; @@ -45,7 +48,80 @@ * direct use of the /kml command is restricted to CommandBlockSender * */ -public class KmlCommand implements CommandExecutor { +public class KmlCommand { + private static final String GEOPOINTS_COMMAND = "geopoints"; + private static final String GEOPATH_COMMAND = "geopath"; + private static final String GEORING_COMMAND = "georing"; + private static final String GEOSURFACE_COMMAND = "geosurface"; + private static final String EXTEND_PREFIX = "-extend:"; + private static final String INVALID_BLOCK_METADATA_MESSAGE = + "§cServer received /btt kml command with invalid blocktype string metadata. Using bricks as fallback."; + + public void register(Module owner) { + var aliases = BuildTeamTools.getInstance().getMainConfig().commands().labels(); + var commandManager = BuildTeamTools.getInstance().getCommandManager(); + commandManager.registerAliased(owner, "kml", aliases.kml(), this::execute, this::suggestions); + registerMode(owner, GEOPOINTS_COMMAND, aliases.geopoints()); + registerMode(owner, GEOPATH_COMMAND, aliases.geopath()); + registerMode(owner, GEORING_COMMAND, aliases.georing()); + registerMode(owner, GEOSURFACE_COMMAND, aliases.geosurface()); + registerUndo(owner, "kml"); + registerUndo(owner, GEOPOINTS_COMMAND); + registerUndo(owner, GEOPATH_COMMAND); + registerUndo(owner, GEORING_COMMAND); + registerUndo(owner, GEOSURFACE_COMMAND); + } + + private void registerUndo(Module owner, String commandName) { + BuildTeamTools.getInstance().getCommandManager().registerSubcommand(owner, commandName, "undo", + "Undo the last KML block operation.", + builder -> builder.optional("arguments", StringParser.greedyStringParser(), + RichDescription.of(Component.text("Additional command-block KML content"))), + context -> { + String trailingArguments = context.optional("arguments").orElse(""); + String[] trailing = trailingArguments.isBlank() + ? new String[0] + : trailingArguments.trim().split("\\s+"); + String[] args = new String[trailing.length + 1]; + args[0] = "undo"; + System.arraycopy(trailing, 0, args, 1, trailing.length); + + CommandSender sender = context.sender().getSender(); + switch (sender) { + case BlockCommandSender commandBlockSender -> processKml(commandBlockSender.getBlock(), args); + case Player player -> undoCommand(player); + default -> execute(sender, commandName, args); + } + }); + } + + private void registerMode(Module owner, String mode, List aliases) { + BuildTeamTools.getInstance().getCommandManager().registerAliased(owner, mode, aliases, + (sender, label, arguments) -> execute(sender, mode, arguments), this::suggestions); + } + + private List suggestions(CommandSender sender, String label, String input) { + if (label.equalsIgnoreCase("kml")) { + return BttCommandManager.matchingSuggestions(List.of("undo"), input); + } + String partial = input.substring(input.lastIndexOf(' ') + 1).toLowerCase(Locale.ROOT); + List suggestions = new ArrayList<>(); + if (partial.startsWith(EXTEND_PREFIX)) { + String blockPrefix = partial.substring(EXTEND_PREFIX.length()); + BLOCK_TYPES.stream().filter(block -> block.startsWith(blockPrefix)) + .map(block -> EXTEND_PREFIX + block).forEach(suggestions::add); + } else { + BLOCK_TYPES.stream().filter(block -> block.startsWith(partial)).forEach(suggestions::add); + if (EXTEND_PREFIX.startsWith(partial)) suggestions.add(EXTEND_PREFIX); + } + suggestions.add("undo"); + return suggestions; + } + + private static final List BLOCK_TYPES = Arrays.stream(Material.values()) + .filter(Material::isBlock) + .map(material -> material.name().toLowerCase(Locale.ROOT)) + .toList(); /** * Handles the execution of KML-related commands (/geopoints, /geopath, /georing, /kml). @@ -57,34 +133,34 @@ public class KmlCommand implements CommandExecutor { * * * @param sender The command sender (Player or CommandBlock) - * @param cmd The command object * @param alias The alias used to invoke this command (geopoints/geopath/georing/kml) * @param args Command arguments (KML content if from CommandBlock, block type if from Player) - * @return true if the command was handled successfully, false otherwise */ - public boolean onCommand(@NonNull CommandSender sender, @NonNull Command cmd, @NonNull String alias, String @NonNull [] args) { + private void execute(@NonNull CommandSender sender, @NonNull String alias, String @NonNull [] args) { if (sender instanceof BlockCommandSender cmdbSender) { Block senderBlock = cmdbSender.getBlock(); - return processKml(senderBlock, args); + processKml(senderBlock, args); + return; } if (!(sender instanceof Player p)) { sender.sendMessage("§cOnly players can execute this command."); - return false; + return; } if (args.length > 0 && args[0].equals("undo")) { - return undoCommand(p); + undoCommand(p); + return; } //check if alias is geopoints or geopath (direct /kml is only allowed for undo) if (alias.equals("kml")) { - sender.sendMessage("§cPlease use /geopoints or /geopath to execute this command."); - return false; + sender.sendMessage("§cPlease use /btt geopoints or /btt geopath to execute this command."); + return; } - return createPasteUI(p, cmd, alias, args); + createPasteUI(p, alias, args); } /** @@ -182,7 +258,7 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) { if (playerName.isEmpty() || blocktypeString.isEmpty()) { //invalid metadata, cancel //send error message to all players within 50m of the command block - ChatHelper.sendMessageToPlayersNearLocation(senderBlock.getLocation(), "§cReceived /kml command from CommandBlock without sufficient metadata.\nThis command can only be executed from a CommandBlock created with the /kml command!", 50); + ChatHelper.sendMessageToPlayersNearLocation(senderBlock.getLocation(), "§cReceived /btt kml command from CommandBlock without sufficient metadata.\nThis command can only be executed from a CommandBlock created with a /btt geo command!", 50); return false; } @@ -197,13 +273,13 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) { Material matchedBlockMaterial = Material.matchMaterial(blocktypeString); final Material blockMaterial = (matchedBlockMaterial != null && matchedBlockMaterial.isBlock()) ? matchedBlockMaterial : Material.BRICKS; if (matchedBlockMaterial == null || !matchedBlockMaterial.isBlock()) { - player.sendMessage("§cServer received /kml command with invalid blocktype string metadata. Using bricks as fallback."); + player.sendMessage(INVALID_BLOCK_METADATA_MESSAGE); } Material matchedExtendMaterial = Material.matchMaterial(extendToGroundBlockType); final Material extendMaterial = (matchedExtendMaterial != null && matchedExtendMaterial.isBlock()) ? matchedExtendMaterial : Material.BRICKS; if (matchedExtendMaterial == null || !matchedExtendMaterial.isBlock()) { - player.sendMessage("§cServer received /kml command with invalid blocktype string metadata. Using bricks as fallback."); + player.sendMessage(INVALID_BLOCK_METADATA_MESSAGE); } //parse kml @@ -310,7 +386,7 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) { debugMessage.append("Some blocks are >1000 blocks from your location (max distance 1000). "); } if (!preventedUnloadedChunkChanges[0] && !preventedFarChanges[0]) { - debugMessage.append("No positions were calculated from the KML. Check your coordinates and alias (/geopoints, /geopath, /georing)."); + debugMessage.append("No positions were calculated from the KML. Check your coordinates and command (/btt geopoints, /btt geopath, /btt georing)."); } player.sendMessage(debugMessage.toString()); return; @@ -350,7 +426,7 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) { Material material = Material.matchMaterial(previousCommandBlockType); if (material == null) { - player.sendMessage("§cServer received /kml command with invalid blocktype string metadata. Using bricks as fallback."); + player.sendMessage(INVALID_BLOCK_METADATA_MESSAGE); material = Material.BRICKS; } @@ -411,18 +487,17 @@ public boolean processKml(@NonNull Block senderBlock, String[] args) { * After creation, the player should: *
    *
  1. Right-click the CommandBlock
  2. - *
  3. Paste the KML content after "/kml "
  4. + *
  5. Paste the KML content after "/btt kml "
  6. *
  7. Set the CommandBlock to "Always Active"
  8. *
  9. Confirm to execute
  10. *
* * @param player The player creating the CommandBlock UI - * @param cmd The command object (unused) * @param alias The command alias used (determines operation mode) * @param args Command arguments (block type and extend options) * @return true if the CommandBlock was created successfully */ - public boolean createPasteUI(Player player, Command cmd, String alias, String @NonNull [] args) { + public boolean createPasteUI(Player player, String alias, String @NonNull [] args) { //The command either creates a command-block at the player location //arguments are an // optional blocktype @@ -430,11 +505,10 @@ public boolean createPasteUI(Player player, Command cmd, String alias, String @N String blocktype = "BRICKS"; boolean extendToGround = false; String extendToGroundBlockType = "GREEN_WOOL"; - String prefix_extendParam = "-extend:"; for (String arg : args) { - if (arg.startsWith(prefix_extendParam)) { + if (arg.startsWith(EXTEND_PREFIX)) { extendToGround = true; - extendToGroundBlockType = arg.substring(prefix_extendParam.length()).toUpperCase(); + extendToGroundBlockType = arg.substring(EXTEND_PREFIX.length()).toUpperCase(); if (Material.matchMaterial(extendToGroundBlockType) == null) { player.sendMessage(String.format("§cInvalid block type for extend parameter '%s'. Using bricks as fallback.", extendToGroundBlockType)); @@ -464,7 +538,7 @@ public boolean createPasteUI(Player player, Command cmd, String alias, String @N //for now, user has to manually set the command to "auto" to get it immediately triggered on confirm CommandBlock cmdBlock = (CommandBlock) block.getState(); - cmdBlock.setCommand("/kml "); //ready to paste kml content + cmdBlock.setCommand("/btt kml "); //ready to paste kml content cmdBlock.setMetadata("kmlPlayerName", new FixedMetadataValue(BuildTeamTools.getInstance(), player.getName())); //cmdBlock.setMetadata("kmlPlayerID", new FixedMetadataValue(BuildTeamTools.getInstance(), p.getUniqueId())); cmdBlock.setMetadata("kmlBlocktype", new FixedMetadataValue(BuildTeamTools.getInstance(), blocktype)); @@ -617,10 +691,10 @@ public boolean undoCommand(Player player) { */ private BlockCreationMode commandToCreationMode(@NonNull String command) { return switch (command) { - case "geopoints" -> BlockCreationMode.POINTS; - case "geopath" -> BlockCreationMode.PATH; - case "georing" -> BlockCreationMode.CLOSED_PATH; - case "geosurface" -> + case GEOPOINTS_COMMAND -> BlockCreationMode.POINTS; + case GEOPATH_COMMAND -> BlockCreationMode.PATH; + case GEORING_COMMAND -> BlockCreationMode.CLOSED_PATH; + case GEOSURFACE_COMMAND -> throw new UnsupportedOperationException("Operation mode 'filled surface' is not yet implemented"); //return BlockCreationMode.FILLED; default -> throw new IllegalArgumentException(command); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlTabCompleter.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlTabCompleter.java deleted file mode 100644 index 74328869..00000000 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlTabCompleter.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.kml; - -import org.bukkit.Material; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; -import org.jspecify.annotations.NonNull; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class KmlTabCompleter implements TabCompleter { - public KmlTabCompleter() { - //compile list of block material types for completion - blocktypes = new ArrayList(); - for (Material mat : Material.values()) { - if (mat.isBlock()) - blocktypes.add(mat.toString().toLowerCase()); - } - } - - - public List onTabComplete(@NonNull CommandSender sender, @NonNull Command command, @NonNull String alias, String @NonNull [] args) { - //the player can use /geopoints and /geopath with a single argument: "undo" or Blocktype - // - List completions = new ArrayList<>(); - - if (sender instanceof Player) { - //alias kml only has undo as return option - if (alias.equalsIgnoreCase("kml")) - completions.add("undo"); - else if (args.length > 0) { - //player has started to type an argument - //allowed arguments are blocktype and/or -extend:blocktype - String currentArg = args[args.length - 1]; - if (currentArg.startsWith("-")) { - if (currentArg.startsWith("-extend:")) { - addMatchingBlocktypeCompletions(currentArg, completions, "-extend:"); - } else { - completions.add("-extend:"); - } - } else { - //add all blocktypes that match the characters of the argument - addMatchingBlocktypeCompletions(currentArg, completions, ""); - } - - Collections.sort(completions); - - completions.add("undo"); - - } - } - - return completions; - } - - private void addMatchingBlocktypeCompletions(String currentArg, List completions, String prefix) { - //add all blocktypes that match the characters of the argument - String argWithoutPrefix = currentArg.substring(prefix.length()); - for (String blocktype : blocktypes) { - if (blocktype.startsWith(argWithoutPrefix.toLowerCase())) { - completions.add(prefix + blocktype); - } - } - } - - private final List blocktypes; -} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java index 4cdcb672..ff0d3639 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/GeneratorComponent.java @@ -116,7 +116,7 @@ public String getCommand(@NonNull Player p) { case FIELD -> "field"; }; - StringBuilder command = new StringBuilder("/gen " + type); + StringBuilder command = new StringBuilder("/btt generate " + type); for (var flag : flags.entrySet()) command.append(" -").append(flag.getKey().getFlag()).append(" ").append(flag.getValue()); @@ -130,7 +130,7 @@ public void sendSuccessMessage(Player p) { .hoverEvent(HoverEvent.showText(Component.text("Click to copy command", NamedTextColor.GRAY))); TextComponent undo = Component.text("[UNDO]", NamedTextColor.RED, TextDecoration.BOLD) - .clickEvent(ClickEvent.runCommand("/gen undo")) + .clickEvent(ClickEvent.runCommand("/btt generate undo")) .hoverEvent(HoverEvent.showText(Component.text("Click to undo last generation", NamedTextColor.GRAY))); Component message = ChatHelper.getSuccessComponent("%s successfully generated.", generatorType.getName()) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/History.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/History.java index 0a022bdc..eb469f09 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/History.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/History.java @@ -39,7 +39,7 @@ public void addHistoryEntry(HistoryEntry entry) { public void undoCommand(Player p) { if (getHistoryEntries().isEmpty()) { p.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( - "You didn't generate any structures yet. Use /gen to create one. You can only undo the last structure." + "You didn't generate any structures yet. Use /btt generate to create one. You can only undo the last structure." ))); return; } @@ -61,8 +61,8 @@ public void undoCommand(Player p) { p.playSound(p.getLocation(), Sound.ENTITY_ZOMBIE_DESTROY_EGG, 1.0F, 1.0F); - Component redoMessage = ChatHelper.getStandardComponent(false, "Use %s to redo it.", "/gen redo") - .clickEvent(ClickEvent.runCommand("/gen redo")) + Component redoMessage = ChatHelper.getStandardComponent(false, "Use %s to redo it.", "/btt generate redo") + .clickEvent(ClickEvent.runCommand("/btt generate redo")) .hoverEvent(HoverEvent.showText(Component.text("Click to redo the last structure.", NamedTextColor.GRAY))); p.sendMessage(ChatHelper.getSuccessComponent("Successfully %s the last structure.", "undid") .appendNewline() @@ -73,7 +73,7 @@ public void undoCommand(Player p) { public void redoCommand(Player p) { if (getUndoHistoryEntries().isEmpty()) { p.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( - "You didn't undo any structures yet. Use /gen undo to undo one. You can only redo the last structure." + "You didn't undo any structures yet. Use /btt generate undo to undo one. You can only redo the last structure." ))); return; } @@ -95,8 +95,8 @@ public void redoCommand(Player p) { p.playSound(p.getLocation(), Sound.ENTITY_ZOMBIE_DESTROY_EGG, 1.0F, 1.0F); - Component undoMessage = ChatHelper.getStandardComponent(false, "Use %s to undo it.", "/gen undo") - .clickEvent(ClickEvent.runCommand("/gen undo")) + Component undoMessage = ChatHelper.getStandardComponent(false, "Use %s to undo it.", "/btt generate undo") + .clickEvent(ClickEvent.runCommand("/btt generate undo")) .hoverEvent(HoverEvent.showText(Component.text("Click to undo the last structure.", NamedTextColor.GRAY))); p.sendMessage(ChatHelper.getSuccessComponent("Successfully %s the last structure.", "redid") .appendNewline() diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/MiscModule.java b/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/MiscModule.java index e84cddc9..3a5ac926 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/MiscModule.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/MiscModule.java @@ -1,17 +1,10 @@ package net.buildtheearth.buildteamtools.modules.miscellaneous; +import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.miscellaneous.blockpalettegui.BlockPaletteCommand; import net.buildtheearth.buildteamtools.modules.miscellaneous.blockpalettegui.BlockPaletteGUI; -import net.buildtheearth.buildteamtools.modules.miscellaneous.blockpalettegui.BlockPaletteManager; import net.buildtheearth.buildteamtools.utils.WikiLinks; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.PluginCommand; -import org.bukkit.command.TabCompleter; -import org.bukkit.plugin.java.JavaPlugin; -import org.jspecify.annotations.NonNull; - -import java.lang.reflect.Constructor; public class MiscModule extends Module { @@ -30,12 +23,8 @@ public static MiscModule getInstance() { public void enable() { super.enable(); - JavaPlugin plugin = resolvePlugin(); - - blockPaletteGUI = new BlockPaletteGUI(plugin); + blockPaletteGUI = new BlockPaletteGUI(BuildTeamTools.getInstance()); blockPaletteGUI.enable(); - - registerCommandSafely(plugin, "blockpalette", blockPaletteGUI.getManager()); } @Override @@ -55,51 +44,8 @@ public void registerListeners() { // No Listeners } - private @NonNull JavaPlugin resolvePlugin() { - try { - return JavaPlugin.getProvidingPlugin(MiscModule.class); - } catch (Exception t) { - throw new IllegalStateException("Cannot resolve JavaPlugin for MiscModule", t); - } - } - - private void registerCommandSafely(@NonNull JavaPlugin plugin, String name, BlockPaletteManager manager) { - PluginCommand cmd = plugin.getCommand(name); - if (cmd == null) { - return; - } - - Object obj = createCommandInstance(manager, plugin); - if (obj instanceof CommandExecutor exec) cmd.setExecutor(exec); - if (obj instanceof TabCompleter tab) cmd.setTabCompleter(tab); - } - - private @NonNull Object createCommandInstance(BlockPaletteManager manager, JavaPlugin plugin) { - try { - Class c = BlockPaletteCommand.class; - Constructor k; - - try { - k = c.getConstructor(BlockPaletteManager.class, JavaPlugin.class); - return k.newInstance(manager, plugin); - } catch (NoSuchMethodException ignored) { - } - - try { - k = c.getConstructor(BlockPaletteManager.class); - return k.newInstance(manager); - } catch (NoSuchMethodException ignored) { - } - - try { - k = c.getConstructor(JavaPlugin.class); - return k.newInstance(plugin); - } catch (NoSuchMethodException ignored) { - } - - return c.getDeclaredConstructor().newInstance(); - } catch (Exception t) { - throw new RuntimeException("Cannot construct BlockPaletteCommand", t); - } + @Override + public void registerCommands() { + new BlockPaletteCommand(() -> blockPaletteGUI.getManager(), BuildTeamTools.getInstance()).register(this); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java index ce5faad9..783a9be8 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/miscellaneous/blockpalettegui/BlockPaletteCommand.java @@ -1,15 +1,23 @@ package net.buildtheearth.buildteamtools.modules.miscellaneous.blockpalettegui; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; +import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; +import net.buildtheearth.buildteamtools.modules.Module; +import net.buildtheearth.buildteamtools.modules.network.model.Permissions; +import net.kyori.adventure.text.Component; import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.util.StringUtil; +import org.incendo.cloud.component.DefaultValue; +import org.incendo.cloud.minecraft.extras.RichDescription; +import org.incendo.cloud.parser.standard.StringParser; +import org.incendo.cloud.suggestion.SuggestionProvider; import org.jspecify.annotations.NonNull; -import java.util.*; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; /** * /bp @@ -17,83 +25,78 @@ * /bp filter * /bp filter … */ -public class BlockPaletteCommand implements CommandExecutor, TabCompleter { +public class BlockPaletteCommand { - private final BlockPaletteManager blockPalletManager; + private final Supplier blockPalletManager; private final JavaPlugin plugin; - public BlockPaletteCommand(BlockPaletteManager blockPalletManager, JavaPlugin plugin) { + public BlockPaletteCommand(Supplier blockPalletManager, JavaPlugin plugin) { this.blockPalletManager = blockPalletManager; this.plugin = plugin; } - @Override - public boolean onCommand(@NonNull CommandSender sender, - @NonNull Command command, - @NonNull String label, - String @NonNull [] args) { + public void register(Module owner) { + var commandManager = BuildTeamTools.getInstance().getCommandManager(); + commandManager.register(owner, "blockpalette", Permissions.BLOCK_PALETTE_USE, this::execute, + (sender, input) -> { + String[] arguments = input.trim().split("\\s+"); + if (input.isBlank() || arguments.length == 1) + return BttCommandManager.matchingSuggestions(List.of("menu", "filter"), input); + return List.of(); + }); + commandManager.registerSubcommand(owner, "blockpalette", "menu", + "Open the block palette and reset its filters.", builder -> + builder.permission(Permissions.BLOCK_PALETTE_USE), context -> { + CommandSender sender = context.sender().getSender(); + if (sender instanceof Player player) + blockPalletManager.get().setPlayerFiltersAndOpen(player); + else + sender.sendMessage("This command can only be used by players."); + }); + commandManager.registerSubcommand(owner, "blockpalette", "filter", + "Open the filter menu, optionally applying filters first.", builder -> + builder.permission(Permissions.BLOCK_PALETTE_USE) + .optional("filters", StringParser.greedyStringParser(), + DefaultValue.constant(""), + RichDescription.of(Component.text("Filter names")), + SuggestionProvider.blockingStrings((context, input) -> + BttCommandManager.matchingSuggestions( + BlockPaletteMenuType.FILTER_OPTIONS, + input.remainingInput() + ))), context -> { + CommandSender sender = context.sender().getSender(); + if (!(sender instanceof Player player)) { + sender.sendMessage("This command can only be used by players."); + return; + } + + String filterInput = context.optional("filters").orElse(""); + BlockPaletteManager manager = blockPalletManager.get(); + if (!filterInput.isBlank()) { + Set newFilters = new HashSet<>(List.of(filterInput.split("\\s+"))); + manager.updatePlayerFilters(player, newFilters); + } + new ChoosePaletteMenu(manager, player, plugin).open(); + }); + } + + private void execute(@NonNull CommandSender sender, String @NonNull [] args) { + if (!Permissions.checkPermission(sender, Permissions.BLOCK_PALETTE_USE)) return; if (!(sender instanceof Player player)) { sender.sendMessage("This command can only be used by players."); - return true; + return; } + BlockPaletteManager manager = blockPalletManager.get(); // /bp ⇒ open block menu with remembered filters (do NOT reset to "color") if (args.length == 0) { - blockPalletManager.openBlockMenu(player); - return true; - } - - // /bp menu ⇒ reset to default filters + open block menu - if (args.length == 1 && args[0].equalsIgnoreCase("menu")) { - blockPalletManager.setPlayerFiltersAndOpen(player); - return true; - } - - // /bp filter ⇒ just open the filter GUI - if (args.length == 1 && args[0].equalsIgnoreCase("filter")) { - new ChoosePaletteMenu(blockPalletManager, player, plugin).open(); - return true; - } - - // /bp filter … ⇒ apply filters + open filter GUI - if (args.length > 1 && args[0].equalsIgnoreCase("filter")) { - Set newFilters = new HashSet<>( - Arrays.asList(Arrays.copyOfRange(args, 1, args.length)) - ); - blockPalletManager.updatePlayerFilters(player, newFilters); - new ChoosePaletteMenu(blockPalletManager, player, plugin).open(); - return true; - } - - // invalid usage ⇒ show help - sender.sendMessage("§cUsage: §7/bp\n" - + "§c or §7/bp menu\n" - + "§c or §7/bp filter\n" - + "§c or §7/bp filter …"); - return true; - } - - @Override - public List onTabComplete(@NonNull CommandSender sender, - @NonNull Command cmd, - @NonNull String alias, - @NonNull String[] args) { - List completions = new ArrayList<>(); - - if (args.length == 1) { - // only "menu" or "filter" - List top = Arrays.asList("menu", "filter"); - StringUtil.copyPartialMatches(args[0], top, completions); - } else if (args.length > 1 && args[0].equalsIgnoreCase("filter")) { - // suggest from BlockPalletMenuType.FILTER_OPTIONS - StringUtil.copyPartialMatches( - args[args.length - 1], - BlockPaletteMenuType.FILTER_OPTIONS, - completions - ); + manager.openBlockMenu(player); + return; } - Collections.sort(completions, String.CASE_INSENSITIVE_ORDER); - return completions; + sender.sendMessage("§cUsage: §7/btt blockpalette\n" + + "§c or §7/btt blockpalette menu\n" + + "§c or §7/btt blockpalette filter\n" + + "§c or §7/btt blockpalette filter …"); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java index ec2a837a..015c023c 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/NavigationModule.java @@ -18,7 +18,7 @@ import net.buildtheearth.buildteamtools.modules.navigation.components.tpll.listeners.TpllListener; import net.buildtheearth.buildteamtools.modules.navigation.components.warps.WarpsComponent; import net.buildtheearth.buildteamtools.modules.navigation.components.warps.commands.WarpCommand; -import net.buildtheearth.buildteamtools.modules.navigation.components.warps.commands.WarpsBtCommand; +import net.buildtheearth.buildteamtools.modules.navigation.components.warps.commands.BtWarpsCommand; import net.buildtheearth.buildteamtools.modules.navigation.components.warps.listeners.WarpJoinListener; import net.buildtheearth.buildteamtools.modules.network.NetworkModule; import net.buildtheearth.buildteamtools.utils.WikiLinks; @@ -162,12 +162,12 @@ private void initializeBluemapComponent(@NonNull NavigationConfig navConfig) { @Override public void registerCommands() { - registerCommand("warp", new WarpCommand()); - registerCommand("navigator", new NavigatorCommand()); - registerCommand("buildteam", new BuildteamCommand()); - registerCommand("warpsbt", new WarpsBtCommand()); - registerCommand("explore", new ExploreCommand()); - registerCommand("address", new AddressCommand()); + new WarpCommand().register(this); + new NavigatorCommand().register(this); + new BuildteamCommand().register(this); + new BtWarpsCommand().register(this); + new ExploreCommand().register(this); + new AddressCommand().register(this); } @Override diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java index 685bc9b9..4fcdd13a 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java @@ -3,6 +3,8 @@ import com.alpsbte.alpslib.utils.ChatHelper; import net.buildtheearth.Projection; import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.Module; +import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; import net.buildtheearth.buildteamtools.modules.network.api.PhotonAPI; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.buildtheearth.model.GeographicalCoordinate; @@ -12,53 +14,70 @@ import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.Location; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; -import org.jetbrains.annotations.Nullable; +import org.incendo.cloud.permission.Permission; +import org.incendo.cloud.parser.standard.StringParser; import org.jspecify.annotations.NonNull; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; -public class AddressCommand implements CommandExecutor, TabCompleter { - @Override - public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String @NonNull [] args) { - if (!(sender instanceof Player player)) { - sender.sendMessage(ChatHelper.getErrorComponent("This command can only be used by a player!")); - return true; - } - - if (args.length < 1) { - player.sendMessage(ChatHelper.getErrorComponent("Usage: /address ")); - return true; - } - - if (args[0].equalsIgnoreCase("get")) { - return handleGetCommand(player, args); - } +public class AddressCommand { + private static final String COMMAND_NAME = "address"; + private static final String PLAYER_ONLY_MESSAGE = "This command can only be used by a player!"; + + public void register(Module owner) { + var commandManager = BuildTeamTools.getInstance().getCommandManager(); + commandManager.register(owner, COMMAND_NAME, + Permission.anyOf( + Permission.of(Permissions.ADDRESS_GET), + Permission.of(Permissions.ADDRESS_TELEPORT) + ), + this::execute, + (sender, input) -> { + List options = new java.util.ArrayList<>(); + if (sender.hasPermission(Permissions.ADDRESS_GET)) options.add("get"); + if (sender.hasPermission(Permissions.ADDRESS_TELEPORT)) options.add("teleport"); + return BttCommandManager.matchingSuggestions(options, input); + }); + commandManager.registerSubcommand(owner, COMMAND_NAME, "get", + "Look up the address at your current location.", + builder -> builder.permission(Permissions.ADDRESS_GET), + context -> { + if (context.sender().getSender() instanceof Player player) + handleGetCommand(player); + else + context.sender().getSender().sendMessage( + ChatHelper.getErrorComponent(PLAYER_ONLY_MESSAGE)); + }); + commandManager.registerSubcommand(owner, COMMAND_NAME, "teleport", + "Find an address and teleport to its coordinates.", + builder -> builder.permission(Permissions.ADDRESS_TELEPORT) + .required(COMMAND_NAME, StringParser.greedyStringParser(), + org.incendo.cloud.minecraft.extras.RichDescription.of( + Component.text("Address to search for"))), + context -> { + if (context.sender().getSender() instanceof Player player) + handleTeleportCommand(player, context.get(COMMAND_NAME)); + else + context.sender().getSender().sendMessage( + ChatHelper.getErrorComponent(PLAYER_ONLY_MESSAGE)); + }); + } - if (args[0].equalsIgnoreCase("teleport")) { - return handleTeleportCommand(player, args); + private void execute(@NonNull CommandSender sender, String @NonNull [] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(ChatHelper.getErrorComponent(PLAYER_ONLY_MESSAGE)); + return; } - player.sendMessage(ChatHelper.getErrorComponent("Usage: /address ")); - return true; + player.sendMessage(ChatHelper.getErrorComponent("Usage: /btt address ")); } - private boolean handleGetCommand(@NonNull Player player, String @NonNull [] args) { + private void handleGetCommand(@NonNull Player player) { if (!player.hasPermission(Permissions.ADDRESS_GET)) { Permissions.sendNoPermissionMessage(player, Permissions.ADDRESS_GET); - return true; - } - - if (args.length > 1) { - player.sendMessage(ChatHelper.getErrorComponent("Usage: /address get")); - return true; + return; } player.sendMessage("Getting closest address..."); @@ -121,23 +140,15 @@ private boolean handleGetCommand(@NonNull Player player, String @NonNull [] args ); } - - return true; } - private boolean handleTeleportCommand(@NonNull Player player, String @NonNull [] args) { + private void handleTeleportCommand(@NonNull Player player, String address) { if (!player.hasPermission(Permissions.ADDRESS_TELEPORT)) { Permissions.sendNoPermissionMessage(player, Permissions.ADDRESS_TELEPORT); - return true; - } - - if (args.length < 2) { - player.sendMessage(ChatHelper.getErrorComponent("Usage: /address teleport
")); - return true; + return; } String lang = player.locale().getLanguage(); - String address = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); PhotonAPI.getCoordinatesFromAddressAsync(address, lang) .thenAccept(coordinates -> { @@ -160,25 +171,5 @@ private boolean handleTeleportCommand(@NonNull Player player, String @NonNull [] return null; }); - return true; - } - - @Override - public @Nullable List onTabComplete(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String @NonNull [] args) { - if (args.length != 1) { - return Collections.emptyList(); - } - List list = new ArrayList<>(); - - if (sender.hasPermission(Permissions.ADDRESS_GET)) list.add("get"); - if (sender.hasPermission(Permissions.ADDRESS_TELEPORT)) { - list.add("teleport"); - list.add("teleport "); - } - - - return list.stream() - .filter(entry -> entry.startsWith(args[0].toLowerCase())) - .toList(); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java index c3c05c5d..6a5f36ed 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/BuildteamCommand.java @@ -1,73 +1,64 @@ package net.buildtheearth.buildteamtools.modules.navigation.components.navigator.commands; import com.alpsbte.alpslib.utils.ChatHelper; +import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.Module; +import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; import net.buildtheearth.buildteamtools.modules.navigation.NavUtils; import net.buildtheearth.buildteamtools.modules.network.NetworkModule; import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; import org.bukkit.entity.Player; +import org.incendo.cloud.parser.standard.StringParser; import org.jspecify.annotations.NonNull; -import java.util.Collections; import java.util.List; import java.util.stream.Stream; -public class BuildteamCommand implements CommandExecutor, TabCompleter { +public class BuildteamCommand { - @Override - public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String @NonNull [] args) { - return btCommand(sender, label, args, Permissions.NAVIGATOR_USE); + public void register(Module owner) { + BuildTeamTools.getInstance().getCommandManager().registerRequiredArgument(owner, "buildteam", + "buildteam", "Build team name or tag", + StringParser.greedyStringParser(), + Permissions.NAVIGATOR_USE, + (sender, input) -> suggestions(input), + (sender, buildTeamName) -> btCommand(sender, buildTeamName, Permissions.NAVIGATOR_USE)); } - protected boolean btCommand(@NonNull CommandSender sender, @NonNull String label, String[] args, String permission) { + protected void btCommand(@NonNull CommandSender sender, String buildTeamName, String permission) { if (!(sender instanceof Player player)) { sender.sendMessage(ChatHelper.getErrorString("You must be a %s to %s this command!", "player", "execute")); - return true; + return; } if (!player.hasPermission(permission)) { player.sendMessage(ChatHelper.getErrorString("You don't have permission to use this command!")); - return true; + return; } - if (args.length < 1) { - player.sendMessage(ChatHelper.getErrorString("Usage: /%s ", label)); - return true; - } else { - String buildTeamName; - if (args.length > 1) buildTeamName = String.join(" ", args); - else { - buildTeamName = args[0]; - } - var teams = NetworkModule.getInstance().getBuildTeams().stream() - .filter(buildTeam -> buildTeam.getTag().equalsIgnoreCase(buildTeamName) || buildTeam.getBlankName().equalsIgnoreCase(buildTeamName)).toArray(); - if (teams.length == 0 || !(teams[0] instanceof BuildTeam bt)) { - player.sendMessage(ChatHelper.getErrorString("Build team '%s' does not exist!", buildTeamName)); - return true; - } - execute(player, bt); + BuildTeam team = NetworkModule.getInstance().getBuildTeams().stream() + .filter(buildTeam -> buildTeam.getTag().equalsIgnoreCase(buildTeamName) + || buildTeam.getBlankName().equalsIgnoreCase(buildTeamName)) + .findFirst() + .orElse(null); + if (team == null) { + player.sendMessage(ChatHelper.getErrorString("Build team '%s' does not exist!", buildTeamName)); + return; } - return true; + execute(player, team); } public void execute(Player player, BuildTeam team) { NavUtils.switchToTeam(team, player); } - @Override - public List onTabComplete(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String @NonNull [] args) { - if (args.length >= 1) { - String partial = args[0].toLowerCase(); - return NetworkModule.getInstance().getBuildTeams().stream() - .flatMap(bt -> Stream.of(bt.getTag(), bt.getBlankName())) - .filter(s -> s.toLowerCase().contains(partial)) - .toList(); - } - return Collections.emptyList(); + protected List suggestions(String input) { + List candidates = NetworkModule.getInstance().getBuildTeams().stream() + .flatMap(team -> Stream.of(team.getTag(), team.getBlankName())) + .toList(); + return BttCommandManager.matchingSuggestions(candidates, input); } -} +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/ExploreCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/ExploreCommand.java index 19dc8bd9..26affd1e 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/ExploreCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/ExploreCommand.java @@ -2,9 +2,9 @@ import com.alpsbte.alpslib.utils.ChatHelper; import net.buildtheearth.buildteamtools.modules.navigation.menu.ExploreMenu; +import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.jspecify.annotations.NonNull; @@ -13,23 +13,24 @@ * Command to open the BuildTeams explore menu. * Usage: /explore */ -public class ExploreCommand implements CommandExecutor { - @Override - public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String @NonNull [] args) { +public class ExploreCommand { + public void register(Module owner) { + BuildTeamTools.getInstance().getCommandManager().registerNoArguments( + owner, "explore", Permissions.NAVIGATOR_USE, this::execute); + } + + private void execute(@NonNull CommandSender sender) { if (!(sender instanceof Player player)) { sender.sendMessage(ChatHelper.getErrorString("You must be a %s to %s this command!", "player", "execute")); - return true; + return; } if (!player.hasPermission(Permissions.NAVIGATOR_USE)) { player.sendMessage(ChatHelper.getErrorString("You don't have permission to use this command!")); - return true; + return; } // Open the buildteams explore menu with a back reference to the main navigator new ExploreMenu(player, true); - - return true; } } - diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java index 96734291..235623f9 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/navigator/commands/NavigatorCommand.java @@ -2,35 +2,47 @@ import com.alpsbte.alpslib.utils.ChatHelper; import net.buildtheearth.buildteamtools.modules.navigation.NavigationModule; +import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.Module; import net.buildtheearth.buildteamtools.modules.navigation.menu.MainMenu; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.jspecify.annotations.NonNull; -public class NavigatorCommand implements CommandExecutor { - @Override - public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String @NonNull [] args) { +public class NavigatorCommand { + public void register(Module owner) { + var commandManager = BuildTeamTools.getInstance().getCommandManager(); + commandManager.registerNoArguments(owner, "navigator", Permissions.NAVIGATOR_USE, this::execute); + commandManager.registerSubcommand(owner, "navigator", "toggle", + "Toggle the navigator item on or off.", + builder -> builder.permission(Permissions.NAVIGATOR_USE), + context -> toggle(context.sender().getSender())); + } + + private void execute(@NonNull CommandSender sender) { if (!(sender instanceof Player player)) { sender.sendMessage(ChatHelper.getErrorString("You must be a %s to %s this command!", "player", "execute")); - return true; + return; } if (!player.hasPermission(Permissions.NAVIGATOR_USE)) { player.sendMessage(ChatHelper.getErrorString("You don't have permission to use this command!")); - return true; + return; } - if (args.length == 1 && args[0].equalsIgnoreCase("toggle")) { - // Toggle the navigator item on/off - NavigationModule.getInstance().getNavigatorComponent().toggle(player); - } else { - // Opens the navigator - new MainMenu(player); + new MainMenu(player); + } + + private void toggle(@NonNull CommandSender sender) { + if (!(sender instanceof Player player)) { + sender.sendMessage(ChatHelper.getErrorString("You must be a %s to %s this command!", "player", "execute")); + return; } - return true; + if (!player.hasPermission(Permissions.NAVIGATOR_USE)) { + player.sendMessage(ChatHelper.getErrorString("You don't have permission to use this command!")); + return; + } + NavigationModule.getInstance().getNavigatorComponent().toggle(player); } } - diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/BtWarpsCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/BtWarpsCommand.java new file mode 100644 index 00000000..fc960d46 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/BtWarpsCommand.java @@ -0,0 +1,27 @@ +package net.buildtheearth.buildteamtools.modules.navigation.components.warps.commands; + +import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.Module; +import net.buildtheearth.buildteamtools.modules.navigation.components.navigator.commands.BuildteamCommand; +import net.buildtheearth.buildteamtools.modules.navigation.components.warps.WarpsComponent; +import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam; +import net.buildtheearth.buildteamtools.modules.network.model.Permissions; +import org.bukkit.entity.Player; +import org.incendo.cloud.parser.standard.StringParser; + +public class BtWarpsCommand extends BuildteamCommand { + @Override + public void register(Module owner) { + BuildTeamTools.getInstance().getCommandManager().registerRequiredArgument(owner, "btwarps", + "buildteam", "Build team name or tag", + StringParser.greedyStringParser(), + Permissions.WARP_USE, + (sender, input) -> suggestions(input), + (sender, buildTeamName) -> btCommand(sender, buildTeamName, Permissions.WARP_USE)); + } + + @Override + public void execute(Player player, BuildTeam team) { + WarpsComponent.openWarpMenu(player, team, null); + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java index a797ebda..709dc0dd 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpCommand.java @@ -10,117 +10,118 @@ import net.buildtheearth.buildteamtools.modules.network.NetworkModule; import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; +import net.buildtheearth.buildteamtools.modules.common.commands.BttCommandManager; import net.buildtheearth.buildteamtools.utils.Utils; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; +import net.kyori.adventure.text.Component; import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; +import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.Module; import org.bukkit.entity.Player; import org.jetbrains.annotations.Nullable; import org.jspecify.annotations.NonNull; +import org.incendo.cloud.parser.standard.EnumParser; +import org.incendo.cloud.minecraft.extras.RichDescription; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; -public class WarpCommand implements CommandExecutor, TabCompleter { +public class WarpCommand { + + public void register(Module owner) { + var commandManager = BuildTeamTools.getInstance().getCommandManager(); + commandManager.register(owner, "warp", Permissions.WARP_USE, this::execute, this::suggestions); + commandManager.registerSubcommand(owner, "warp", "create", "Create a warp at your current location.", + builder -> builder.permission(Permissions.WARP_CREATE), + context -> { + CommandSender sender = context.sender().getSender(); + if (!(sender instanceof Player player)) { + sender.sendMessage(ChatHelper.getErrorComponent("This command can only be used by a player!")); + } else if (checkWarpModule(sender)) { + handleCreateCommand(player); + } + }); + commandManager.registerSubcommand(owner, "warp", "migrate", "Migrate warps from another warp provider.", + builder -> builder.permission(Permissions.WARP_MIGRATE) + .required("source", EnumParser.enumParser(WarpMigrationSource.class), + RichDescription.of(Component.text("Warp migration provider"))), + context -> { + CommandSender sender = context.sender().getSender(); + if (!(sender instanceof Player player)) { + sender.sendMessage(ChatHelper.getErrorComponent("This command can only be used by a player!")); + } else if (checkWarpModule(sender)) { + handleMigrateCommand(player, context.get("source")); + } + }); + commandManager.registerSubcommand(owner, "warp", "random", "Warp to a random build team warp.", + builder -> builder.permission(Permissions.WARP_RANDOM), + context -> { + CommandSender sender = context.sender().getSender(); + if (!(sender instanceof Player player)) { + sender.sendMessage(ChatHelper.getErrorComponent("This command can only be used by a player!")); + } else if (checkWarpModule(sender)) { + handleRandomWarpCommand(player); + } + }); + } - @Override - public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String @NonNull [] args) { + private void execute(@NonNull CommandSender sender, String @NonNull [] args) { if (!(sender instanceof Player player)) { sender.sendMessage(ChatHelper.getErrorComponent("This command can only be used by a player!")); - return true; + return; } - if (NetworkModule.getInstance().getBuildTeam() == null) { - sender.sendMessage(ChatHelper.getErrorComponent("The Warp Module is currently disabled because the Build Team " + - "failed to load!")); - return true; - } + if (!checkWarpModule(sender)) return; // If no arguments were supplied assume the player wants to open the warp menu if (args.length == 0) { if (checkForWarpUsePermissionAndMessage(player)) WarpsComponent.openWarpMenu(player); - return true; - } - - if (args[0].equalsIgnoreCase("create")) { - return handleCreateCommand(player, args); - } - - if (args[0].equalsIgnoreCase("migrate")) { - return handleMigrateCommand(player, args); + return; } - if (args[0].equalsIgnoreCase("random")) { - return handleRandomWarpCommand(player, args); - } + handleWarpTeleport(player, args); + } - return handleWarpTeleport(player, args); + private boolean checkWarpModule(CommandSender sender) { + if (NetworkModule.getInstance().getBuildTeam() != null) return true; + sender.sendMessage(ChatHelper.getErrorComponent("The Warp Module is currently disabled because the Build Team " + + "failed to load!")); + return false; } - private boolean handleCreateCommand(@NonNull Player player, String @NonNull [] args) { + private void handleCreateCommand(@NonNull Player player) { if (!player.hasPermission(Permissions.WARP_CREATE)) { player.sendMessage(ChatHelper.getErrorComponent("You don't have the required %s to %s warps.", "permission", "create")); - return true; - } - - if (args.length > 1) { - player.sendMessage(ChatHelper.getErrorComponent("Usage: /warp create")); - return true; + return; } player.sendActionBar(ChatHelper.getStandardComponent(false, "Creating the warp...")); WarpsComponent.createWarp(player); - return true; } - private boolean handleMigrateCommand(@NonNull Player player, String @NonNull [] args) { + private void handleMigrateCommand(@NonNull Player player, WarpMigrationSource source) { if (!player.hasPermission(Permissions.WARP_MIGRATE)) { player.sendMessage(ChatHelper.getErrorString("You don't have the required %s to %s warps.", "permission", "migrate")); - return true; - } - - if (args.length != 2) { - player.sendMessage(ChatHelper.getErrorComponent("Usage: /warp migrate ")); - player.sendMessage(ChatHelper.getErrorComponent("Valid sources are: %s", - Arrays.toString(WarpMigrationSource.values()))); - return true; - } - - WarpMigrationSource source = WarpMigrationSource.fromString(args[1].toLowerCase()); - if (source == null) { - player.sendMessage(ChatHelper.getErrorComponent("Invalid source: %s", args[1])); - player.sendMessage(ChatHelper.getErrorComponent("Valid sources are: %s", - Arrays.toString(WarpMigrationSource.values()))); - return true; + return; } WarpMigrator migrator = new WarpMigrator(source); player.sendMessage(ChatHelper.getStandardComponent(true, "Migrating the warps...")); migrator.migrate(player).whenComplete((result, throwable) -> handleMigrationResult(player, result, throwable)); - return true; } - private boolean handleRandomWarpCommand(@NonNull Player player, String @NonNull [] args) { + private void handleRandomWarpCommand(@NonNull Player player) { if (!player.hasPermission(Permissions.WARP_RANDOM)) { Permissions.sendNoPermissionMessage(player, Permissions.WARP_RANDOM); - return true; - } - - if (args.length > 1) { - player.sendMessage(ChatHelper.getErrorComponent("Usage: /warp random")); - return true; + return; } BuildTeam buildTeam = NetworkModule.getInstance().getBuildTeam(); if (buildTeam == null) { - return true; + return; } List warps = buildTeam.getWarpGroups().stream() @@ -130,12 +131,10 @@ private boolean handleRandomWarpCommand(@NonNull Player player, String @NonNull Warp warp = Utils.pickRandom(warps); if (warp == null) { player.sendMessage("No warp found"); - return true; + return; } NavigationModule.getInstance().getWarpsComponent().warpPlayer(player, warp); - - return true; } private void handleMigrationResult(@NonNull Player player, @NonNull MigrationResult result, @Nullable Throwable throwable) { @@ -163,20 +162,19 @@ private void handleMigrationResult(@NonNull Player player, @NonNull MigrationRes } } - private boolean handleWarpTeleport(@NonNull Player player, String @NonNull [] args) { + private void handleWarpTeleport(@NonNull Player player, String @NonNull [] args) { String key = String.join(" ", args); - if (!checkForWarpUsePermissionAndMessage(player)) return true; + if (!checkForWarpUsePermissionAndMessage(player)) return; Warp warp = NavigationModule.getInstance().getWarpsComponent().getWarpByName(key); if (warp == null) { player.sendMessage(ChatHelper.getErrorComponent("The warp with the name %s does not exist in this team!", key)); - return true; + return; } NavigationModule.getInstance().getWarpsComponent().warpPlayer(player, warp); - return true; } private static boolean checkForWarpUsePermissionAndMessage(@NonNull Player player) { @@ -187,30 +185,20 @@ private static boolean checkForWarpUsePermissionAndMessage(@NonNull Player playe return true; } - @Override - public @Nullable List onTabComplete(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String @NonNull [] args) { - if (args.length == 1) { - List list = new ArrayList<>(); - if (sender.hasPermission(Permissions.WARP_CREATE)) list.add("create"); - if (sender.hasPermission(Permissions.WARP_MIGRATE)) list.add("migrate"); - if (sender.hasPermission(Permissions.WARP_RANDOM)) list.add("random"); - - String partial = args[0].toLowerCase(); + private List suggestions(CommandSender sender, String input) { + String[] arguments = input.trim().split("\\s+"); + if (input.isBlank() || arguments.length == 1) { + List options = new ArrayList<>(); + if (sender.hasPermission(Permissions.WARP_CREATE)) options.add("create"); + if (sender.hasPermission(Permissions.WARP_MIGRATE)) options.add("migrate"); + if (sender.hasPermission(Permissions.WARP_RANDOM)) options.add("random"); BuildTeam buildTeam = NetworkModule.getInstance().getBuildTeam(); - if (buildTeam != null && buildTeam.getWarpGroups() != null) { - List warps = buildTeam.getWarpGroups().stream() - .flatMap(gr -> gr.getWarps().stream().map(Warp::getName)) - .filter(s -> s.toLowerCase().startsWith(partial)) - .toList(); - - list.addAll(warps); - } - - return list.stream() - .filter(s -> s.toLowerCase().startsWith(partial)) - .toList(); + if (buildTeam != null && buildTeam.getWarpGroups() != null) + buildTeam.getWarpGroups().stream().flatMap(group -> group.getWarps().stream()) + .map(Warp::getName).forEach(options::add); + return BttCommandManager.matchingSuggestions(options, input); } - return Collections.emptyList(); - + return List.of(); } + } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpsBtCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpsBtCommand.java deleted file mode 100644 index 4229753c..00000000 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpsBtCommand.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.buildtheearth.buildteamtools.modules.navigation.components.warps.commands; - -import net.buildtheearth.buildteamtools.modules.navigation.components.navigator.commands.BuildteamCommand; -import net.buildtheearth.buildteamtools.modules.navigation.components.warps.WarpsComponent; -import net.buildtheearth.buildteamtools.modules.network.model.BuildTeam; -import net.buildtheearth.buildteamtools.modules.network.model.Permissions; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; -import org.jspecify.annotations.NonNull; - -public class WarpsBtCommand extends BuildteamCommand implements CommandExecutor, TabCompleter { - @Override - public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String @NonNull [] args) { - return btCommand(sender, label, args, Permissions.WARP_USE); - } - - @Override - public void execute(Player player, BuildTeam team) { - WarpsComponent.openWarpMenu(player, team, null); - } -} - diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/network/model/Permissions.java b/src/main/java/net/buildtheearth/buildteamtools/modules/network/model/Permissions.java index d63fc11b..5ec17f99 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/network/model/Permissions.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/network/model/Permissions.java @@ -26,6 +26,7 @@ public class Permissions { public static final String BLOCK_PALETTE_EDIT = "btt.bp.edit"; + public static final String BLOCK_PALETTE_USE = "btt.bp.use"; public static final String NAVIGATOR_USE = "btt.navigator.use"; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/plotsystem/PlotSystemModule.java b/src/main/java/net/buildtheearth/buildteamtools/modules/plotsystem/PlotSystemModule.java index a6e526b3..d8a184a9 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/plotsystem/PlotSystemModule.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/plotsystem/PlotSystemModule.java @@ -1,7 +1,6 @@ package net.buildtheearth.buildteamtools.modules.plotsystem; import net.buildtheearth.buildteamtools.modules.Module; -import net.buildtheearth.buildteamtools.modules.plotsystem.commands.PlotSystemTerraCommand; import net.buildtheearth.buildteamtools.utils.WikiLinks; public class PlotSystemModule extends Module { @@ -116,11 +115,6 @@ public void enable() { } - @Override - public void registerCommands() { - registerCommand("plotsystemterra", new PlotSystemTerraCommand()); - } - @Override public void registerListeners() { // No Listeners diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/plotsystem/commands/PlotSystemTerraCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/plotsystem/commands/PlotSystemTerraCommand.java deleted file mode 100644 index f25a3279..00000000 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/plotsystem/commands/PlotSystemTerraCommand.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.buildtheearth.buildteamtools.modules.plotsystem.commands; - -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.jspecify.annotations.NonNull; - -public class PlotSystemTerraCommand implements CommandExecutor { - - @Override - public boolean onCommand(@NonNull CommandSender sender, @NonNull Command cmd, @NonNull String label, String @NonNull [] args) { - return false; - } -} diff --git a/src/main/java/net/buildtheearth/buildteamtools/utils/Utils.java b/src/main/java/net/buildtheearth/buildteamtools/utils/Utils.java index a4cab0e8..a246240a 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/utils/Utils.java +++ b/src/main/java/net/buildtheearth/buildteamtools/utils/Utils.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.Collectors; public class Utils { @@ -115,20 +114,4 @@ public static String toDate(long time) { return s; } - /** - * Returns a list of suggestions for the tab completer. - * If the player is already typing the argument, only the suggestions that start with the argument will be returned. - * - * @param args the args[] from the onTabComplete method - * @param parentArg the parent argument - * @param argPos the position of the argument - * @param suggestions the suggestions as a list - * @return a list of suggestions for the tab completer filtered by the player's input - */ - public static List getTabCompleterArgs(String[] args, String parentArg, int argPos, List suggestions) { - if (args.length == argPos && args[0].equalsIgnoreCase(parentArg)) - return suggestions.stream().filter(s -> s.toLowerCase().startsWith(args[argPos - 1].toLowerCase())).collect(Collectors.toList()); - return null; - } - } diff --git a/src/main/java/net/buildtheearth/buildteamtools/utils/io/MainConfig.java b/src/main/java/net/buildtheearth/buildteamtools/utils/io/MainConfig.java index 5f9b8b67..81bdb694 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/utils/io/MainConfig.java +++ b/src/main/java/net/buildtheearth/buildteamtools/utils/io/MainConfig.java @@ -1,6 +1,7 @@ package net.buildtheearth.buildteamtools.utils.io; import space.arim.dazzleconf.engine.Comments; +import space.arim.dazzleconf.engine.liaison.SubSection; import java.util.List; @@ -24,6 +25,8 @@ default String earthWorld() { } @Comments("The modules that should be disabled. E.g. \"Generator\"") + @Comments("Commands belonging to disabled or failed-to-enable modules are not registered.") + @Comments("Restart the server after changing this setting.") default List disabledModules() { return List.of(); } @@ -37,4 +40,41 @@ default boolean autoUpdate() { default boolean debug() { return false; } + + @Comments("Command settings. Changes require a server restart.") + default @SubSection Commands commands() { + return new Commands() {}; + } + + interface Commands { + @Comments("Standalone command labels (e.g. /warp). Keep a command's own name in its list,") + @Comments("otherwise it is no longer available standalone. An empty list disables the standalone command.") + @Comments("/btt is always available.") + default @SubSection Labels labels() { + return new Labels() {}; + } + } + + interface Labels { + @Comments("Extra aliases for /btt. /btt itself is always registered.") + default List btt() { return List.of("buildteamtools"); } + + default List generate() { return List.of("generate", "gen", "g"); } + default List kml() { return List.of("kml"); } + @Comments("Aliases for the KML points command.") + default List geopoints() { return List.of("geopoints"); } + @Comments("Aliases for the KML path command.") + default List geopath() { return List.of("geopath"); } + @Comments("Aliases for the KML closed-path command.") + default List georing() { return List.of("georing"); } + @Comments("Aliases for the KML filled-surface command.") + default List geosurface() { return List.of("geosurface"); } + default List navigator() { return List.of("navigator", "nav", "navigate"); } + default List buildteam() { return List.of("buildteam", "bt"); } + default List btwarps() { return List.of("btwarps", "btwarp", "wbt", "wpt", "buildteamwarps"); } + default List explore() { return List.of("explore"); } + default List address() { return List.of("address"); } + default List warp() { return List.of("warp", "warps", "wp"); } + default List blockpalette() { return List.of("blockpalette", "bp", "blocks"); } + } } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f09152b8..05429c39 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,52 +1,11 @@ name: BuildTeamTools main: net.buildtheearth.buildteamtools.BuildTeamTools -api-version: 1.13 +api-version: 1.20.6 version: ${version} description: ${description} author: MineFact, frikandelworst, George112n, ELgamer, SirJodelstein, Zoriot softdepend: [ WorldEdit, HeadDatabase, PlotSystem-Terra, BlueMap ] -commands: - buildteamtools: - description: Main command of the Build Team Tools plugin. - usage: /buildteamtools - aliases: [ btt ] - generate: - description: Allows usage of the Generator Tool. - usage: /generate - aliases: [ gen, g ] - navigator: - description: Opens the universal navigator. - usage: /navigator - aliases: [ nav, navigate ] - explore: - description: Opens the BuildTeams explore menu. - usage: /explore - warp: - description: Command for managing warps. - usage: /warp - aliases: [ warps, wp ] - warpbt: - description: Opens the warp menu for the specified BuildTeam - usage: /warpsbt - aliases: [ wbt, wpt, warpsbuildteam, warpsbt ] - kml: - description: kml pasting and parsing - usage: /kml - aliases: [ geopoints, geopath, georing, geosurface ] - address: - description: Get or teleport to an address - usage: /address [get|teleport] - - blockpalette: - description: Opens the block palette menu. - usage: /blockpalette [filter|menu|filter ...] - aliases: [ bp, blocks ] - permission: btt.bp.use - buildteam: - description: Sends the player to the specified BuildTeam - usage: /buildteam - aliases: [ bt ] permissions: btt.permpack.visitor: description: Contains all permissions for visitors / everyone @@ -115,4 +74,5 @@ permissions: btt.bp.edit: description: Allows players to create and edit custom block palettes default: op + github-repo: "BuildTheEarth/BuildTeamTools" diff --git a/src/test/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManagerTest.java b/src/test/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManagerTest.java new file mode 100644 index 00000000..9daedffa --- /dev/null +++ b/src/test/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManagerTest.java @@ -0,0 +1,366 @@ +package net.buildtheearth.buildteamtools.modules.common.commands; + +import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.buildtheearth.buildteamtools.modules.Module; +import net.buildtheearth.buildteamtools.modules.navigation.components.warps.model.WarpMigrationSource; +import net.buildtheearth.buildteamtools.modules.network.model.Permissions; +import net.buildtheearth.buildteamtools.utils.io.MainConfig; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.context.CommandInput; +import org.incendo.cloud.component.DefaultValue; +import org.incendo.cloud.exception.ArgumentParseException; +import org.incendo.cloud.exception.InvalidSyntaxException; +import org.incendo.cloud.exception.NoPermissionException; +import org.incendo.cloud.exception.NoSuchCommandException; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.internal.CommandRegistrationHandler; +import org.incendo.cloud.parser.standard.EnumParser; +import org.incendo.cloud.parser.standard.StringParser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.bukkit.command.CommandSender; + +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; + +class BttCommandManagerTest { + private TestManager manager; + private BttCommandManager commands; + private CommandSourceStack sender; + + @BeforeEach + void setUp() { + manager = new TestManager(); + commands = new BttCommandManager(manager, new MainConfig() {}, Logger.getAnonymousLogger()); + CommandSender bukkitSender = (CommandSender) Proxy.newProxyInstance(CommandSender.class.getClassLoader(), + new Class[]{CommandSender.class}, (proxy, method, args) -> { + throw new UnsupportedOperationException(method.getName()); + }); + sender = (CommandSourceStack) Proxy.newProxyInstance(CommandSourceStack.class.getClassLoader(), + new Class[]{CommandSourceStack.class}, (proxy, method, args) -> { + if (method.getName().equals("getSender")) return bukkitSender; + throw new UnsupportedOperationException(method.getName()); + }); + commands.registerRoot(null, ignored -> {}); + } + + @Test + void rootAliasesDoNotCreateDuplicateRoots() { + commands.registerRootHelp(null); + commands.registerRootSubcommand(null, "cache", "Show cache", Permissions.BUILD_TEAM_TOOLS_CACHE, + (sender, label, args) -> {}); + commands.registerRootNestedSubcommand(null, "cache", "update", "Update cache", + Permissions.BUILD_TEAM_TOOLS_CACHE, ignored -> {}); + + commands.registerEnabledCommands(); + assertEquals(1, manager.commandTree().rootNodes().size()); + assertSame(parse("btt help warp").command(), parse("buildteamtools help warp").command()); + assertSame(parse("btt cache update").command(), parse("buildteamtools cache update").command()); + } + + @Test + void rootCommandRequiresItsDeclaredPermission() { + manager.deniedPermissions = Set.of(Permissions.BUILD_TEAM_TOOLS); + + assertParseFailure(NoPermissionException.class, "btt"); + assertParseFailure(NoPermissionException.class, "buildteamtools"); + } + + @Test + void configuredDisabledModuleCommandsAreNotRegistered() { + manager = new TestManager(); + commands = new BttCommandManager(manager, new MainConfig() { + @Override + public List disabledModules() { + return List.of("Test"); + } + }, Logger.getAnonymousLogger()); + commands.registerRoot(null, ignored -> {}); + commands.register(new EnabledModule(), "warp", (sender, args) -> {}); + + assertParseFailure(NoSuchCommandException.class, "wp"); + assertParseFailure(InvalidSyntaxException.class, "btt warp"); + } + + @Test + void disabledModuleCommandsRequireTheModuleToBeEnabled() { + Module owner = new DisabledModule(); + commands.register(owner, "warp", (sender, args) -> {}); + + assertParseFailure(NoSuchCommandException.class, "wp"); + assertParseFailure(InvalidSyntaxException.class, "btt warp"); + } + + @Test + void commandsForModulesThatFailStartupAreNotRegistered() { + Module owner = new FailedStartupModule(); + commands.register(owner, "warp", (sender, args) -> {}); + owner.enable(); + + assertParseFailure(NoSuchCommandException.class, "wp"); + assertParseFailure(InvalidSyntaxException.class, "btt warp"); + } + + @Test + void commandsAreRegisteredWhenDisabledModuleIsEnabledLater() { + Module owner = new DisabledModule(); + commands.register(owner, "warp", (sender, args) -> {}); + commands.registerEnabledCommands(); + + assertParseFailure(NoSuchCommandException.class, "wp"); + + manager.finalizeRegistration(); + owner.enable(); + commands.registerEnabledCommands(owner); + + assertNotNull(parse("wp").command()); + assertNotNull(parse("btt warp").command()); + } + + @Test + void allDefaultFeatureAliasesRegisterWithoutAmbiguity() { + for (String name : List.of("generate", "kml", "geopoints", "geopath", "georing", "geosurface", + "warp", "address", "blockpalette")) { + commands.register(null, name, (sender, args) -> {}); + } + commands.registerNoArguments(null, "navigator", Permissions.NAVIGATOR_USE, ignored -> {}); + commands.registerNoArguments(null, "explore", Permissions.NAVIGATOR_USE, ignored -> {}); + for (String name : List.of("buildteam", "btwarps")) { + commands.registerRequiredArgument(null, name, "team", "Build team", + StringParser.greedyStringParser(), (sender, team) -> {}); + } + + assertSame(parse("navigator").command(), parse("nav").command()); + assertSame(parse("btt navigator").command(), parse("buildteamtools navigate").command()); + assertSame(parse("buildteam New York").command(), parse("bt New York").command()); + assertEquals("New York", parse("buildteamtools bt New York").get("team")); + assertEquals("New York", parse("btt btwarp New York").get("team")); + assertEquals("New York", parse("btwarps New York").get("team")); + assertParseFailure(NoSuchCommandException.class, "warpsbt New York"); + } + + @Test + void subcommandsRetainArgumentsPermissionsAndHandlersForEveryAlias() throws Exception { + AtomicInteger invocations = new AtomicInteger(); + Module owner = new EnabledModule(); + commands.register(null, "warp", (sender, args) -> {}); + commands.registerSubcommand(owner, "warp", "migrate", "Migrate warps", + builder -> builder.permission(Permissions.WARP_MIGRATE) + .required("source", EnumParser.enumParser(WarpMigrationSource.class)), + ignored -> invocations.incrementAndGet()); + + for (String input : List.of("warp migrate essentials", "wp migrate essentials", + "btt warp migrate essentials", "buildteamtools wp migrate essentials")) { + var context = parse(input); + assertEquals(WarpMigrationSource.ESSENTIALS, context.get("source")); + assertEquals(Permissions.WARP_MIGRATE, context.command().commandPermission().permissionString()); + context.command().commandExecutionHandler().execute(context); + } + assertEquals(4, invocations.get()); + assertParseFailure(InvalidSyntaxException.class, "wp migrate"); + assertParseFailure(ArgumentParseException.class, "btt warp migrate invalid"); + manager.deniedPermissions = Set.of(Permissions.WARP_MIGRATE); + assertParseFailure(NoPermissionException.class, "wp migrate essentials"); + assertParseFailure(NoPermissionException.class, "buildteamtools wp migrate essentials"); + } + + @Test + void optionalSubcommandArgumentsPreserveTheirDefaultsAndMultiWordInput() { + commands.register(null, "blockpalette", (sender, args) -> {}); + commands.registerSubcommand(null, "blockpalette", "filter", "Apply filters", + builder -> builder.optional("filters", StringParser.greedyStringParser(), + DefaultValue.constant("")), + ignored -> {}); + assertEquals("", parse("bp filter").get("filters")); + assertEquals("color natural", parse("buildteamtools bp filter color natural").get("filters")); + } + + @Test + void noArgumentCommandsKeepPermissionsAndRejectExtraInput() { + commands.registerNoArguments(null, "navigator", Permissions.NAVIGATOR_USE, ignored -> {}); + assertEquals(Permissions.NAVIGATOR_USE, + parse("nav").command().commandPermission().permissionString()); + assertParseFailure(InvalidSyntaxException.class, "nav unexpected"); + manager.deniedPermissions = Set.of(Permissions.NAVIGATOR_USE); + assertParseFailure(NoPermissionException.class, "buildteamtools nav"); + } + + @Test + void requiredAndGreedyCommandsKeepPermissions() { + commands.registerRequiredArgument(null, "buildteam", "team", "Build team", + StringParser.greedyStringParser(), Permissions.NAVIGATOR_USE, + (sender, input) -> List.of(), (sender, team) -> {}); + commands.register(null, "generate", Permissions.GENERATOR_USE, + (sender, args) -> {}, (sender, input) -> List.of()); + + assertParseFailure(InvalidSyntaxException.class, "bt"); + assertEquals(Permissions.GENERATOR_USE, + parse("g house -w stone").command().commandPermission().permissionString()); + manager.deniedPermissions = Set.of(Permissions.NAVIGATOR_USE, Permissions.GENERATOR_USE); + assertParseFailure(NoPermissionException.class, "btt bt New York"); + assertParseFailure(NoPermissionException.class, "buildteamtools g house -w stone"); + } + + @Test + void emptyAliasListsRegisterOnlyTheBttRootAndKeepAllFeatureRoutes() { + MainConfig.Labels noAliases = (MainConfig.Labels) Proxy.newProxyInstance( + MainConfig.Labels.class.getClassLoader(), new Class[]{MainConfig.Labels.class}, + (proxy, method, args) -> List.of()); + manager = new TestManager(); + commands = new BttCommandManager(manager, new MainConfig() { + @Override + public Commands commands() { + return new Commands() { + @Override + public Labels labels() { + return noAliases; + } + }; + } + }, Logger.getAnonymousLogger()); + commands.registerRoot(null, ignored -> {}); + commands.registerRootHelp(null); + List featureCommands = List.of("generate", "kml", "geopoints", "geopath", "georing", "geosurface", + "warp", "address", "blockpalette"); + for (String name : featureCommands) { + commands.register(null, name, (sender, args) -> {}); + } + List noArgumentCommands = List.of("navigator", "explore"); + for (String name : noArgumentCommands) { + commands.registerNoArguments(null, name, ignored -> {}); + } + List argumentCommands = List.of("buildteam", "btwarps"); + for (String name : argumentCommands) { + commands.registerRequiredArgument(null, name, "team", "Build team", + StringParser.greedyStringParser(), (sender, team) -> {}); + } + commands.registerSubcommand(null, "warp", "migrate", "Migrate warps", + builder -> builder.required("source", EnumParser.enumParser(WarpMigrationSource.class)), + ignored -> {}); + + for (String name : featureCommands) { + parse("btt " + name); + assertParseFailure(NoSuchCommandException.class, name); + } + for (String name : noArgumentCommands) { + parse("btt " + name); + assertParseFailure(NoSuchCommandException.class, name); + } + for (String name : argumentCommands) { + assertEquals("New York", parse("btt " + name + " New York").get("team")); + assertParseFailure(NoSuchCommandException.class, name + " New York"); + } + assertEquals(WarpMigrationSource.ESSENTIALS, parse("btt warp migrate essentials").get("source")); + assertEquals(1, manager.commandTree().rootNodes().size()); + assertParseFailure(NoSuchCommandException.class, "buildteamtools help"); + assertParseFailure(NoSuchCommandException.class, "wp migrate essentials"); + } + + @Test + void configuredStandaloneLabelsDoNotImplicitlyIncludeTheCanonicalName() { + manager = new TestManager(); + commands = new BttCommandManager(manager, new MainConfig() { + @Override + public Commands commands() { + return new Commands() { + @Override + public Labels labels() { + return new Labels() { + @Override + public List warp() { + return List.of("travel"); + } + }; + } + }; + } + }, Logger.getAnonymousLogger()); + commands.registerRoot(null, ignored -> {}); + commands.register(null, "warp", (sender, args) -> {}); + parse("travel New York"); + parse("btt warp New York"); + parse("btt travel New York"); + assertParseFailure(NoSuchCommandException.class, "warp New York"); + assertParseFailure(NoSuchCommandException.class, "wp New York"); + } + + private CommandContext parse(String input) { + commands.registerEnabledCommands(); + var context = new CommandContext<>(sender, manager); + Command command = manager.commandTree() + .parse(context, CommandInput.of(input), Runnable::run).join(); + context.command(command); + return context; + } + + private void assertParseFailure(Class expected, String input) { + CompletionException exception = assertThrows(CompletionException.class, () -> parse(input)); + assertInstanceOf(expected, exception.getCause()); + } + + private static final class TestManager extends CommandManager { + private Set deniedPermissions = Set.of(); + + private TestManager() { + super(ExecutionCoordinator.simpleCoordinator(), + CommandRegistrationHandler.nullCommandRegistrationHandler()); + } + + @Override + public boolean hasPermission(CommandSourceStack sender, String permission) { + return !deniedPermissions.contains(permission); + } + + private void finalizeRegistration() { + super.lockRegistration(); + } + } + + private static final class EnabledModule extends Module { + private EnabledModule() { + super("Test", ""); + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public void registerListeners() { + } + } + + private static final class DisabledModule extends Module { + private DisabledModule() { + super("Test", ""); + } + + @Override + public void registerListeners() { + } + } + + private static final class FailedStartupModule extends Module { + private FailedStartupModule() { + super("Test", ""); + } + + @Override + public void enable() { + shutdown("Test module startup failed."); + } + + @Override + public void registerListeners() { + } + } +}