) result;
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to invoke tab completer", e);
+ }
+ };
+ }
+
+ private void invokeMethod(Object handler, Method method, S sender, Arguments args) {
+ try {
+ Parameter[] params = method.getParameters();
+ Object[] invokeArgs = new Object[params.length];
+
+ for (int i = 0; i < params.length; i++) {
+ Parameter param = params[i];
+ Class> paramType = param.getType();
+ boolean isOptional = paramType == Optional.class;
+
+ // First param: sender
+ if (i == 0) {
+ Class> senderType = isOptional ? extractOptionalType(param) : paramType;
+ if (senderResolver.canResolve(senderType)) {
+ Object resolved = senderResolver.resolve(sender, senderType);
+ invokeArgs[i] = isOptional ? Optional.ofNullable(resolved) : resolved;
+ continue;
+ }
+ }
+
+ // Other params: @Arg
+ Arg argAnnotation = param.getAnnotation(Arg.class);
+ if (argAnnotation != null) {
+ String argName = argAnnotation.value();
+
+ if (isOptional) {
+ invokeArgs[i] = args.getOptional(argName);
+ } else {
+ invokeArgs[i] = args.get(argName);
+ }
+ }
+ }
+
+ method.invoke(handler, invokeArgs);
+
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to invoke command method: " + method.getName(), e);
+ }
+ }
+
+ private record CommandMethodInfo(Object handler, Method method, String name) {}
+ private record TabCompleterMethod(Object handler, Method method) {}
+}
\ No newline at end of file
diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Arg.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Arg.java
new file mode 100644
index 0000000..011a52e
--- /dev/null
+++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Arg.java
@@ -0,0 +1,39 @@
+package fr.traqueur.commands.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a method parameter as a command argument.
+ *
+ * The argument type is inferred from the parameter type. The type must
+ * have a registered converter in the CommandManager.
+ *
+ * Example:
+ * {@code
+ * @Command(name = "give")
+ * public void give(Player sender,
+ * @Arg("player") Player target,
+ * @Arg("item") Material item,
+ * @Arg("amount") @Optional Integer amount) {
+ * int qty = (amount != null) ? amount : 1;
+ * target.getInventory().addItem(new ItemStack(item, qty));
+ * }
+ * }
+ *
+ * @since 5.0.0
+ * @see Infinite
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface Arg {
+
+ /**
+ * The argument name used for parsing and tab completion.
+ *
+ * @return the argument name
+ */
+ String value();
+}
\ No newline at end of file
diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Command.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Command.java
new file mode 100644
index 0000000..97be715
--- /dev/null
+++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Command.java
@@ -0,0 +1,61 @@
+package fr.traqueur.commands.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines a command on a method within a {@link CommandContainer}.
+ *
+ * The method's first parameter should be the sender type (resolved via
+ * the platform's SenderResolver). Additional parameters should be annotated
+ * with {@link Arg}.
+ *
+ * Example:
+ * {@code
+ * @Command(name = "heal", permission = "admin.heal", description = "Heal a player")
+ * public void heal(Player sender, @Arg("target") @Optional Player target) {
+ * Player toHeal = (target != null) ? target : sender;
+ * toHeal.setHealth(20);
+ * }
+ * }
+ *
+ * @since 5.0.0
+ * @see CommandContainer
+ * @see Arg
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Command {
+
+ /**
+ * The command name (without /).
+ *
+ * @return the command name
+ */
+ String name();
+
+ /**
+ * The permission required to execute this command.
+ * Empty string means no permission required.
+ *
+ * @return the permission node
+ */
+ String permission() default "";
+
+ /**
+ * The command description.
+ *
+ * @return the description
+ */
+ String description() default "";
+
+ /**
+ * The usage string displayed on incorrect usage.
+ * If empty, auto-generated from arguments.
+ *
+ * @return the usage string
+ */
+ String usage() default "";
+}
\ No newline at end of file
diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/CommandContainer.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/CommandContainer.java
new file mode 100644
index 0000000..955f1bc
--- /dev/null
+++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/CommandContainer.java
@@ -0,0 +1,38 @@
+package fr.traqueur.commands.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a class as a container for annotated commands.
+ *
+ * A command container holds multiple commands defined via {@link Command}
+ * annotations on methods.
+ *
+ * Example:
+ * {@code
+ * @CommandContainer
+ * public class AdminCommands {
+ *
+ * @Command(name = "heal", permission = "admin.heal")
+ * public void heal(Player sender, @Arg("target") Player target) {
+ * target.setHealth(20);
+ * }
+ *
+ * @Command(name = "feed", permission = "admin.feed")
+ * public void feed(Player sender) {
+ * sender.setFoodLevel(20);
+ * }
+ * }
+ * }
+ *
+ * @since 5.0.0
+ * @see Command
+ * @see AnnotationCommandProcessor
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface CommandContainer {
+}
\ No newline at end of file
diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Infinite.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Infinite.java
new file mode 100644
index 0000000..18c1d57
--- /dev/null
+++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Infinite.java
@@ -0,0 +1,38 @@
+package fr.traqueur.commands.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks an {@link Arg} parameter as infinite (varargs).
+ *
+ * An infinite argument consumes all remaining input and joins it
+ * into a single string. Typically used for messages or reasons.
+ *
+ * There can only be one infinite argument per command, and it must
+ * be the last argument.
+ *
+ * Example:
+ * {@code
+ * @Command(name = "broadcast")
+ * public void broadcast(CommandSender sender, @Arg("message") @Infinite String message) {
+ * Bukkit.broadcastMessage(message);
+ * }
+ *
+ * @Command(name = "kick")
+ * public void kick(Player sender,
+ * @Arg("player") Player target,
+ * @Arg("reason") @Optional @Infinite String reason) {
+ * target.kick(reason != null ? reason : "You have been kicked");
+ * }
+ * }
+ *
+ * @since 5.0.0
+ * @see Arg
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface Infinite {
+}
\ No newline at end of file
diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/TabComplete.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/TabComplete.java
new file mode 100644
index 0000000..7848a55
--- /dev/null
+++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/TabComplete.java
@@ -0,0 +1,58 @@
+package fr.traqueur.commands.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines a tab completer for a specific argument of a command.
+ *
+ * The annotated method must return {@code List} and can have
+ * the sender as first parameter, followed by the current input string.
+ *
+ * Example:
+ * {@code
+ * @Command(name = "warp")
+ * public void warp(Player sender, @Arg("name") String warpName) {
+ * // teleport to warp
+ * }
+ *
+ * @TabComplete(command = "warp", arg = "name")
+ * public List completeWarpName(Player sender, String current) {
+ * return getWarps().stream()
+ * .filter(w -> w.startsWith(current.toLowerCase()))
+ * .toList();
+ * }
+ * }
+ *
+ * For subcommands, use dot notation in the command parameter:
+ * {@code
+ * @TabComplete(command = "warp.set", arg = "name")
+ * public List completeWarpSetName(Player sender, String current) {
+ * // ...
+ * }
+ * }
+ *
+ * @since 5.0.0
+ * @see Command
+ * @see Arg
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface TabComplete {
+
+ /**
+ * The command name (or path for subcommands using dot notation).
+ *
+ * @return the command path
+ */
+ String command();
+
+ /**
+ * The argument name to provide completions for.
+ *
+ * @return the argument name
+ */
+ String arg();
+}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java
new file mode 100644
index 0000000..e548aac
--- /dev/null
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java
@@ -0,0 +1,401 @@
+package fr.traqueur.commands.annotations;
+
+import fr.traqueur.commands.annotations.commands.*;
+import fr.traqueur.commands.api.arguments.Argument;
+import fr.traqueur.commands.api.models.Command;
+import fr.traqueur.commands.test.mocks.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("AnnotationCommandProcessor")
+class AnnotationCommandProcessorTest {
+
+ private MockPlatform platform;
+ private AnnotationCommandProcessor processor;
+
+ @BeforeEach
+ void setUp() {
+ platform = new MockPlatform();
+ MockCommandManager manager = new MockCommandManager(platform);
+ processor = new AnnotationCommandProcessor<>(manager);
+ }
+
+ @Nested
+ @DisplayName("Basic Registration")
+ class BasicRegistration {
+
+ @Test
+ @DisplayName("should register simple command")
+ void shouldRegisterSimpleCommand() {
+ SimpleTestCommands commands = new SimpleTestCommands();
+ processor.register(commands);
+
+ assertTrue(platform.hasCommand("test"));
+ Command cmd = platform.getCommand("test");
+ assertNotNull(cmd);
+ assertEquals("A test command", cmd.getDescription());
+ assertEquals("test.use", cmd.getPermission());
+ }
+
+ @Test
+ @DisplayName("should register command with string argument")
+ void shouldRegisterCommandWithStringArg() {
+ SimpleTestCommands commands = new SimpleTestCommands();
+ processor.register(commands);
+
+ assertTrue(platform.hasCommand("greet"));
+ Command cmd = platform.getCommand("greet");
+
+ List> args = cmd.getArgs();
+ assertEquals(1, args.size());
+ assertEquals("name", args.get(0).name());
+ }
+
+ @Test
+ @DisplayName("should register command with multiple arguments")
+ void shouldRegisterCommandWithMultipleArgs() {
+ SimpleTestCommands commands = new SimpleTestCommands();
+ processor.register(commands);
+
+ assertTrue(platform.hasCommand("add"));
+ Command cmd = platform.getCommand("add");
+
+ List> args = cmd.getArgs();
+ assertEquals(2, args.size());
+ assertEquals("a", args.get(0).name());
+ assertEquals("b", args.get(1).name());
+ }
+
+ @Test
+ @DisplayName("should register multiple handlers")
+ void shouldRegisterMultipleHandlers() {
+ SimpleTestCommands simple = new SimpleTestCommands();
+ AliasTestCommands alias = new AliasTestCommands();
+
+ processor.register(simple, alias);
+
+ assertTrue(platform.hasCommand("test"));
+ assertTrue(platform.hasCommand("greet"));
+ assertTrue(platform.hasCommand("gamemode"));
+ assertTrue(platform.hasCommand("teleport"));
+ }
+ }
+
+ @Nested
+ @DisplayName("Hierarchical Commands")
+ class HierarchicalCommands {
+
+ @Test
+ @DisplayName("should register parent command")
+ void shouldRegisterParentCommand() {
+ HierarchicalTestCommands commands = new HierarchicalTestCommands();
+ processor.register(commands);
+
+ assertTrue(platform.hasCommand("admin"));
+ }
+
+ @Test
+ @DisplayName("should register first level subcommands")
+ void shouldRegisterFirstLevelSubcommands() {
+ HierarchicalTestCommands commands = new HierarchicalTestCommands();
+ processor.register(commands);
+
+ assertTrue(platform.hasCommand("admin.reload"));
+ assertTrue(platform.hasCommand("admin.info"));
+ }
+
+ @Test
+ @DisplayName("should register second level subcommands")
+ void shouldRegisterSecondLevelSubcommands() {
+ HierarchicalTestCommands commands = new HierarchicalTestCommands();
+ processor.register(commands);
+
+ assertTrue(platform.hasCommand("admin.reload.config"));
+ assertTrue(platform.hasCommand("admin.reload.plugins"));
+ }
+
+ @Test
+ @DisplayName("should add subcommands to parent")
+ void shouldAddSubcommandsToParent() {
+ HierarchicalTestCommands commands = new HierarchicalTestCommands();
+ processor.register(commands);
+
+ Command admin = platform.getCommand("admin");
+ List> subcommands = admin.getSubcommands();
+ assertEquals(2, subcommands.size());
+
+ Command reload = subcommands.stream()
+ .filter(c -> c.getName().equals("reload"))
+ .findFirst()
+ .orElse(null);
+ assertNotNull(reload);
+ assertEquals(2, reload.getSubcommands().size());
+ }
+
+ @Test
+ @DisplayName("should preserve permission on subcommands")
+ void shouldPreservePermissionOnSubcommands() {
+ HierarchicalTestCommands commands = new HierarchicalTestCommands();
+ processor.register(commands);
+
+ Command admin = platform.getCommand("admin");
+ Command reload = admin.getSubcommands().stream()
+ .filter(c -> c.getName().equals("reload"))
+ .findFirst()
+ .orElse(null);
+
+ assertNotNull(reload);
+ assertEquals("admin.reload", reload.getPermission());
+ }
+ }
+
+ @Nested
+ @DisplayName("Orphan Commands")
+ class OrphanCommands {
+
+ @Test
+ @DisplayName("should register orphan subcommand directly")
+ void shouldRegisterOrphanSubcommandDirectly() {
+ OrphanTestCommands commands = new OrphanTestCommands();
+ processor.register(commands);
+
+ assertTrue(platform.hasCommand("warp.set"));
+ assertTrue(platform.hasCommand("warp.delete"));
+ }
+
+ @Test
+ @DisplayName("should register deep orphan command")
+ void shouldRegisterDeepOrphanCommand() {
+ OrphanTestCommands commands = new OrphanTestCommands();
+ processor.register(commands);
+
+ assertTrue(platform.hasCommand("config.database.reset"));
+ }
+
+ @Test
+ @DisplayName("orphan commands should be gameOnly when using MockPlayer")
+ void orphanCommandsShouldBeGameOnly() {
+ OrphanTestCommands commands = new OrphanTestCommands();
+ processor.register(commands);
+
+ Command warpSet = platform.getCommand("warp.set");
+ assertTrue(warpSet.inGameOnly());
+ }
+ }
+
+ @Nested
+ @DisplayName("Optional Arguments")
+ class OptionalArguments {
+
+ @Test
+ @DisplayName("should register command with optional argument")
+ void shouldRegisterCommandWithOptionalArg() {
+ OptionalArgsTestCommands commands = new OptionalArgsTestCommands();
+ processor.register(commands);
+
+ Command heal = platform.getCommand("heal");
+
+ assertEquals(0, heal.getArgs().size());
+ assertEquals(1, heal.getOptionalArgs().size());
+ assertEquals("target", heal.getOptionalArgs().get(0).name());
+ }
+
+ @Test
+ @DisplayName("should register command with mixed required and optional args")
+ void shouldRegisterMixedArgs() {
+ OptionalArgsTestCommands commands = new OptionalArgsTestCommands();
+ processor.register(commands);
+
+ Command give = platform.getCommand("give");
+
+ assertEquals(1, give.getArgs().size());
+ assertEquals("item", give.getArgs().getFirst().name());
+
+ assertEquals(1, give.getOptionalArgs().size());
+ assertEquals("amount", give.getOptionalArgs().getFirst().name());
+ }
+ }
+
+ @Nested
+ @DisplayName("Infinite Arguments")
+ class InfiniteArguments {
+
+ @Test
+ @DisplayName("should register command with infinite argument")
+ void shouldRegisterCommandWithInfiniteArg() {
+ InfiniteArgsTestCommands commands = new InfiniteArgsTestCommands();
+ processor.register(commands);
+
+ Command broadcast = platform.getCommand("broadcast");
+
+ assertEquals(1, broadcast.getArgs().size());
+ Argument arg = broadcast.getArgs().get(0);
+ assertEquals("message", arg.name());
+ assertTrue(arg.type().isInfinite());
+ }
+
+ @Test
+ @DisplayName("should register command with optional infinite argument")
+ void shouldRegisterOptionalInfiniteArg() {
+ InfiniteArgsTestCommands commands = new InfiniteArgsTestCommands();
+ processor.register(commands);
+
+ Command kick = platform.getCommand("kick");
+
+ assertEquals(1, kick.getArgs().size());
+ assertEquals(1, kick.getOptionalArgs().size());
+
+ Argument reason = kick.getOptionalArgs().get(0);
+ assertEquals("reason", reason.name());
+ assertTrue(reason.type().isInfinite());
+ }
+ }
+
+ @Nested
+ @DisplayName("Aliases")
+ class Aliases {
+
+ @Test
+ @DisplayName("should register command with single alias")
+ void shouldRegisterSingleAlias() {
+ AliasTestCommands commands = new AliasTestCommands();
+ processor.register(commands);
+
+ Command gamemode = platform.getCommand("gamemode");
+
+ List aliases = gamemode.getAliases();
+ assertEquals(1, aliases.size());
+ assertTrue(aliases.contains("gm"));
+ }
+
+ @Test
+ @DisplayName("should register command with multiple aliases")
+ void shouldRegisterMultipleAliases() {
+ AliasTestCommands commands = new AliasTestCommands();
+ processor.register(commands);
+
+ Command teleport = platform.getCommand("teleport");
+
+ List aliases = teleport.getAliases();
+ assertEquals(3, aliases.size());
+ assertTrue(aliases.contains("tp"));
+ assertTrue(aliases.contains("tpto"));
+ assertTrue(aliases.contains("goto"));
+ }
+
+ @Test
+ @DisplayName("all labels should be registered")
+ void allLabelsShouldBeRegistered() {
+ AliasTestCommands commands = new AliasTestCommands();
+ processor.register(commands);
+
+ assertTrue(platform.hasCommand("spawn"));
+ assertTrue(platform.hasCommand("hub"));
+ assertTrue(platform.hasCommand("lobby"));
+ assertTrue(platform.hasCommand("s"));
+ }
+ }
+
+ @Nested
+ @DisplayName("Tab Completion")
+ class TabCompletion {
+
+ @Test
+ @DisplayName("should register tab completer for argument")
+ void shouldRegisterTabCompleter() {
+ TabCompleteTestCommands commands = new TabCompleteTestCommands();
+ processor.register(commands);
+
+ Command world = platform.getCommand("world");
+
+ List> args = world.getArgs();
+ assertEquals(1, args.size());
+ assertNotNull(args.get(0).tabCompleter());
+ }
+ }
+
+ @Nested
+ @DisplayName("Game Only Detection")
+ class GameOnlyDetection {
+
+ @Test
+ @DisplayName("should detect gameOnly when first param is MockPlayer")
+ void shouldDetectGameOnlyWithMockPlayer() {
+ AliasTestCommands commands = new AliasTestCommands();
+ processor.register(commands);
+
+ Command gamemode = platform.getCommand("gamemode");
+ assertTrue(gamemode.inGameOnly());
+ }
+
+ @Test
+ @DisplayName("should not be gameOnly when first param is MockSender")
+ void shouldNotBeGameOnlyWithMockSender() {
+ SimpleTestCommands commands = new SimpleTestCommands();
+ processor.register(commands);
+
+ Command test = platform.getCommand("test");
+ assertFalse(test.inGameOnly());
+ }
+ }
+
+ @Nested
+ @DisplayName("Error Handling")
+ class ErrorHandling {
+
+ @Test
+ @DisplayName("should throw when class is not annotated with @CommandContainer")
+ void shouldThrowWhenMissingCommandContainer() {
+ InvalidContainerNoAnnotation invalid = new InvalidContainerNoAnnotation();
+
+ IllegalArgumentException ex = assertThrows(
+ IllegalArgumentException.class,
+ () -> processor.register(invalid)
+ );
+
+ assertTrue(ex.getMessage().contains("@CommandContainer"));
+ }
+
+ @Test
+ @DisplayName("should throw when parameter is missing @Arg annotation")
+ void shouldThrowWhenMissingArgAnnotation() {
+ InvalidContainerMissingArg invalid = new InvalidContainerMissingArg();
+
+ IllegalArgumentException ex = assertThrows(
+ IllegalArgumentException.class,
+ () -> processor.register(invalid)
+ );
+
+ assertTrue(ex.getMessage().contains("@Arg"));
+ }
+ }
+
+ @Nested
+ @DisplayName("Registration Order")
+ class RegistrationOrder {
+
+ @Test
+ @DisplayName("commands should be sorted by depth (parents first)")
+ void commandsShouldBeSortedByDepth() {
+ HierarchicalTestCommands commands = new HierarchicalTestCommands();
+ processor.register(commands);
+
+ List labels = platform.getRegisteredLabels();
+
+ int adminIndex = labels.indexOf("admin");
+ int adminReloadIndex = labels.indexOf("admin.reload");
+ int adminReloadConfigIndex = labels.indexOf("admin.reload.config");
+
+ assertTrue(adminIndex < adminReloadIndex,
+ "admin should be registered before admin.reload");
+ assertTrue(adminReloadIndex < adminReloadConfigIndex,
+ "admin.reload should be registered before admin.reload.config");
+ }
+ }
+}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/AliasTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/AliasTestCommands.java
new file mode 100644
index 0000000..890dd0d
--- /dev/null
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/AliasTestCommands.java
@@ -0,0 +1,31 @@
+package fr.traqueur.commands.annotations.commands;
+
+import fr.traqueur.commands.annotations.*;
+import fr.traqueur.commands.test.mocks.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@CommandContainer
+public class AliasTestCommands {
+
+ public final List executedCommands = new ArrayList<>();
+
+ @Command(name = "gamemode", description = "Change game mode")
+ @Alias({"gm"})
+ public void gamemode(MockPlayer sender, @Arg("mode") String mode) {
+ executedCommands.add("gamemode:" + mode);
+ }
+
+ @Command(name = "teleport", description = "Teleport to player")
+ @Alias({"tp", "tpto", "goto"})
+ public void teleport(MockPlayer sender, @Arg("target") String target) {
+ executedCommands.add("teleport:" + target);
+ }
+
+ @Command(name = "spawn")
+ @Alias({"hub", "lobby", "s"})
+ public void spawn(MockPlayer sender) {
+ executedCommands.add("spawn");
+ }
+}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/HierarchicalTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/HierarchicalTestCommands.java
new file mode 100644
index 0000000..86f9630
--- /dev/null
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/HierarchicalTestCommands.java
@@ -0,0 +1,39 @@
+package fr.traqueur.commands.annotations.commands;
+
+import fr.traqueur.commands.annotations.Command;
+import fr.traqueur.commands.annotations.CommandContainer;
+import fr.traqueur.commands.test.mocks.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@CommandContainer
+public class HierarchicalTestCommands {
+
+ public final List executedCommands = new ArrayList<>();
+
+ @Command(name = "admin", description = "Admin commands")
+ public void admin(MockSender sender) {
+ executedCommands.add("admin");
+ }
+
+ @Command(name = "admin.reload", description = "Reload configuration", permission = "admin.reload")
+ public void adminReload(MockSender sender) {
+ executedCommands.add("admin.reload");
+ }
+
+ @Command(name = "admin.info", description = "Show server info")
+ public void adminInfo(MockSender sender) {
+ executedCommands.add("admin.info");
+ }
+
+ @Command(name = "admin.reload.config", description = "Reload config file")
+ public void adminReloadConfig(MockSender sender) {
+ executedCommands.add("admin.reload.config");
+ }
+
+ @Command(name = "admin.reload.plugins", description = "Reload plugins")
+ public void adminReloadPlugins(MockSender sender) {
+ executedCommands.add("admin.reload.plugins");
+ }
+}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InfiniteArgsTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InfiniteArgsTestCommands.java
new file mode 100644
index 0000000..516c7aa
--- /dev/null
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InfiniteArgsTestCommands.java
@@ -0,0 +1,32 @@
+package fr.traqueur.commands.annotations.commands;
+
+import fr.traqueur.commands.annotations.Arg;
+import fr.traqueur.commands.annotations.Command;
+import fr.traqueur.commands.annotations.CommandContainer;
+import fr.traqueur.commands.annotations.Infinite;
+import fr.traqueur.commands.test.mocks.MockSender;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+@CommandContainer
+public class InfiniteArgsTestCommands {
+
+ public final List executedCommands = new ArrayList<>();
+ public final List executedArgs = new ArrayList<>();
+
+ @Command(name = "broadcast", description = "Broadcast a message")
+ public void broadcast(MockSender sender, @Arg("message") @Infinite String message) {
+ executedCommands.add("broadcast");
+ executedArgs.add(new Object[]{sender, message});
+ }
+
+ @Command(name = "kick", description = "Kick a player")
+ public void kick(MockSender sender,
+ @Arg("player") String player,
+ @Arg("reason") @Infinite Optional reason) {
+ executedCommands.add("kick");
+ executedArgs.add(new Object[]{sender, player, reason.orElse(null)});
+ }
+}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerMissingArg.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerMissingArg.java
new file mode 100644
index 0000000..de44b04
--- /dev/null
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerMissingArg.java
@@ -0,0 +1,13 @@
+package fr.traqueur.commands.annotations.commands;
+
+import fr.traqueur.commands.annotations.*;
+import fr.traqueur.commands.test.mocks.*;
+
+@CommandContainer
+public class InvalidContainerMissingArg {
+
+ @Command(name = "test")
+ public void test(MockSender sender, String missingArgAnnotation) {
+ // Should fail - second parameter has no @Arg
+ }
+}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerNoAnnotation.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerNoAnnotation.java
new file mode 100644
index 0000000..305981d
--- /dev/null
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerNoAnnotation.java
@@ -0,0 +1,12 @@
+package fr.traqueur.commands.annotations.commands;
+
+import fr.traqueur.commands.annotations.Command;
+import fr.traqueur.commands.test.mocks.MockSender;
+
+// Missing @CommandContainer - should throw error
+public class InvalidContainerNoAnnotation {
+
+ @Command(name = "test")
+ public void test(MockSender sender) {
+ }
+}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OptionalArgsTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OptionalArgsTestCommands.java
new file mode 100644
index 0000000..0405942
--- /dev/null
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OptionalArgsTestCommands.java
@@ -0,0 +1,31 @@
+package fr.traqueur.commands.annotations.commands;
+
+import fr.traqueur.commands.annotations.Arg;
+import fr.traqueur.commands.annotations.Command;
+import fr.traqueur.commands.annotations.CommandContainer;
+import fr.traqueur.commands.test.mocks.MockPlayer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+@CommandContainer
+public class OptionalArgsTestCommands {
+
+ public final List executedCommands = new ArrayList<>();
+ public final List executedArgs = new ArrayList<>();
+
+ @Command(name = "heal", description = "Heal a player")
+ public void heal(MockPlayer sender, @Arg("target") Optional target) {
+ executedCommands.add("heal");
+ executedArgs.add(new Object[]{sender, target.orElse(null)});
+ }
+
+ @Command(name = "give", description = "Give items")
+ public void give(MockPlayer sender,
+ @Arg("item") String item,
+ @Arg("amount") Optional amount) {
+ executedCommands.add("give");
+ executedArgs.add(new Object[]{sender, item, amount.orElse(null)});
+ }
+}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OrphanTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OrphanTestCommands.java
new file mode 100644
index 0000000..43f632d
--- /dev/null
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OrphanTestCommands.java
@@ -0,0 +1,30 @@
+package fr.traqueur.commands.annotations.commands;
+
+import fr.traqueur.commands.annotations.*;
+import fr.traqueur.commands.test.mocks.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@CommandContainer
+public class OrphanTestCommands {
+
+ public final List executedCommands = new ArrayList<>();
+
+ // No "warp" parent defined - core will handle it
+ @Command(name = "warp.set", description = "Set a warp", permission = "warp.set")
+ public void warpSet(MockPlayer sender, @Arg("name") String name) {
+ executedCommands.add("warp.set:" + name);
+ }
+
+ @Command(name = "warp.delete", description = "Delete a warp")
+ public void warpDelete(MockPlayer sender, @Arg("name") String name) {
+ executedCommands.add("warp.delete:" + name);
+ }
+
+ // Deep orphan - no "config" or "config.database" defined
+ @Command(name = "config.database.reset", description = "Reset database")
+ public void configDatabaseReset(MockSender sender) {
+ executedCommands.add("config.database.reset");
+ }
+}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/SimpleTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/SimpleTestCommands.java
new file mode 100644
index 0000000..2786169
--- /dev/null
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/SimpleTestCommands.java
@@ -0,0 +1,34 @@
+package fr.traqueur.commands.annotations.commands;
+
+import fr.traqueur.commands.annotations.Arg;
+import fr.traqueur.commands.annotations.Command;
+import fr.traqueur.commands.annotations.CommandContainer;
+import fr.traqueur.commands.test.mocks.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@CommandContainer
+public class SimpleTestCommands {
+
+ public final List executedCommands = new ArrayList<>();
+ public final List executedArgs = new ArrayList<>();
+
+ @Command(name = "test", description = "A test command", permission = "test.use")
+ public void testCommand(MockSender sender) {
+ executedCommands.add("test");
+ executedArgs.add(new Object[]{sender});
+ }
+
+ @Command(name = "greet", description = "Greet someone")
+ public void greetCommand(MockSender sender, @Arg("name") String name) {
+ executedCommands.add("greet");
+ executedArgs.add(new Object[]{sender, name});
+ }
+
+ @Command(name = "add", description = "Add two numbers")
+ public void addCommand(MockSender sender, @Arg("a") Integer a, @Arg("b") Integer b) {
+ executedCommands.add("add");
+ executedArgs.add(new Object[]{sender, a, b});
+ }
+}
\ No newline at end of file
diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/TabCompleteTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/TabCompleteTestCommands.java
new file mode 100644
index 0000000..a68c14b
--- /dev/null
+++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/TabCompleteTestCommands.java
@@ -0,0 +1,56 @@
+package fr.traqueur.commands.annotations.commands;
+
+import fr.traqueur.commands.annotations.*;
+import fr.traqueur.commands.test.mocks.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@CommandContainer
+public class TabCompleteTestCommands {
+
+ public final List executedCommands = new ArrayList<>();
+ public final List tabCompleteInvocations = new ArrayList<>();
+
+ private final List availableWorlds = Arrays.asList("world", "world_nether", "world_the_end");
+ private final List availableWarps = Arrays.asList("spawn", "shop", "arena", "hub");
+
+ @Command(name = "world", description = "Change world")
+ public void world(MockPlayer sender, @Arg("world") String world) {
+ executedCommands.add("world:" + world);
+ }
+
+ @TabComplete(command = "world", arg = "world")
+ public List completeWorld(MockPlayer sender, String current) {
+ tabCompleteInvocations.add("world:" + current);
+ return availableWorlds.stream()
+ .filter(w -> w.toLowerCase().startsWith(current.toLowerCase()))
+ .toList();
+ }
+
+ @Command(name = "warp", description = "Warp to location")
+ public void warp(MockPlayer sender, @Arg("name") String name) {
+ executedCommands.add("warp:" + name);
+ }
+
+ @TabComplete(command = "warp", arg = "name")
+ public List completeWarp(MockPlayer sender, String current) {
+ tabCompleteInvocations.add("warp:" + current);
+ return availableWarps.stream()
+ .filter(w -> w.toLowerCase().startsWith(current.toLowerCase()))
+ .toList();
+ }
+
+ // Tab completer without parameters
+ @Command(name = "gamemode", description = "Change gamemode")
+ public void gamemode(MockPlayer sender, @Arg("mode") String mode) {
+ executedCommands.add("gamemode:" + mode);
+ }
+
+ @TabComplete(command = "gamemode", arg = "mode")
+ public List completeGamemode() {
+ tabCompleteInvocations.add("gamemode:no-args");
+ return Arrays.asList("survival", "creative", "adventure", "spectator");
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index eb10988..d2e5a4b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,3 +1,8 @@
+plugins {
+ id 'java-library'
+ id 'maven-publish'
+}
+
allprojects {
group = 'fr.traqueur.commands'
version = property('version')
@@ -6,8 +11,11 @@ allprojects {
plugin 'java-library'
}
- tasks.withType(JavaCompile).configureEach {
- options.compilerArgs += ['-nowarn']
+ ext.classifier = System.getProperty('archive.classifier')
+ ext.sha = System.getProperty('github.sha')
+
+ if (rootProject.ext.has('sha') && rootProject.ext.sha) {
+ version = rootProject.ext.sha
}
repositories {
@@ -19,35 +27,140 @@ allprojects {
}
subprojects {
- if (!project.name.contains('test-plugin')) {
+ if (project.name.contains('test-')) {
+ return;
+ }
+
+ apply plugin: 'maven-publish'
+
+ java {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ withSourcesJar()
+ withJavadocJar()
+ }
+
+ tasks.withType(JavaCompile).configureEach {
+ options.encoding = 'UTF-8'
+ options.compilerArgs.addAll([
+ '-Xlint:all',
+ '-Xlint:-processing',
+ '-Xlint:-serial'
+ ])
+ }
+
+ tasks.withType(Javadoc).configureEach {
+ options.encoding = 'UTF-8'
+ options.addStringOption('Xdoclint:none', '-quiet')
+ }
+
+ dependencies {
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
+ testImplementation 'org.mockito:mockito-core:5.3.1'
+ }
+ test {
+ useJUnitPlatform()
+ jvmArgs += ['-XX:+EnableDynamicAgentLoading']
+ reports {
+ html.required.set(true)
+ junitXml.required.set(true)
+ }
- dependencies {
- testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
- testImplementation 'org.mockito:mockito-core:5.3.1'
+ testLogging {
+ showStandardStreams = true
+ events("passed", "skipped", "failed", "standardOut", "standardError")
+ exceptionFormat = "full"
}
- test {
- useJUnitPlatform()
- jvmArgs += ['-XX:+EnableDynamicAgentLoading']
- reports {
- html.required.set(true)
- junitXml.required.set(true)
+ }
+
+ publishing {
+ repositories {
+ maven {
+ def repository = System.getProperty('repository.name', 'snapshots')
+ def repoType = repository.toLowerCase()
+
+ name = "groupez${repository.capitalize()}"
+ url = uri("https://repo.groupez.dev/${repoType}")
+
+ credentials {
+ username = findProperty("${name}Username") ?: System.getenv('MAVEN_USERNAME')
+ password = findProperty("${name}Password") ?: System.getenv('MAVEN_PASSWORD')
+ }
+
+ authentication {
+ create("basic", BasicAuthentication)
+ }
}
+ }
+
+ publications {
+ create('maven', MavenPublication) {
+ from components.java
+
+ groupId = rootProject.group.toString()
+
+ if (project.name == 'core') {
+ artifactId = 'core'
+ } else if (project.name == 'annotations-addon') {
+ artifactId = 'annotations-addon'
+ } else {
+ def platform = project.name.replaceFirst(/^platform-/, '')
+ artifactId = "platform-${platform}"
+ }
- testLogging {
- showStandardStreams = true
- events("passed", "skipped", "failed", "standardOut", "standardError")
- exceptionFormat "full"
+ version = rootProject.version.toString()
+
+ pom {
+ name = artifactId
+ description = 'CommandsAPI - A flexible command framework for Java applications'
+ url = 'https://github.com/Traqueur-dev/CommandsAPI'
+
+ licenses {
+ license {
+ name = 'MIT License'
+ url = 'https://opensource.org/licenses/MIT'
+ }
+ }
+
+ developers {
+ developer {
+ id = 'traqueur'
+ name = 'Traqueur'
+ }
+ }
+
+ scm {
+ connection = 'scm:git:git://github.com/Traqueur-dev/CommandsAPI.git'
+ developerConnection = 'scm:git:ssh://github.com/Traqueur-dev/CommandsAPI.git'
+ url = 'https://github.com/Traqueur-dev/CommandsAPI'
+ }
+ }
}
}
}
+
}
-tasks.register("testAll") {
- group = "verification"
- description = "Execute tous les tests des sous-projets qui ont une tâche 'test'"
+tasks.register('publishAll') {
+ description = 'Publishes all subprojects to Maven repository'
+ group = 'publishing'
- // Déclare la dépendance vers chaque tâche 'test' de chaque sous-projet
- dependsOn subprojects
- .findAll { it.tasks.findByName('test') != null }
- .collect { it.tasks.named('test') }
+ subprojects.each { sub ->
+ sub.plugins.withId('maven-publish') {
+ dependsOn sub.tasks.named('publish')
+ }
+ }
}
+
+
+tasks.register('testAll') {
+ description = 'Runs all tests across all subprojects'
+ group = 'verification'
+
+ subprojects.each { sub ->
+ sub.plugins.withId('java') {
+ dependsOn sub.tasks.named('test')
+ }
+ }
+}
+
diff --git a/core/build.gradle b/core/build.gradle
index 9f48b74..3cfd6a9 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -1,15 +1,7 @@
plugins {
- id 'maven-publish'
id("me.champeau.jmh") version "0.7.3"
}
-java {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- withSourcesJar()
- withJavadocJar()
-}
-
dependencies {
jmh 'org.openjdk.jmh:jmh-core:1.37'
jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
@@ -35,15 +27,4 @@ sourceSets {
srcDir generatedResourcesDir
}
}
-}
-
-publishing {
- publications {
- maven(MavenPublication) {
- from components.java
- groupId = project.group
- artifactId = 'core'
- version = project.version
- }
- }
}
\ No newline at end of file
diff --git a/core/src/jmh/java/fr/traqueur/commands/CommandLookupBenchmark.java b/core/src/jmh/java/fr/traqueur/commands/CommandLookupBenchmark.java
index eaed6a6..23efe55 100644
--- a/core/src/jmh/java/fr/traqueur/commands/CommandLookupBenchmark.java
+++ b/core/src/jmh/java/fr/traqueur/commands/CommandLookupBenchmark.java
@@ -4,7 +4,9 @@
import fr.traqueur.commands.api.models.collections.CommandTree;
import org.openjdk.jmh.annotations.*;
-import java.util.*;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@@ -16,10 +18,10 @@
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CommandLookupBenchmark {
- @Param({ "1000", "10000", "50000" })
+ @Param({"1000", "10000", "50000"})
public int N;
- @Param({ "1", "2", "3" })
+ @Param({"1", "2", "3"})
public int maxDepth;
private Map flatMap;
@@ -29,7 +31,7 @@ public class CommandLookupBenchmark {
@Setup(Level.Trial)
public void setup() {
flatMap = new HashMap<>(N);
- tree = new CommandTree<>();
+ tree = new CommandTree<>();
rawLabels = new String[N];
ThreadLocalRandom rnd = ThreadLocalRandom.current();
@@ -74,7 +76,12 @@ public CommandTree.MatchResult treeLookup() {
}
public static class DummyCommand extends Command {
- public DummyCommand(String name) { super(null, name); }
- @Override public void execute(Object s, fr.traqueur.commands.api.arguments.Arguments a) {}
+ public DummyCommand(String name) {
+ super(null, name);
+ }
+
+ @Override
+ public void execute(Object s, fr.traqueur.commands.api.arguments.Arguments a) {
+ }
}
}
diff --git a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java
index acc91df..a3782e2 100644
--- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java
+++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java
@@ -5,14 +5,17 @@
import fr.traqueur.commands.api.arguments.Arguments;
import fr.traqueur.commands.api.arguments.TabCompleter;
import fr.traqueur.commands.api.exceptions.ArgumentIncorrectException;
-import fr.traqueur.commands.api.exceptions.CommandRegistrationException;
import fr.traqueur.commands.api.exceptions.TypeArgumentNotExistException;
import fr.traqueur.commands.api.logging.Logger;
import fr.traqueur.commands.api.logging.MessageHandler;
import fr.traqueur.commands.api.models.Command;
+import fr.traqueur.commands.api.models.CommandBuilder;
import fr.traqueur.commands.api.models.CommandInvoker;
import fr.traqueur.commands.api.models.CommandPlatform;
import fr.traqueur.commands.api.models.collections.CommandTree;
+import fr.traqueur.commands.api.parsing.ArgumentParser;
+import fr.traqueur.commands.api.parsing.ParseError;
+import fr.traqueur.commands.api.parsing.ParseResult;
import fr.traqueur.commands.api.updater.Updater;
import fr.traqueur.commands.impl.arguments.BooleanArgument;
import fr.traqueur.commands.impl.arguments.DoubleArgument;
@@ -20,42 +23,33 @@
import fr.traqueur.commands.impl.arguments.LongArgument;
import fr.traqueur.commands.impl.logging.InternalLogger;
import fr.traqueur.commands.impl.logging.InternalMessageHandler;
+import fr.traqueur.commands.impl.parsing.DefaultArgumentParser;
import java.util.*;
-import java.util.stream.Collectors;
/**
* This class is the command manager.
* It allows you to register commands and subcommands.
* It also allows you to register argument converters and tab completer.
+ *
* @param The type of the platform that will use this command manager.
* @param The type of the sender that will use this command manager.
*/
public abstract class CommandManager {
- /**
- * The parser used to separate the type of the argument from its name.
- * For example: "player:Player" will be parsed as "player" and "Player".
- */
- public static final String TYPE_PARSER = ":";
- /**
- * The parser used to separate the infinite argument from its name.
- * For example: "args:infinite" will be parsed as "args" and "infinite".
- */
- private static final String INFINITE = "infinite";
-
- private final CommandPlatform platform;
+ private final ArgumentParser parser;
+ private final CommandPlatform platform;
/**
* The commands registered in the command manager.
*/
- private final CommandTree commands;
+ private final CommandTree commands;
/**
* The argument converters registered in the command manager.
*/
- private final Map, ArgumentConverter>>> typeConverters;
+ private final Map> typeConverters;
/**
* The tab completer registered in the command manager.
@@ -63,7 +57,7 @@ public abstract class CommandManager {
private final Map>> completers;
- private final CommandInvoker invoker;
+ private final CommandInvoker invoker;
/**
* The message handler of the command manager.
@@ -83,9 +77,10 @@ public abstract class CommandManager {
/**
* Create a new command manager.
+ *
* @param platform The platform of the command manager.
*/
- public CommandManager(CommandPlatform platform) {
+ public CommandManager(CommandPlatform platform) {
Updater.checkUpdates();
this.platform = platform;
this.platform.injectManager(this);
@@ -96,20 +91,22 @@ public CommandManager(CommandPlatform platform) {
this.typeConverters = new HashMap<>();
this.completers = new HashMap<>();
this.invoker = new CommandInvoker<>(this);
+ this.parser = new DefaultArgumentParser<>(this.typeConverters, this.logger);
this.registerInternalConverters();
}
-
/**
- * Set the custom logger of the command manager.
- * @param logger The logger to set.
+ * Get the message handler of the command manager.
+ *
+ * @return The message handler of the command manager.
*/
- public void setLogger(Logger logger) {
- this.logger = logger;
+ public MessageHandler getMessageHandler() {
+ return messageHandler;
}
/**
* Set the message handler of the command manager.
+ *
* @param messageHandler The message handler to set.
*/
public void setMessageHandler(MessageHandler messageHandler) {
@@ -117,46 +114,38 @@ public void setMessageHandler(MessageHandler messageHandler) {
}
/**
- * Get the message handler of the command manager.
- * @return The message handler of the command manager.
+ * Get the debug mode of the command manager.
+ *
+ * @return If the debug mode is enabled.
*/
- public MessageHandler getMessageHandler() {
- return messageHandler;
+ public boolean isDebug() {
+ return this.debug;
}
/**
* Set the debug mode of the command manager.
+ *
* @param debug If the debug mode is enabled.
*/
public void setDebug(boolean debug) {
this.debug = debug;
}
- /**
- * Get the debug mode of the command manager.
- * @return If the debug mode is enabled.
- */
- public boolean isDebug() {
- return this.debug;
- }
-
/**
* Register a command in the command manager.
+ *
* @param command The command to register.
*/
- public void registerCommand(Command command) {
- try {
- for (String alias : command.getAliases()) {
- this.addCommand(command, alias);
- this.registerSubCommands(alias, command.getSubcommands());
- }
- } catch(TypeArgumentNotExistException e) {
- throw new CommandRegistrationException("Failed to register command: " + command.getClass().getSimpleName(), e);
+ public void registerCommand(Command command) {
+ for (String label : command.getAllLabels()) {
+ this.addCommand(command, label);
+ this.registerSubCommands(label, command.getSubcommands());
}
}
/**
* Unregister a command in the command manager.
+ *
* @param label The label of the command to unregister.
*/
public void unregisterCommand(String label) {
@@ -165,15 +154,16 @@ public void unregisterCommand(String label) {
/**
* Unregister a command in the command manager.
- * @param label The label of the command to unregister.
+ *
+ * @param label The label of the command to unregister.
* @param subcommands If the subcommands must be unregistered.
*/
public void unregisterCommand(String label, boolean subcommands) {
String[] rawArgs = label.split("\\.");
- Optional> commandOptional = this.commands.findNode(rawArgs)
- .flatMap(result -> result.node.getCommand());
+ Optional> commandOptional = this.commands.findNode(rawArgs)
+ .flatMap(result -> result.node().getCommand());
- if (!commandOptional.isPresent()) {
+ if (commandOptional.isEmpty()) {
throw new IllegalArgumentException("Command with label '" + label + "' does not exist.");
}
this.unregisterCommand(commandOptional.get(), subcommands);
@@ -181,127 +171,145 @@ public void unregisterCommand(String label, boolean subcommands) {
/**
* Unregister a command in the command manager.
+ *
* @param command The command to unregister.
*/
- public void unregisterCommand(Command command) {
+ public void unregisterCommand(Command command) {
this.unregisterCommand(command, true);
}
/**
* Unregister a command in the command manager.
- * @param command The command to unregister.
+ *
+ * @param command The command to unregister.
* @param subcommands If the subcommands must be unregistered.
*/
- public void unregisterCommand(Command command, boolean subcommands) {
- List aliases = new ArrayList<>(command.getAliases());
- aliases.add(command.getName());
- for (String alias : aliases) {
- this.removeCommand(alias, subcommands);
- if(subcommands) {
- this.unregisterSubCommands(alias, command.getSubcommands());
+ public void unregisterCommand(Command command, boolean subcommands) {
+ List labels = new ArrayList<>(command.getAllLabels());
+ for (String label : labels) {
+ this.removeCommand(label, subcommands);
+ if (subcommands) {
+ this.unregisterSubCommands(label, command.getSubcommands());
}
}
}
/**
* Register an argument converter in the command manager.
+ *
* @param typeClass The class of the type.
* @param converter The converter of the argument.
- * @param The type of the argument.
+ * @param The type of the argument.
*/
public void registerConverter(Class typeClass, ArgumentConverter converter) {
- this.typeConverters.put(typeClass.getSimpleName().toLowerCase(), new AbstractMap.SimpleEntry<>(typeClass, converter));
- }
-
- /**
- * Register an argument converter in the command manager.
- * @param typeClass The class of the type.
- * @param type The type of the argument.
- * @param converter The converter of the argument.
- * @param The type of the argument.
- */
- @Deprecated
- public void registerConverter(Class typeClass, String type, ArgumentConverter converter) {
- this.typeConverters.put(type, new AbstractMap.SimpleEntry<>(typeClass, converter));
+ this.typeConverters.put(typeClass.getSimpleName().toLowerCase(), new ArgumentConverter.Wrapper<>(typeClass, converter));
}
/**
* Parse the arguments of the command.
+ *
* @param command The command to parse.
- * @param args The arguments to parse.
+ * @param args The arguments to parse.
* @return The arguments parsed.
* @throws TypeArgumentNotExistException If the type of the argument does not exist.
- * @throws ArgumentIncorrectException If the argument is incorrect.
- */
- public Arguments parse(Command command, String[] args) throws TypeArgumentNotExistException, ArgumentIncorrectException {
- Arguments arguments = new Arguments(this.logger);
- List> templates = command.getArgs();
- for (int i = 0; i < templates.size(); i++) {
- String input = args[i];
- if (applyParsing(args, arguments, templates, i, input)) break;
- }
-
- List> optArgs = command.getOptinalArgs();
- if (optArgs.isEmpty()) {
- return arguments;
- }
-
- for (int i = 0; i < optArgs.size(); i++) {
- if (args.length > templates.size() + i) {
- String input = args[templates.size() + i];
- if (applyParsing(args, arguments, optArgs, i, input)) break;
+ * @throws ArgumentIncorrectException If the argument is incorrect.
+ */
+ public Arguments parse(Command command, String[] args) throws TypeArgumentNotExistException, ArgumentIncorrectException {
+ ParseResult result = parser.parse(command, args);
+ if (!result.isSuccess()) {
+ ParseError error = result.error();
+ switch (error.type()) {
+ case TYPE_NOT_FOUND -> throw new TypeArgumentNotExistException();
+ case CONVERSION_FAILED -> throw new ArgumentIncorrectException(error.input());
+ default -> throw new ArgumentIncorrectException(error.message());
}
}
-
- return arguments;
+ return result.arguments();
}
/**
* Get the commands of the command manager.
+ *
* @return The commands of the command manager.
*/
public CommandTree getCommands() {
return commands;
}
-
/**
* Get the completers of the command manager
+ *
* @return The completers of command manager
*/
public Map>> getCompleters() {
return this.completers;
}
+ /**
+ * Check if a TabCompleter exists for the given type.
+ *
+ * @param type The type to check.
+ * @return true if a TabCompleter is registered for this type.
+ */
+ public boolean hasTabCompleterForType(String type) {
+ ArgumentConverter.Wrapper> wrapper = this.typeConverters.get(type.toLowerCase());
+ return wrapper != null && wrapper.converter() instanceof TabCompleter;
+ }
+
+ /**
+ * Get the TabCompleter for the given type.
+ *
+ * @param type The type to get the TabCompleter for.
+ * @return The TabCompleter for this type, or null if none exists.
+ */
+ @SuppressWarnings("unchecked")
+ public TabCompleter getTabCompleterForType(String type) {
+ ArgumentConverter.Wrapper> wrapper = this.typeConverters.get(type.toLowerCase());
+ if (wrapper != null && wrapper.converter() instanceof TabCompleter) {
+ return (TabCompleter) wrapper.converter();
+ }
+ return null;
+ }
+
/**
* Get the platform of the command manager.
+ *
* @return The platform of the command manager.
*/
- public CommandPlatform getPlatform() {
+ public CommandPlatform getPlatform() {
return platform;
}
/**
* Get the logger of the command manager.
+ *
* @return The logger of the command manager.
*/
public Logger getLogger() {
return this.logger;
}
+ /**
+ * Set the custom logger of the command manager.
+ *
+ * @param logger The logger to set.
+ */
+ public void setLogger(Logger logger) {
+ this.logger = logger;
+ }
+
/**
* Register a list of subcommands in the command manager.
+ *
* @param parentLabel The parent label of the commands.
* @param subcommands The list of subcommands to register.
- * @throws TypeArgumentNotExistException If the type of the argument does not exist.
*/
- private void registerSubCommands(String parentLabel, List> subcommands) throws TypeArgumentNotExistException {
- if(subcommands == null || subcommands.isEmpty()) {
+ private void registerSubCommands(String parentLabel, List> subcommands) {
+ if (subcommands == null || subcommands.isEmpty()) {
return;
}
- for (Command subcommand : subcommands) {
- // getAliases() already returns [name, ...aliases], so no need to add the name again
- List aliasesSub = new ArrayList<>(subcommand.getAliases());
+ for (Command subcommand : subcommands) {
+ List aliasesSub = new ArrayList<>(subcommand.getAllLabels());
for (String aliasSub : aliasesSub) {
this.addCommand(subcommand, parentLabel + "." + aliasSub);
this.registerSubCommands(parentLabel + "." + aliasSub, subcommand.getSubcommands());
@@ -311,26 +319,27 @@ private void registerSubCommands(String parentLabel, List> subcomma
/**
* Unregister the subcommands of a command.
- * @param parentLabel The parent label of the subcommands.
+ *
+ * @param parentLabel The parent label of the subcommands.
* @param subcommandsList The list of subcommands to unregister.
*/
- private void unregisterSubCommands(String parentLabel, List> subcommandsList) {
- if(subcommandsList == null || subcommandsList.isEmpty()) {
+ private void unregisterSubCommands(String parentLabel, List> subcommandsList) {
+ if (subcommandsList == null || subcommandsList.isEmpty()) {
return;
}
- for (Command subcommand : subcommandsList) {
- // getAliases() already returns [name, ...aliases], so no need to add the name again
- List aliasesSub = new ArrayList<>(subcommand.getAliases());
- for (String aliasSub : aliasesSub) {
- this.removeCommand(parentLabel + "." + aliasSub, true);
- this.unregisterSubCommands(parentLabel + "." + aliasSub, subcommand.getSubcommands());
+ for (Command subcommand : subcommandsList) {
+ List labelsSub = subcommand.getAllLabels();
+ for (String labelSub : labelsSub) {
+ this.removeCommand(parentLabel + "." + labelSub, true);
+ this.unregisterSubCommands(parentLabel + "." + labelSub, subcommand.getSubcommands());
}
}
}
/**
* Unregister a command in the command manager.
- * @param label The label of the command.
+ *
+ * @param label The label of the command.
* @param subcommand If the subcommand must be unregistered.
*/
private void removeCommand(String label, boolean subcommand) {
@@ -339,25 +348,32 @@ private void removeCommand(String label, boolean subcommand) {
this.completers.remove(label);
}
+ /**
+ * Create a new command builder bound to this manager.
+ * This allows for a fluent API without specifying generic types.
+ *
+ * @param name the command name
+ * @return a new command builder
+ */
+ public CommandBuilder command(String name) {
+ return new CommandBuilder<>(this, name);
+ }
+
/**
* Register a command in the command manager.
+ *
* @param command The command to register.
- * @param label The label of the command.
- * @throws TypeArgumentNotExistException If the type of the argument does not exist.
+ * @param label The label of the command.
*/
- private void addCommand(Command command, String label) throws TypeArgumentNotExistException {
- if(this.isDebug()) {
+ private void addCommand(Command command, String label) {
+ if (this.isDebug()) {
this.logger.info("Register command " + label);
}
List> args = command.getArgs();
- List> optArgs = command.getOptinalArgs();
+ List> optArgs = command.getOptionalArgs();
String[] labelParts = label.split("\\.");
int labelSize = labelParts.length;
- if(!this.checkTypeForArgs(args) || !this.checkTypeForArgs(optArgs)) {
- throw new TypeArgumentNotExistException();
- }
-
command.setManager(this);
this.platform.addCommand(command, label);
commands.addCommand(label, command);
@@ -369,6 +385,7 @@ private void addCommand(Command command, String label) throws TypeArgumentN
/**
* Register the completions of the command.
+ *
* @param labelParts The parts of the label.
*/
private void addCompletionsForLabel(String[] labelParts) {
@@ -385,23 +402,22 @@ private void addCompletionsForLabel(String[] labelParts) {
/**
* Register the completions of the arguments.
- * @param label The label of the command.
+ *
+ * @param label The label of the command.
* @param commandSize The size of the command.
- * @param args The arguments to register.
+ * @param args The arguments to register.
*/
+ @SuppressWarnings({"unchecked", "rawtypes"})
private void addCompletionForArgs(String label, int commandSize, List> args) {
for (int i = 0; i < args.size(); i++) {
Argument arg = args.get(i);
- String[] parts = arg.arg().split(TYPE_PARSER);
- String type = parts[1].trim();
- ArgumentConverter> converter = this.typeConverters.get(type).getValue();
- TabCompleter argConverter = arg.tabConverter();
+ String type = arg.type().key();
+ ArgumentConverter.Wrapper> entry = this.typeConverters.get(type);
+ TabCompleter argConverter = arg.tabCompleter();
if (argConverter != null) {
- this.addCompletion(label,commandSize + i, argConverter);
- } else if (converter instanceof TabCompleter) {
- @SuppressWarnings("unchecked")
- TabCompleter tabCompleter = (TabCompleter) converter;
- this.addCompletion(label,commandSize + i, tabCompleter);
+ this.addCompletion(label, commandSize + i, argConverter);
+ } else if (entry != null && entry.converter() instanceof TabCompleter completer) {
+ this.addCompletion(label, commandSize + i, (TabCompleter) completer);
} else {
this.addCompletion(label, commandSize + i, (s, argsInner) -> new ArrayList<>());
}
@@ -410,9 +426,10 @@ private void addCompletionForArgs(String label, int commandSize, List converter) {
Map> mapInner = this.completers.getOrDefault(label, new HashMap<>());
@@ -421,9 +438,9 @@ private void addCompletion(String label, int commandSize, TabCompleter conver
TabCompleter existing = mapInner.get(commandSize);
if (existing != null) {
- combined = (s,args) -> {
- List completions = new ArrayList<>(existing.onCompletion(s,args));
- completions.addAll(converter.onCompletion(s,args));
+ combined = (s, args) -> {
+ List completions = new ArrayList<>(existing.onCompletion(s, args));
+ completions.addAll(converter.onCompletion(s, args));
return completions;
};
} else {
@@ -434,83 +451,9 @@ private void addCompletion(String label, int commandSize, TabCompleter conver
this.completers.put(label, mapInner);
}
- /**
- * Check if the type of the argument exists.
- * @param args The arguments to check.
- */
- private boolean checkTypeForArgs(List> args) throws TypeArgumentNotExistException {
- for(String arg: args.stream().map(Argument::arg).collect(Collectors.toList())) {
- String[] parts = arg.split(TYPE_PARSER);
-
- if (parts.length != 2) {
- throw new TypeArgumentNotExistException();
- }
- String type = parts[1].trim();
- if(!this.typeExist(type)) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Check if the type of the argument exists.
- * @param type The type of the argument.
- * @return If the type of the argument exists.
- */
- private boolean typeExist(String type) {
- return this.typeConverters.containsKey(type);
- }
-
- /**
- * Apply the parsing of the arguments.
- * @param args The arguments to parse.
- * @param arguments The arguments parsed.
- * @param templates The templates of the arguments.
- * @param argIndex The index of the argument.
- * @param input The input of the argument.
- * @return If the parsing is applied.
- * @throws TypeArgumentNotExistException If the type of the argument does not exist.
- * @throws ArgumentIncorrectException If the argument is incorrect.
- */
- private boolean applyParsing(String[] args, Arguments arguments, List> templates, int argIndex,
- String input) throws TypeArgumentNotExistException, ArgumentIncorrectException {
- String template = templates.get(argIndex).arg();
- String[] parts = template.split(TYPE_PARSER);
-
- if (parts.length != 2) {
- throw new TypeArgumentNotExistException();
- }
-
- String key = parts[0].trim();
- String type = parts[1].trim();
-
- if (type.equals(INFINITE)) {
- StringBuilder builder = new StringBuilder();
- for (int i = argIndex; i < args.length; i++) {
- builder.append(args[i]);
- if (i < args.length - 1) {
- builder.append(" ");
- }
- }
- arguments.add(key, String.class, builder.toString());
- return true;
- }
-
- if (typeConverters.containsKey(type)) {
- Class> typeClass = typeConverters.get(type).getKey();
- ArgumentConverter> converter = typeConverters.get(type).getValue();
- Object obj = converter.apply(input);
- if (obj == null) {
- throw new ArgumentIncorrectException(input);
- }
- arguments.add(key, typeClass, obj);
- }
- return false;
- }
-
/**
* Get the command invoker of the command manager.
+ *
* @return The command invoker of the command manager.
*/
public CommandInvoker getInvoker() {
@@ -521,11 +464,10 @@ public CommandInvoker getInvoker() {
* Register the internal converters of the command manager.
*/
private void registerInternalConverters() {
- this.registerConverter(String.class, (s) -> s);
+ this.registerConverter(String.class, (s) -> s);
this.registerConverter(Boolean.class, new BooleanArgument<>());
this.registerConverter(Integer.class, new IntegerArgument());
this.registerConverter(Double.class, new DoubleArgument());
- this.registerConverter(Long.class, new LongArgument());
- this.registerConverter(String.class, INFINITE, s -> s);
+ this.registerConverter(Long.class, new LongArgument());
}
}
diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java b/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java
index 46b9f00..a287103 100644
--- a/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java
+++ b/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java
@@ -1,55 +1,59 @@
package fr.traqueur.commands.api.arguments;
+import java.util.Objects;
+
/**
* The class Argument.
* This class is used to represent an argument of a command.
- * @param The type of the sender that will use this argument.
+ *
+ * @param The type of the sender that will use this argument.
+ * @param name The argument name.
+ *
+ * This is the name of the argument that will be used in the command.
+ *
+ * @param tabCompleter The tab completer for this argument.
+ *
+ * This is used to provide tab completion for the argument.
+ *
*/
-public class Argument {
-
- /**
- * The argument name.
- *
- * This is the name of the argument that will be used in the command.
- *
- */
- private final String arg;
-
- /**
- * The tab completer for this argument.
- *
- * This is used to provide tab completion for the argument.
- *
- */
- private final TabCompleter tabCompleter;
+public record Argument(String name, ArgumentType type, TabCompleter tabCompleter) {
/**
* Constructor for Argument.
*
- * @param arg The argument name.
+ * @param name The argument name.
+ * @param type The argument type.
* @param tabCompleter The tab completer for this argument.
*/
- public Argument(String arg, TabCompleter tabCompleter) {
- this.arg = arg;
+ public Argument(String name, ArgumentType type, TabCompleter tabCompleter) {
+ this.name = Objects.requireNonNull(name, "Argument name cannot be null");
+ this.type = Objects.requireNonNull(type, "Argument type cannot be null");
this.tabCompleter = tabCompleter;
}
+
/**
- * Get the argument name.
+ * Create an argument without tab completer.
*
- * @return The argument name.
+ * @param name the argument name
+ * @param type the argument type
*/
- public String arg() {
- return this.arg;
+ public Argument(String name, ArgumentType type) {
+ this(name, type, null);
}
+ public String canonicalName() {
+ return this.name + ":" + this.type.key();
+ }
+
+
/**
- * Get the tab completer for this argument.
+ * Check if this argument is infinite.
*
- * @return The tab completer.
+ * @return true if infinite type
*/
- public TabCompleter tabConverter() {
- return this.tabCompleter;
+ public boolean isInfinite() {
+ return type.isInfinite();
}
}
diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java
index e81e726..51d9990 100644
--- a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java
+++ b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java
@@ -5,6 +5,7 @@
/**
* The class ArgumentConverter.
* This class is used to convert a string to an object.
+ *
* @param The type of the object.
*/
@FunctionalInterface
@@ -12,9 +13,24 @@ public interface ArgumentConverter extends Function {
/**
* Apply the conversion.
+ *
* @param s The string to convert.
* @return The object.
*/
@Override
T apply(String s);
+
+ record Wrapper(Class clazz, ArgumentConverter converter) {
+
+ public boolean convertAndApply(String input, String name, Arguments arguments) {
+ T result = converter.apply(input);
+ if (result == null) {
+ return false;
+ }
+ arguments.add(name, clazz, result);
+ return true;
+ }
+
+ }
+
}
diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java
new file mode 100644
index 0000000..55c958d
--- /dev/null
+++ b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java
@@ -0,0 +1,41 @@
+package fr.traqueur.commands.api.arguments;
+
+public sealed interface ArgumentType permits ArgumentType.Simple, ArgumentType.Infinite {
+
+ static ArgumentType of(Class> clazz) {
+ if (clazz.isAssignableFrom(fr.traqueur.commands.api.arguments.Infinite.class)) {
+ return Infinite.INSTANCE;
+ }
+ return new Simple(clazz);
+ }
+
+ String key();
+
+ /**
+ * Check if this is the infinite type.
+ *
+ * @return true if infinite
+ */
+ default boolean isInfinite() {
+ return this instanceof Infinite;
+ }
+
+ record Simple(Class> clazz) implements ArgumentType {
+
+ @Override
+ public String key() {
+ return clazz.getSimpleName().toLowerCase();
+ }
+
+ }
+
+ record Infinite() implements ArgumentType {
+ public static final Infinite INSTANCE = new Infinite();
+
+ @Override
+ public String key() {
+ return "infinite";
+ }
+ }
+
+}
diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentValue.java b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentValue.java
index f74a424..7ee15a8 100644
--- a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentValue.java
+++ b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentValue.java
@@ -3,35 +3,19 @@
/**
* Represents a value of an argument with its type.
* This class is used to store the type and value of an argument.
+ *
+ * @param type The type of the argument.
+ * @param value The value of the argument.
*/
-public class ArgumentValue {
-
- /**
- * The type of the argument.
- */
- private final Class> type;
- /**
- * The value of the argument.
- */
- private final Object value;
-
- /**
- * Constructor to create an ArgumentValue with a specified type and value.
- *
- * @param type The class type of the argument.
- * @param value The value of the argument.
- */
- public ArgumentValue(Class> type, Object value) {
- this.type = type;
- this.value = value;
- }
+public record ArgumentValue(Class> type, Object value) {
/**
* Get the type of the argument.
*
* @return The type of the argument.
*/
- public Class> getType() {
+ @Override
+ public Class> type() {
return this.type;
}
@@ -40,7 +24,8 @@ public Class> getType() {
*
* @return The value of the argument.
*/
- public Object getValue() {
+ @Override
+ public Object value() {
return this.value;
}
diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java b/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java
index 07f98d0..53200f3 100644
--- a/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java
+++ b/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java
@@ -4,9 +4,8 @@
import fr.traqueur.commands.api.exceptions.NoGoodTypeArgumentException;
import fr.traqueur.commands.api.logging.Logger;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
+import java.util.function.BiConsumer;
/**
* This class is used to store arguments.
@@ -25,6 +24,7 @@ public class Arguments {
/**
* Constructor of the class.
+ *
* @param logger The logger of the class.
*/
public Arguments(Logger logger) {
@@ -32,323 +32,61 @@ public Arguments(Logger logger) {
this.logger = logger;
}
- /**
- * Get an argument from the map.
- *
- * @param argument The key of the argument.
- * @param The type of the argument.
- * @return The argument.
- */
- public T get(String argument) {
- try {
- Optional value = this.getOptional(argument);
- if (!value.isPresent()) {
- throw new ArgumentNotExistException();
- }
- return value.get();
- } catch (ArgumentNotExistException e) {
- logger.error("The argument " + argument + " does not exist.");
- logger.error(e.getMessage());
- }
- return null;
- }
-
- /**
- * Get an argument from the map as an integer.
- *
- * @param argument The key of the argument.
- * @param defaultValue The default value to return if the argument is not present.
- * @return The integer or the default value if not present.
- */
- public int getAsInt(String argument, int defaultValue) {
- Optional value = this.getAsInt(argument);
- return value.orElse(defaultValue);
- }
-
- /**
- * Get an argument from the map as a double.
- *
- * @param argument The key of the argument.
- * @param defaultValue The default value to return if the argument is not present.
- * @return The double or the default value if not present.
- */
- public double getAsDouble(String argument, double defaultValue) {
- Optional value = this.getAsDouble(argument);
- return value.orElse(defaultValue);
- }
-
- /**
- * Get an argument from the map as a boolean.
- *
- * @param argument The key of the argument.
- * @param defaultValue The default value to return if the argument is not present.
- * @return The boolean or the default value if not present.
- */
- public boolean getAsBoolean(String argument, boolean defaultValue) {
- Optional value = this.getAsBoolean(argument);
- return value.orElse(defaultValue);
- }
-
- /**
- * Get an argument from the map as a string.
- *
- * @param argument The key of the argument.
- * @param defaultValue The default value to return if the argument is not present.
- * @return The string or the default value if not present.
- */
- public String getAsString(String argument, String defaultValue) {
- Optional value = this.getAsString(argument);
- return value.orElse(defaultValue);
- }
-
- /**
- * Get an argument from the map as a long.
- *
- * @param argument The key of the argument.
- * @param defaultValue The default value to return if the argument is not present.
- * @return The long or the default value if not present.
- */
- public long getAsLong(String argument, long defaultValue) {
- Optional value = this.getAsLong(argument);
- return value.orElse(defaultValue);
- }
-
- /**
- * Get an argument from the map as a float.
- *
- * @param argument The key of the argument.
- * @param defaultValue The default value to return if the argument is not present.
- * @return The float or the default value if not present.
- */
- public float getAsFloat(String argument, float defaultValue) {
- Optional value = this.getAsFloat(argument);
- return value.orElse(defaultValue);
+ public Map toMap() {
+ Map result = new HashMap<>();
+ arguments.forEach((k, v) -> result.put(k, v.value()));
+ return result;
}
- /**
- * Get an argument from the map as a short.
- *
- * @param argument The key of the argument.
- * @param defaultValue The default value to return if the argument is not present.
- * @return The short or the default value if not present.
- */
- public short getAsShort(String argument, short defaultValue) {
- Optional value = this.getAsShort(argument);
- return value.orElse(defaultValue);
+ public int size() {
+ return arguments.size();
}
- /**
- * Get an argument from the map as a byte.
- *
- * @param argument The key of the argument.
- * @param defaultValue The default value to return if the argument is not present.
- * @return The byte or the default value if not present.
- */
- public byte getAsByte(String argument, byte defaultValue) {
- Optional value = this.getAsByte(argument);
- return value.orElse(defaultValue);
+ public boolean isEmpty() {
+ return arguments.isEmpty();
}
- /**
- * Get an argument from the map as a character.
- *
- * @param argument The key of the argument.
- * @param defaultValue The default value to return if the argument is not present.
- * @return The character or the default value if not present.
- */
- public char getAsChar(String argument, char defaultValue) {
- Optional value = this.getAsChar(argument);
- return value.orElse(defaultValue);
+ public Set getKeys() {
+ return Collections.unmodifiableSet(arguments.keySet());
}
- /**
- * Get an argument from the map as an integer.
- *
- * @param argument The key of the argument.
- * @return The integer or empty if not present.
- */
- public Optional getAsInt(String argument) {
- try {
- return this.getAs(argument, String.class).map(Integer::parseInt);
- } catch (NumberFormatException e) {
- return Optional.empty();
- }
+ public void forEach(BiConsumer action) {
+ arguments.forEach((k, v) -> action.accept(k, v.value()));
}
/**
- * Get an argument from the map as a double.
- *
- * @param argument The key of the argument.
- * @return The double or empty if not present.
- */
- public Optional getAsDouble(String argument) {
- try {
- return this.getAs(argument, String.class).map(Double::parseDouble);
- } catch (NumberFormatException e) {
- return Optional.empty();
- }
- }
-
- /**
- * Get an argument from the map as a boolean.
- *
- * @param argument The key of the argument.
- * @return The boolean or empty if not present.
- */
- public Optional getAsBoolean(String argument) {
- return this.getAs(argument, String.class).map(Boolean::parseBoolean);
- }
-
- /**
- * Get an argument from the map as a string.
- *
- * @param argument The key of the argument.
- * @return The string or empty if not present.
- */
- public Optional getAsString(String argument) {
- return this.getAs(argument, String.class);
- }
-
- /**
- * Get an argument from the map as a long.
- *
- * @param argument The key of the argument.
- * @return The long or empty if not present.
- */
- public Optional getAsLong(String argument) {
- try {
- return this.getAs(argument, String.class).map(Long::parseLong);
- } catch (NumberFormatException e) {
- return Optional.empty();
- }
- }
-
- /**
- * Get an argument from the map as a float.
- *
- * @param argument The key of the argument.
- * @return The float or empty if not present.
- */
- public Optional getAsFloat(String argument) {
- try {
- return this.getAs(argument, String.class).map(Float::parseFloat);
- } catch (NumberFormatException e) {
- return Optional.empty();
- }
- }
-
- /**
- * Get an argument from the map as a short.
- *
- * @param argument The key of the argument.
- * @return The short or empty if not present.
- */
- public Optional getAsShort(String argument) {
- try {
- return this.getAs(argument, String.class).map(Short::parseShort);
- } catch (NumberFormatException e) {
- return Optional.empty();
- }
- }
-
- /**
- * Get an argument from the map as a byte.
- *
- * @param argument The key of the argument.
- * @return The byte or empty if not present.
- */
- public Optional getAsByte(String argument) {
- try {
- return this.getAs(argument, String.class).map(Byte::parseByte);
- } catch (NumberFormatException e) {
- return Optional.empty();
- }
- }
-
- /**
- * Get an argument from the map as a character.
- *
- * @param argument The key of the argument.
- * @return The character or empty if not present.
- */
- public Optional getAsChar(String argument) {
- return this.getAs(argument, String.class).map(s -> s.charAt(0));
- }
-
- /**
- * Get an argument from the map as a specific type.
- *
- * @param argument The key of the argument.
- * @param typeRef The type of the argument.
- * @param defaultValue The default value to return if the argument is not present.
- * @param The type of the argument.
- * @return The argument or the default value if not present.
- */
- public T getAs(String argument, Class typeRef, T defaultValue) {
- Optional value = this.getAs(argument, typeRef);
- return value.orElse(defaultValue);
- }
-
- /**
- * Get an argument from the map as optional.
+ * Get an argument from the map.
*
* @param argument The key of the argument.
- * @param typeRef The type of the argument.
- * @param The type of the argument.
+ * @param The type of the argument.
* @return The argument.
*/
- public Optional getAs(String argument, Class typeRef) {
- if(typeRef.isPrimitive()) {
- throw new IllegalArgumentException("The type " + typeRef.getName() + " is a primitive type. You must use the primitive methode");
- }
- if(this.arguments.isEmpty()) {
- return Optional.empty();
- }
-
- ArgumentValue argumentValue = this.arguments.getOrDefault(argument, null);
-
- if(argumentValue == null) {
- return Optional.empty();
- }
-
- Class> type = argumentValue.getType();
- Object value = argumentValue.getValue();
-
- try {
- if(!typeRef.isAssignableFrom(type)) {
- throw new NoGoodTypeArgumentException();
- }
- if (!typeRef.isInstance(value)) {
- throw new NoGoodTypeArgumentException();
- }
- } catch (NoGoodTypeArgumentException e) {
- logger.error("The argument " + argument + " is not the good type.");
- return Optional.empty();
- }
-
- return Optional.of(typeRef.cast(value));
+ public T get(String argument) {
+ return this.getOptional(argument).orElseThrow(ArgumentNotExistException::new);
}
+
/**
* Get an argument from the map as optional.
*
* @param argument The key of the argument.
- * @param The type of the argument.
+ * @param The type of the argument.
* @return The argument.
*/
+ @SuppressWarnings("unchecked")
public Optional getOptional(String argument) {
- if(this.arguments.isEmpty()) {
+ if (this.isEmpty()) {
return Optional.empty();
}
ArgumentValue argumentValue = this.arguments.getOrDefault(argument, null);
- if(argumentValue == null) {
+ if (argumentValue == null) {
return Optional.empty();
}
- Class> type = argumentValue.getType();
- Object value = argumentValue.getValue();
+ Class> type = argumentValue.type();
+ Object value = argumentValue.value();
Class goodType = (Class) type;
try {
@@ -366,11 +104,11 @@ public Optional getOptional(String argument) {
/**
* Add an argument to the map.
*
- * @param key The key of the argument.
- * @param type The type of the argument.
+ * @param key The key of the argument.
+ * @param type The type of the argument.
* @param object The object of the argument.
*/
- public void add(String key, Class> type, Object object) {
+ public void add(String key, Class type, T object) {
ArgumentValue argumentValue = new ArgumentValue(type, object);
this.arguments.put(key, argumentValue);
}
diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/Infinite.java b/core/src/main/java/fr/traqueur/commands/api/arguments/Infinite.java
new file mode 100644
index 0000000..c0e3843
--- /dev/null
+++ b/core/src/main/java/fr/traqueur/commands/api/arguments/Infinite.java
@@ -0,0 +1,4 @@
+package fr.traqueur.commands.api.arguments;
+
+public interface Infinite {
+}
diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/TabCompleter.java b/core/src/main/java/fr/traqueur/commands/api/arguments/TabCompleter.java
index 231007f..a9170d7 100644
--- a/core/src/main/java/fr/traqueur/commands/api/arguments/TabCompleter.java
+++ b/core/src/main/java/fr/traqueur/commands/api/arguments/TabCompleter.java
@@ -5,8 +5,9 @@
/**
* The class TabConverter.
*
- * This class is used to represent a tabulation command converter.
+ * This class is used to represent a tabulation command converter.
*
+ *
* @param The type of the sender that will use this tab completer.
*/
@FunctionalInterface
@@ -15,8 +16,9 @@ public interface TabCompleter {
/**
* This method is called when the tabulation is used.
* It is used to get the completion of the command.
+ *
* @param sender The sender that will use this tab completer.
- * @param args The arguments of the command.
+ * @param args The arguments of the command.
* @return The completion of the command.
*/
List onCompletion(S sender, List args);
diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgsWithInfiniteArgumentException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgsWithInfiniteArgumentException.java
index 16d5b1f..6413e57 100644
--- a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgsWithInfiniteArgumentException.java
+++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgsWithInfiniteArgumentException.java
@@ -7,6 +7,7 @@ public class ArgsWithInfiniteArgumentException extends Exception {
/**
* Create a new instance of the exception with the default message.
+ *
* @param optional if the argument is optional
*/
public ArgsWithInfiniteArgumentException(boolean optional) {
diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentIncorrectException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentIncorrectException.java
index 917a748..6d78018 100644
--- a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentIncorrectException.java
+++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentIncorrectException.java
@@ -3,7 +3,7 @@
/**
* Exception thrown when an argument is incorrect.
*/
-public class ArgumentIncorrectException extends Exception {
+public class ArgumentIncorrectException extends RuntimeException {
/**
* The input that caused the exception.
@@ -22,6 +22,7 @@ public ArgumentIncorrectException(String input) {
/**
* Get the input that caused the exception.
+ *
* @return The input.
*/
public String getInput() {
diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentNotExistException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentNotExistException.java
index 35f105a..98ac597 100644
--- a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentNotExistException.java
+++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentNotExistException.java
@@ -3,7 +3,7 @@
/**
* This exception is thrown when the argument does not exist.
*/
-public class ArgumentNotExistException extends Exception {
+public class ArgumentNotExistException extends RuntimeException {
/**
* Create a new instance of the exception with the default message.
diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java
index d0ad43b..f1814e3 100644
--- a/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java
+++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java
@@ -19,7 +19,7 @@ public CommandRegistrationException(String message) {
* Constructs a new exception with the specified detail message and cause.
*
* @param message the detail message
- * @param cause the cause of the exception
+ * @param cause the cause of the exception
*/
public CommandRegistrationException(String message, Throwable cause) {
super(message, cause);
diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/TypeArgumentNotExistException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/TypeArgumentNotExistException.java
index d27e073..a28df77 100644
--- a/core/src/main/java/fr/traqueur/commands/api/exceptions/TypeArgumentNotExistException.java
+++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/TypeArgumentNotExistException.java
@@ -3,7 +3,7 @@
/**
* Exception thrown when an type of an argument is not found.
*/
-public class TypeArgumentNotExistException extends Exception {
+public class TypeArgumentNotExistException extends RuntimeException {
/**
* Constructs a new exception with the default message.
diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java
index 6fc453a..58d91ec 100644
--- a/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java
+++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java
@@ -19,7 +19,7 @@ public UpdaterInitializationException(String message) {
* Constructs a new exception with the specified detail message and cause.
*
* @param message the detail message
- * @param cause the cause of the exception
+ * @param cause the cause of the exception
*/
public UpdaterInitializationException(String message, Throwable cause) {
super(message, cause);
diff --git a/core/src/main/java/fr/traqueur/commands/api/logging/Logger.java b/core/src/main/java/fr/traqueur/commands/api/logging/Logger.java
index 96e38ee..a44b013 100644
--- a/core/src/main/java/fr/traqueur/commands/api/logging/Logger.java
+++ b/core/src/main/java/fr/traqueur/commands/api/logging/Logger.java
@@ -7,12 +7,14 @@ public interface Logger {
/**
* Logs an error message.
+ *
* @param message The message to log.
*/
void error(String message);
/**
* Logs an information message.
+ *
* @param message The message to log.
*/
void info(String message);
diff --git a/core/src/main/java/fr/traqueur/commands/api/logging/MessageHandler.java b/core/src/main/java/fr/traqueur/commands/api/logging/MessageHandler.java
index b457e66..27a8896 100644
--- a/core/src/main/java/fr/traqueur/commands/api/logging/MessageHandler.java
+++ b/core/src/main/java/fr/traqueur/commands/api/logging/MessageHandler.java
@@ -3,32 +3,43 @@
/**
* The class MessageHandler.
*
- * This class is used to represent a message handler.
+ * This class is used to represent a message handler.
*
*/
public interface MessageHandler {
/**
* This method is used to get the no permission message.
+ *
* @return The no permission message.
*/
String getNoPermissionMessage();
/**
* This method is used to get the only in game message.
+ *
* @return The only in game message.
*/
String getOnlyInGameMessage();
/**
* This method is used to get the arg not recognized message.
+ *
* @return The arg not recognized message.
*/
String getArgNotRecognized();
/**
* This method is used to get the requirement message.
+ *
* @return The requirement message.
*/
String getRequirementMessage();
+
+ /**
+ * This method is used to get the command disabled message.
+ *
+ * @return The command disabled message.
+ */
+ String getCommandDisabledMessage();
}
diff --git a/core/src/main/java/fr/traqueur/commands/api/models/Command.java b/core/src/main/java/fr/traqueur/commands/api/models/Command.java
index 22eeb00..24e5943 100644
--- a/core/src/main/java/fr/traqueur/commands/api/models/Command.java
+++ b/core/src/main/java/fr/traqueur/commands/api/models/Command.java
@@ -1,64 +1,59 @@
package fr.traqueur.commands.api.models;
-import fr.traqueur.commands.api.arguments.Arguments;
import fr.traqueur.commands.api.CommandManager;
import fr.traqueur.commands.api.arguments.Argument;
+import fr.traqueur.commands.api.arguments.ArgumentType;
+import fr.traqueur.commands.api.arguments.Arguments;
import fr.traqueur.commands.api.arguments.TabCompleter;
-import fr.traqueur.commands.api.exceptions.ArgsWithInfiniteArgumentException;
import fr.traqueur.commands.api.requirements.Requirement;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
/**
* This class is the base class for all commands.
* It contains all the necessary methods to create a command.
* It is abstract and must be inherited to be used.
+ *
* @param The plugin that owns the command.
* @param The type of the sender who use the command.
*/
public abstract class Command {
- private CommandManager manager;
-
+ private static final Pattern DOT_PATTERN = Pattern.compile("\\.");
/**
* The plugin that owns the command.
*/
private final T plugin;
-
/**
* The name of the command.
*/
private final String name;
-
/**
* The aliases of the command.
*/
private final List aliases;
-
/**
* The subcommands of the command.
*/
private final List> subcommands;
-
/**
* The arguments of the command.
*/
private final List> args;
-
/**
* The optional arguments of the command.
*/
private final List> optionalArgs;
-
/**
* The requirements of the command.
*/
private final List> requirements;
-
+ private CommandManager manager;
/**
* The description of the command.
*/
@@ -84,6 +79,8 @@ public abstract class Command {
*/
private boolean infiniteArgs;
+ private boolean enable;
+
/**
* If the command is subcommand
*/
@@ -91,8 +88,9 @@ public abstract class Command {
/**
* The constructor of the command.
+ *
* @param plugin The plugin that owns the command.
- * @param name The name of the command.
+ * @param name The name of the command.
*/
public Command(T plugin, String name) {
this.plugin = plugin;
@@ -107,10 +105,12 @@ public Command(T plugin, String name) {
this.optionalArgs = new ArrayList<>();
this.requirements = new ArrayList<>();
this.subcommand = false;
+ this.enable = true;
}
/**
* This method is called to set the manager of the command.
+ *
* @param manager The manager of the command.
*/
public void setManager(CommandManager manager) {
@@ -119,7 +119,8 @@ public void setManager(CommandManager manager) {
/**
* This method is called when the command is executed.
- * @param sender The sender of the command.
+ *
+ * @param sender The sender of the command.
* @param arguments The arguments of the command.
*/
public abstract void execute(S sender, Arguments arguments);
@@ -133,10 +134,11 @@ public void unregister() {
/**
* This method is called to unregister the command.
+ *
* @param subcommands If the subcommands must be unregistered.
*/
public void unregister(boolean subcommands) {
- if(this.manager == null) {
+ if (this.manager == null) {
throw new IllegalArgumentException("The command is not registered.");
}
this.manager.unregisterCommand(this, subcommands);
@@ -144,6 +146,7 @@ public void unregister(boolean subcommands) {
/**
* This method is called to get the name of the command.
+ *
* @return The name of the command.
*/
public final String getName() {
@@ -152,44 +155,79 @@ public final String getName() {
/**
* This method is called to get the description of the command.
+ *
* @return The description of the command.
*/
public final String getDescription() {
return description;
}
+ /**
+ * This method is called to set the description of the command
+ *
+ * @param description The description of the command.
+ */
+ public final void setDescription(String description) {
+ this.description = description;
+ }
+
/**
* This method is called to get the permission of the command.
+ *
* @return The permission of the command.
*/
public final String getPermission() {
return permission;
}
+ /**
+ * This method is called to set the permission of the command.
+ *
+ * @param permission The permission of the command.
+ */
+ public final void setPermission(String permission) {
+ this.permission = permission;
+ }
+
/**
* This method is called to get the usage of the command.
+ *
* @return The usage of the command.
*/
public final String getUsage() {
return usage;
}
+ /**
+ * This method is called to set the usage of the command.
+ *
+ * @param usage The usage of the command.
+ */
+ public final void setUsage(String usage) {
+ this.usage = usage;
+ }
+
/**
* This method is called to get the aliases of the command.
+ *
* @return The aliases of the command.
*/
public final List getAliases() {
- List aliases = new ArrayList<>();
- aliases.add(name);
+ return Collections.unmodifiableList(aliases);
+ }
+
+ public final List getAllLabels() {
+ List labels = new ArrayList<>();
+ labels.add(name);
if (!this.aliases.isEmpty()) {
- aliases.addAll(this.aliases);
+ labels.addAll(this.aliases);
}
- return aliases;
+ return labels;
}
-
/**
* This method is called to get the subcommands of the command.
+ *
* @return The subcommands of the command.
*/
public final List> getSubcommands() {
@@ -198,6 +236,7 @@ public final List> getSubcommands() {
/**
* This method is called to get the arguments of the command.
+ *
* @return The arguments of the command.
*/
public final List> getArgs() {
@@ -206,14 +245,16 @@ public final List