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