From b4a2d2998c2a5645ddc59bbf52848334700db2db Mon Sep 17 00:00:00 2001 From: Zoriot Date: Sun, 4 Oct 2026 21:03:46 +0200 Subject: [PATCH 1/6] feat(commands)!: migrate to modern Cloud command trees Replace Bukkit command executors, plugin.yml command definitions and tab-completer infrastructure with a shared Cloud command manager. Remove obsolete command listeners, completion helpers and inert command registrations while retaining permission metadata and module-enabled guards. Use the modern PaperCommandManager with native Brigadier integration and require Paper 1.20.6 or newer. Add cloud-minecraft-extras, MinecraftHelp at /btt help, and rich command descriptions. Register administrative, generator, navigation, address, warp, palette and KML actions as explicit Cloud subcommands. Parse warp migration sources as enums and preserve free-form geographic payloads and multi-word names. Share canonical command trees with configured aliases to avoid ambiguous root registration. Respect immutable Cloud builders so handlers, arguments and permissions are retained. Apply the same command shapes and permission checks to standalone labels and /btt routes. Make every standalone label configurable, including canonical names such as warp. Empty alias lists disable standalone registration without removing /btt feature routes. Update internal generator links, copied commands and KML command blocks to use always-available root routes. Rename warpsbt to btwarps and reverse the corresponding aliases to btwarp and buildteamwarps. Document configuration migration, root-only registration and the new minimum Paper version. Add command-tree regression coverage for default aliases, root-only configuration, custom standalone labels, nested routes, typed arguments, defaults, permissions, handler execution and invalid input. BREAKING CHANGE: Paper 1.20.6+ is required. Standalone labels must be explicitly listed in commands.aliases; empty lists now expose features only under /btt. Rename the warpsbt configuration key to btwarps and add canonical standalone labels to existing lists where desired. --- README.md | 15 +- build.gradle.kts | 3 + gradle/libs.versions.toml | 7 + .../buildteamtools/BuildTeamTools.java | 4 + .../buildteamtools/modules/Module.java | 54 --- .../modules/common/CommonModule.java | 5 +- .../common/commands/BttCommandManager.java | 458 ++++++++++++++++++ .../commands/BuildTeamToolsCommand.java | 150 +++--- .../common/listeners/CommandListener.java | 42 -- .../modules/generator/GeneratorModule.java | 5 +- .../generator/commands/GeneratorCommand.java | 217 +++++---- .../generator/components/kml/KmlCommand.java | 130 +++-- .../components/kml/KmlTabCompleter.java | 70 --- .../generator/model/GeneratorComponent.java | 4 +- .../modules/generator/model/History.java | 12 +- .../modules/miscellaneous/MiscModule.java | 64 +-- .../blockpalettegui/BlockPaletteCommand.java | 139 +++--- .../modules/navigation/NavigationModule.java | 14 +- .../address/commands/AddressCommand.java | 113 ++--- .../navigator/commands/BuildteamCommand.java | 69 ++- .../navigator/commands/ExploreCommand.java | 21 +- .../navigator/commands/NavigatorCommand.java | 42 +- .../warps/commands/BtWarpsCommand.java | 27 ++ .../warps/commands/WarpCommand.java | 170 +++---- .../warps/commands/WarpsBtCommand.java | 25 - .../modules/network/model/Permissions.java | 1 + .../modules/plotsystem/PlotSystemModule.java | 6 - .../commands/PlotSystemTerraCommand.java | 14 - .../buildteamtools/utils/Utils.java | 17 - .../buildteamtools/utils/io/MainConfig.java | 37 ++ src/main/resources/plugin.yml | 44 +- .../commands/BttCommandManagerTest.java | 266 ++++++++++ 32 files changed, 1405 insertions(+), 840 deletions(-) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java delete mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/common/listeners/CommandListener.java delete mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/kml/KmlTabCompleter.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/BtWarpsCommand.java delete mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/warps/commands/WarpsBtCommand.java delete mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/plotsystem/commands/PlotSystemTerraCommand.java create mode 100644 src/test/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManagerTest.java diff --git a/README.md b/README.md index aea96de7..813200fc 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,17 @@ outdated.* **For more help, please see the [wiki](https://resources.buildtheearth.net/s/btt).** +Use `/btt help` for in-game command help. Feature commands and their subcommands +also work under `/btt`, for example `/btt warp migrate essentials`. Configured +aliases share the same command tree and permissions; alias changes require a restart. +Every standalone label, including `/warp`, must be listed in `commands.aliases`. +Set a feature's list to `[]` to expose it only under `/btt`; clear all lists +(including `btt`) to register only the `/btt` root. Default lists include the +canonical standalone labels. Existing configurations must add those labels +explicitly if they should remain available. The build-team warp command is +`/btt btwarps`, with default standalone labels `btwarps`, `btwarp`, `wbt`, +`wpt`, and `buildteamwarps`; rename the old `warpsbt` config key to `btwarps`. + ## 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..1c37ec8d 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( diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/Module.java b/src/main/java/net/buildtheearth/buildteamtools/modules/Module.java index adc437b2..53fbd5cc 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,7 +63,6 @@ protected Module(String moduleName, String wikiPage, Module... dependsOnModules) public void enable() { checkForModuleDependencies(); - loadCommands(); loadListeners(); enabled = true; @@ -113,48 +101,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..a952a589 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java @@ -0,0 +1,458 @@ +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.execution.ExecutionCoordinator; +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.minecraft.extras.MinecraftHelp; +import org.incendo.cloud.minecraft.extras.RichDescription; +import org.incendo.cloud.paper.PaperCommandManager; +import org.incendo.cloud.permission.Permission; +import org.incendo.cloud.parser.ParserDescriptor; +import org.incendo.cloud.parser.standard.StringParser; +import org.incendo.cloud.suggestion.SuggestionProvider; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +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", "reload", "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<>(); + + 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; + 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) { + List aliases = validatedAliases( + "btt", + config.commands().aliases().btt(), + true + ); + aliasesByCommand.put("btt", aliases); + 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 + ) { + 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 + ) { + RichDescription parentDescription = description(rootSubcommandDescription(parent)); + 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) { + 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); + } + + 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); + } + + private void registerAliased( + Module owner, + String name, + List aliases, + CommandHandler handler, + SuggestionHandler suggestions, + String permission + ) { + List validAliases = validatedAliases(name, aliases, false); + aliasesByCommand.put(name, validAliases); + 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 + ) { + List aliases = validatedAliases(name, configuredAliases(name), false); + aliasesByCommand.put(name, aliases); + 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, + BiFunction> suggestions, + BiConsumer handler + ) { + registerRequiredArgument(owner, name, argumentName, argumentDescription, parser, + null, suggestions, handler); + } + + public void registerRequiredArgument( + Module owner, + String name, + String argumentName, + String argumentDescription, + ParserDescriptor parser, + String permission, + BiFunction> suggestions, + BiConsumer handler + ) { + List aliases = validatedAliases(name, configuredAliases(name), false); + aliasesByCommand.put(name, aliases); + 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 + ) { + RichDescription richDescription = description(description); + 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 Command.Builder rootBuilder() { + return manager.commandBuilder("btt", aliasesByCommand.getOrDefault("btt", List.of()).toArray(String[]::new)); + } + + private List> featureBuilders(String name) { + List aliases = aliasesByCommand.get(name); + if (aliases == null) { + throw new IllegalStateException("Register command '" + name + "' before its subcommands."); + } + List> builders = new ArrayList<>(); + if (!aliases.isEmpty()) { + builders.add(manager.commandBuilder(aliases.getFirst(), + aliases.subList(1, aliases.size()).toArray(String[]::new))); + } + String[] subcommandAliases = aliases.stream().filter(alias -> !alias.equals(name)).toArray(String[]::new); + builders.add(rootBuilder().literal(name, descriptionFor(name), subcommandAliases)); + return builders; + } + + private List configuredAliases(String name) { + MainConfig.Aliases aliases = config.commands().aliases(); + return switch (name) { + case "generate" -> aliases.generate(); + case "kml" -> aliases.kml(); + case "geopoints" -> aliases.geopoints(); + case "geopath" -> aliases.geopath(); + case "georing" -> aliases.georing(); + case "geosurface" -> aliases.geosurface(); + case "navigator" -> aliases.navigator(); + case "buildteam" -> aliases.buildteam(); + case "btwarps" -> aliases.btwarps(); + case "explore" -> aliases.explore(); + case "address" -> aliases.address(); + case "warp" -> aliases.warp(); + case "blockpalette" -> aliases.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); + } +} 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..a0d57905 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,111 @@ 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, "reload", + "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 +150,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..1636e2e9 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().aliases(); + 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..0a96634c 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,64 @@ 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.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, 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 +134,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 +165,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..a2d5f16f 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", 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..3d48c5dd 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; @@ -37,4 +38,40 @@ default boolean autoUpdate() { default boolean debug() { return false; } + + @Comments("Command names and aliases. Changes require a server restart.") + default @SubSection Commands commands() { + return new Commands() {}; + } + + interface Commands { + @Comments("Standalone command labels. An empty list disables standalone registration for that feature.") + @Comments("Canonical /btt subcommands are always registered. Changes require a restart.") + default @SubSection Aliases aliases() { + return new Aliases() {}; + } + } + + interface Aliases { + @Comments("Aliases are registered at server startup. Restart the server after changing them.") + 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..614c8760 --- /dev/null +++ b/src/test/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManagerTest.java @@ -0,0 +1,266 @@ +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 -> {}); + + 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 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.Aliases noAliases = (MainConfig.Aliases) Proxy.newProxyInstance( + MainConfig.Aliases.class.getClassLoader(), new Class[]{MainConfig.Aliases.class}, + (proxy, method, args) -> List.of()); + manager = new TestManager(); + commands = new BttCommandManager(manager, new MainConfig() { + @Override + public Commands commands() { + return new Commands() { + @Override + public Aliases aliases() { + return noAliases; + } + }; + } + }, Logger.getAnonymousLogger()); + commands.registerRoot(null, ignored -> {}); + commands.registerRootHelp(null); + for (String name : List.of("generate", "kml", "geopoints", "geopath", "georing", "geosurface", + "warp", "address", "blockpalette")) { + commands.register(null, name, (sender, args) -> {}); + parse("btt " + name); + assertParseFailure(NoSuchCommandException.class, name); + } + for (String name : List.of("navigator", "explore")) { + commands.registerNoArguments(null, name, ignored -> {}); + parse("btt " + name); + assertParseFailure(NoSuchCommandException.class, name); + } + for (String name : List.of("buildteam", "btwarps")) { + commands.registerRequiredArgument(null, name, "team", "Build team", + StringParser.greedyStringParser(), (sender, team) -> {}); + assertEquals("New York", parse("btt " + name + " New York").get("team")); + assertParseFailure(NoSuchCommandException.class, name + " New York"); + } + commands.registerSubcommand(null, "warp", "migrate", "Migrate warps", + builder -> builder.required("source", EnumParser.enumParser(WarpMigrationSource.class)), + ignored -> {}); + 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 Aliases aliases() { + return new Aliases() { + @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) { + 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 static final class EnabledModule extends Module { + private EnabledModule() { + super("Test", ""); + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public void registerListeners() { + } + } +} From 2ab826263d517228e39618b7cd451e1ecf685692 Mon Sep 17 00:00:00 2001 From: Zoriot Date: Sun, 11 Oct 2026 01:18:57 +0200 Subject: [PATCH 2/6] docs: simplify README command and configuration section Replace outdated command and alias details with links to maintained documentation pages for commands, permissions, and configuration. --- README.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 813200fc..167f8beb 100644 --- a/README.md +++ b/README.md @@ -102,16 +102,10 @@ outdated.* **For more help, please see the [wiki](https://resources.buildtheearth.net/s/btt).** -Use `/btt help` for in-game command help. Feature commands and their subcommands -also work under `/btt`, for example `/btt warp migrate essentials`. Configured -aliases share the same command tree and permissions; alias changes require a restart. -Every standalone label, including `/warp`, must be listed in `commands.aliases`. -Set a feature's list to `[]` to expose it only under `/btt`; clear all lists -(including `btt`) to register only the `/btt` root. Default lists include the -canonical standalone labels. Existing configurations must add those labels -explicitly if they should remain available. The build-team warp command is -`/btt btwarps`, with default standalone labels `btwarps`, `btwarp`, `wbt`, -`wpt`, and `buildteamwarps`; rename the old `warpsbt` config key to `btwarps`. +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 From 7add017a881053c4b60f4c2cb3528b3a16803905 Mon Sep 17 00:00:00 2001 From: Zoriot Date: Sun, 11 Oct 2026 21:54:20 +0200 Subject: [PATCH 3/6] fix(commands): register commands only for enabled modules Commands of modules that are disabled in the config or failed to start (for example Navigation and Stats when the API key is not configured) were still registered, and therefore visible in help and tab completion. - Defer registration until module state is known: registrations are queued per owner module and flushed by registerEnabledCommands() after ModuleHandler.enableAll() in BuildTeamTools.onEnable. - Register late when a module is enabled afterwards (for example via NetworkModule.enableDisabledModules() after /btt reload-config or /btt cache update). Module.enable() flushes that module's queue and ManagerSetting.ALLOW_UNSAFE_REGISTRATION is enabled for this. - Skip registration entirely for modules listed in disabledModules. - Add Permission-typed register/registerAliased overloads and give the /warp and /address roots the permissions their handlers enforce, so help only lists commands the sender can actually execute. - Update the MainConfig comments accordingly. - Add regression tests for root permissions, config-disabled modules, failed-startup modules and late enabling. --- .../buildteamtools/BuildTeamTools.java | 1 + .../buildteamtools/modules/Module.java | 3 + .../common/commands/BttCommandManager.java | 247 ++++++++++++------ .../address/commands/AddressCommand.java | 8 +- .../warps/commands/WarpCommand.java | 2 +- .../buildteamtools/utils/io/MainConfig.java | 4 +- .../commands/BttCommandManagerTest.java | 120 ++++++++- 7 files changed, 296 insertions(+), 89 deletions(-) diff --git a/src/main/java/net/buildtheearth/buildteamtools/BuildTeamTools.java b/src/main/java/net/buildtheearth/buildteamtools/BuildTeamTools.java index 1c37ec8d..e9314e42 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/BuildTeamTools.java +++ b/src/main/java/net/buildtheearth/buildteamtools/BuildTeamTools.java @@ -69,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 53fbd5cc..5993d5a5 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/Module.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/Module.java @@ -66,6 +66,9 @@ public void enable() { loadListeners(); enabled = true; + BuildTeamTools plugin = BuildTeamTools.getInstance(); + if (plugin != null && plugin.getCommandManager() != null) + plugin.getCommandManager().registerEnabledCommands(this); } /** 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 index a952a589..5660c662 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java @@ -23,6 +23,7 @@ import org.incendo.cloud.permission.Permission; import org.incendo.cloud.parser.ParserDescriptor; import org.incendo.cloud.parser.standard.StringParser; +import org.incendo.cloud.setting.ManagerSetting; import org.incendo.cloud.suggestion.SuggestionProvider; import java.util.ArrayList; @@ -55,6 +56,7 @@ public final class BttCommandManager { 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() @@ -66,6 +68,7 @@ public BttCommandManager(BuildTeamTools plugin) { 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 -> @@ -83,22 +86,26 @@ public BttCommandManager(BuildTeamTools plugin) { } public void registerRoot(Module owner, Consumer handler) { + if (isConfiguredDisabled(owner)) return; + List aliases = validatedAliases( "btt", config.commands().aliases().btt(), true ); aliasesByCommand.put("btt", aliases); - 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; - } + 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( @@ -108,15 +115,17 @@ public void registerRootSubcommand( String permission, CommandHandler handler ) { - 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])))); + 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( @@ -127,27 +136,31 @@ public void registerRootNestedSubcommand( String permission, Consumer> handler ) { + if (isConfiguredDisabled(owner)) return; + RichDescription parentDescription = description(rootSubcommandDescription(parent)); - 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)))); + 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) { - 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())))); + 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) { @@ -161,6 +174,19 @@ public void register( 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), @@ -200,36 +226,51 @@ public void registerAliased( CommandHandler handler, SuggestionHandler suggestions ) { - registerAliased(owner, name, aliases, handler, suggestions, null); + registerAliased(owner, name, aliases, handler, suggestions, (Permission) 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 registerAliased( + private void registerAliasedInternal( Module owner, String name, List aliases, CommandHandler handler, SuggestionHandler suggestions, - String permission + Permission permission ) { + if (isConfiguredDisabled(owner)) return; + List validAliases = validatedAliases(name, aliases, false); aliasesByCommand.put(name, validAliases); - 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("")))))); + 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; } - } catch (RuntimeException exception) { - logger.log(Level.SEVERE, "Failed to register command '" + name + "'.", exception); - throw exception; - } + }); } public void registerNoArguments(Module owner, String name, Consumer handler) { @@ -242,14 +283,18 @@ public void registerNoArguments( String permission, Consumer handler ) { + if (isConfiguredDisabled(owner)) return; + List aliases = validatedAliases(name, configuredAliases(name), false); aliasesByCommand.put(name, aliases); - 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())))); - } + 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( @@ -287,16 +332,20 @@ public void registerRequiredArgument( BiFunction> suggestions, BiConsumer handler ) { + if (isConfiguredDisabled(owner)) return; + List aliases = validatedAliases(name, configuredAliases(name), false); aliasesByCommand.put(name, aliases); - 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))))); - } + 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( @@ -307,14 +356,57 @@ public void registerSubcommand( UnaryOperator> argumentBuilder, Consumer> handler ) { + if (isConfiguredDisabled(owner)) return; + RichDescription richDescription = description(description); - 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)))); + 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)); } } @@ -455,4 +547,7 @@ public interface CommandHandler { 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/navigation/components/address/commands/AddressCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/navigation/components/address/commands/AddressCommand.java index 0a96634c..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 @@ -16,6 +16,7 @@ import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.incendo.cloud.permission.Permission; import org.incendo.cloud.parser.standard.StringParser; import org.jspecify.annotations.NonNull; @@ -27,7 +28,12 @@ public class AddressCommand { public void register(Module owner) { var commandManager = BuildTeamTools.getInstance().getCommandManager(); - commandManager.register(owner, COMMAND_NAME, this::execute, + 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"); 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 a2d5f16f..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 @@ -29,7 +29,7 @@ public class WarpCommand { public void register(Module owner) { var commandManager = BuildTeamTools.getInstance().getCommandManager(); - commandManager.register(owner, "warp", this::execute, this::suggestions); + 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 -> { 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 3d48c5dd..e10461e0 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/utils/io/MainConfig.java +++ b/src/main/java/net/buildtheearth/buildteamtools/utils/io/MainConfig.java @@ -25,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(); } @@ -46,7 +48,7 @@ default boolean debug() { interface Commands { @Comments("Standalone command labels. An empty list disables standalone registration for that feature.") - @Comments("Canonical /btt subcommands are always registered. Changes require a restart.") + @Comments("Canonical /btt subcommands are registered for enabled modules. Changes require a restart.") default @SubSection Aliases aliases() { return new Aliases() {}; } 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 index 614c8760..56a2307c 100644 --- a/src/test/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManagerTest.java +++ b/src/test/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManagerTest.java @@ -60,11 +60,71 @@ void rootAliasesDoNotCreateDuplicateRoots() { 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", @@ -168,26 +228,36 @@ public Aliases aliases() { }, Logger.getAnonymousLogger()); commands.registerRoot(null, ignored -> {}); commands.registerRootHelp(null); - for (String name : List.of("generate", "kml", "geopoints", "geopath", "georing", "geosurface", - "warp", "address", "blockpalette")) { + List featureCommands = List.of("generate", "kml", "geopoints", "geopath", "georing", "geosurface", + "warp", "address", "blockpalette"); + for (String name : featureCommands) { commands.register(null, name, (sender, args) -> {}); - parse("btt " + name); - assertParseFailure(NoSuchCommandException.class, name); } - for (String name : List.of("navigator", "explore")) { + List noArgumentCommands = List.of("navigator", "explore"); + for (String name : noArgumentCommands) { commands.registerNoArguments(null, name, ignored -> {}); - parse("btt " + name); - assertParseFailure(NoSuchCommandException.class, name); } - for (String name : List.of("buildteam", "btwarps")) { + List argumentCommands = List.of("buildteam", "btwarps"); + for (String name : argumentCommands) { commands.registerRequiredArgument(null, name, "team", "Build team", StringParser.greedyStringParser(), (sender, team) -> {}); - assertEquals("New York", parse("btt " + name + " New York").get("team")); - assertParseFailure(NoSuchCommandException.class, name + " New York"); } 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"); @@ -223,6 +293,7 @@ public List warp() { } 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(); @@ -247,6 +318,10 @@ private TestManager() { public boolean hasPermission(CommandSourceStack sender, String permission) { return !deniedPermissions.contains(permission); } + + private void finalizeRegistration() { + super.lockRegistration(); + } } private static final class EnabledModule extends Module { @@ -263,4 +338,29 @@ public boolean isEnabled() { 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() { + } + } } From 568437f9a7262737e7e846933c70d0d97743ae3a Mon Sep 17 00:00:00 2001 From: Zoriot Date: Sun, 11 Oct 2026 21:54:26 +0200 Subject: [PATCH 4/6] refactor(commands)!: remove duplicate /btt reload command /btt reload and /btt reload-config ran the same handler. Keep only reload-config, which is the more descriptive name, and drop "reload" from the reserved command names. BREAKING CHANGE: /btt reload no longer exists, use /btt reload-config. --- .../modules/common/commands/BttCommandManager.java | 2 +- .../modules/common/commands/BuildTeamToolsCommand.java | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) 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 index 5660c662..6f77ef6d 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java @@ -45,7 +45,7 @@ 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", "reload", "update" + "help", "communicators", "cache", "debug", "checkforupdates", "reload-config", "update" ); private static final ParserDescriptor ARGUMENT_PARSER = StringParser.greedyStringParser(); 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 a0d57905..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 @@ -56,10 +56,6 @@ public void register(Module owner) { "Reload module configuration files.", Permissions.BUILD_TEAM_TOOLS_RELOAD, (sender, label, args) -> reloadCommand(sender)); - commandManager.registerRootSubcommand(owner, "reload", - "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, From d37791381ad32c80f4e6263920f477149d746170 Mon Sep 17 00:00:00 2001 From: Zoriot Date: Sun, 11 Oct 2026 21:54:27 +0200 Subject: [PATCH 5/6] refactor(config)!: rename commands.aliases to commands.labels The configured lists are the full set of standalone command labels (the first entry is the primary label), not only extra aliases. Removing a command's own name from its list removes the standalone command, which the old name did not make clear. - Rename MainConfig.Aliases to Labels and commands().aliases() to labels(); update BttCommandManager, KmlCommand and the tests. - Shorten the config comments and state that a command's own name must stay in its list, that an empty list disables the standalone command and that /btt is always available. - Rename the local variable in featureBuilders and add a short comment. BREAKING CHANGE: the commands.aliases section in config.yml is now commands.labels. Existing customized lists must be moved to the new key, otherwise the defaults are used. --- .../common/commands/BttCommandManager.java | 43 ++++++++++--------- .../generator/components/kml/KmlCommand.java | 2 +- .../buildteamtools/utils/io/MainConfig.java | 15 ++++--- .../commands/BttCommandManagerTest.java | 10 ++--- 4 files changed, 36 insertions(+), 34 deletions(-) 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 index 6f77ef6d..dac9c1a9 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java @@ -90,7 +90,7 @@ public void registerRoot(Module owner, Consumer handler) { List aliases = validatedAliases( "btt", - config.commands().aliases().btt(), + config.commands().labels().btt(), true ); aliasesByCommand.put("btt", aliases); @@ -415,36 +415,37 @@ private Command.Builder rootBuilder() { } private List> featureBuilders(String name) { - List aliases = aliasesByCommand.get(name); - if (aliases == null) { + List labels = aliasesByCommand.get(name); + if (labels == null) { throw new IllegalStateException("Register command '" + name + "' before its subcommands."); } List> builders = new ArrayList<>(); - if (!aliases.isEmpty()) { - builders.add(manager.commandBuilder(aliases.getFirst(), - aliases.subList(1, aliases.size()).toArray(String[]::new))); + // 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 = aliases.stream().filter(alias -> !alias.equals(name)).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.Aliases aliases = config.commands().aliases(); + MainConfig.Labels labels = config.commands().labels(); return switch (name) { - case "generate" -> aliases.generate(); - case "kml" -> aliases.kml(); - case "geopoints" -> aliases.geopoints(); - case "geopath" -> aliases.geopath(); - case "georing" -> aliases.georing(); - case "geosurface" -> aliases.geosurface(); - case "navigator" -> aliases.navigator(); - case "buildteam" -> aliases.buildteam(); - case "btwarps" -> aliases.btwarps(); - case "explore" -> aliases.explore(); - case "address" -> aliases.address(); - case "warp" -> aliases.warp(); - case "blockpalette" -> aliases.blockpalette(); + 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(); }; } 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 1636e2e9..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 @@ -58,7 +58,7 @@ public class KmlCommand { "ยง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().aliases(); + 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()); 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 e10461e0..81bdb694 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/utils/io/MainConfig.java +++ b/src/main/java/net/buildtheearth/buildteamtools/utils/io/MainConfig.java @@ -41,21 +41,22 @@ default boolean debug() { return false; } - @Comments("Command names and aliases. Changes require a server restart.") + @Comments("Command settings. Changes require a server restart.") default @SubSection Commands commands() { return new Commands() {}; } interface Commands { - @Comments("Standalone command labels. An empty list disables standalone registration for that feature.") - @Comments("Canonical /btt subcommands are registered for enabled modules. Changes require a restart.") - default @SubSection Aliases aliases() { - return new Aliases() {}; + @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 Aliases { - @Comments("Aliases are registered at server startup. Restart the server after changing them.") + 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"); } 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 index 56a2307c..9daedffa 100644 --- a/src/test/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManagerTest.java +++ b/src/test/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManagerTest.java @@ -211,8 +211,8 @@ void requiredAndGreedyCommandsKeepPermissions() { @Test void emptyAliasListsRegisterOnlyTheBttRootAndKeepAllFeatureRoutes() { - MainConfig.Aliases noAliases = (MainConfig.Aliases) Proxy.newProxyInstance( - MainConfig.Aliases.class.getClassLoader(), new Class[]{MainConfig.Aliases.class}, + 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() { @@ -220,7 +220,7 @@ void emptyAliasListsRegisterOnlyTheBttRootAndKeepAllFeatureRoutes() { public Commands commands() { return new Commands() { @Override - public Aliases aliases() { + public Labels labels() { return noAliases; } }; @@ -272,8 +272,8 @@ void configuredStandaloneLabelsDoNotImplicitlyIncludeTheCanonicalName() { public Commands commands() { return new Commands() { @Override - public Aliases aliases() { - return new Aliases() { + public Labels labels() { + return new Labels() { @Override public List warp() { return List.of("travel"); From c92587652c8268c60cf3af5e3658ccdfe9ec7fcf Mon Sep 17 00:00:00 2001 From: Zoriot Date: Sun, 11 Oct 2026 22:05:09 +0200 Subject: [PATCH 6/6] refactor: Remove unnecessary cast + code --- .../common/commands/BttCommandManager.java | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) 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 index dac9c1a9..c44a4f32 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/common/commands/BttCommandManager.java @@ -12,27 +12,21 @@ import org.incendo.cloud.CommandManager; import org.incendo.cloud.component.DefaultValue; import org.incendo.cloud.context.CommandContext; -import org.incendo.cloud.execution.ExecutionCoordinator; 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.permission.Permission; 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.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -226,7 +220,7 @@ public void registerAliased( CommandHandler handler, SuggestionHandler suggestions ) { - registerAliased(owner, name, aliases, handler, suggestions, (Permission) null); + registerAliased(owner, name, aliases, handler, suggestions, null); } public void registerAliased( @@ -309,19 +303,6 @@ public void registerRequiredArgument( null, (sender, input) -> List.of(), handler); } - public void registerRequiredArgument( - Module owner, - String name, - String argumentName, - String argumentDescription, - ParserDescriptor parser, - BiFunction> suggestions, - BiConsumer handler - ) { - registerRequiredArgument(owner, name, argumentName, argumentDescription, parser, - null, suggestions, handler); - } - public void registerRequiredArgument( Module owner, String name,