From a7a8dfc506c9f3b9c448a4e00d63f6d0abd7eeff Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 23 Dec 2025 19:03:11 +0100 Subject: [PATCH 01/23] feat(arguments) : modernize argument system --- build.gradle | 4 - core/build.gradle | 4 +- .../traqueur/commands/api/CommandManager.java | 173 ++-------- .../commands/api/arguments/Argument.java | 62 ++-- .../api/arguments/ArgumentConverter.java | 14 + .../commands/api/arguments/ArgumentValue.java | 31 +- .../commands/api/arguments/Arguments.java | 308 ++---------------- .../ArgumentIncorrectException.java | 2 +- .../exceptions/ArgumentNotExistException.java | 2 +- .../TypeArgumentNotExistException.java | 2 +- .../traqueur/commands/api/models/Command.java | 158 +++------ .../commands/api/models/CommandInvoker.java | 52 ++- .../api/models/collections/CommandTree.java | 45 +-- .../commands/api/CommandManagerTest.java | 99 +++--- .../commands/api/arguments/ArgumentsTest.java | 94 +----- .../api/models/CommandInvokerTest.java | 84 +++-- .../commands/api/models/CommandTest.java | 128 +++++--- .../models/collections/CommandTreeTest.java | 25 +- .../logging/InternalMessageHandlerTest.java | 5 +- .../commands/test/commands/AdminCommand.java | 23 +- .../commands/test/commands/GreetCommand.java | 12 +- .../commands/test/commands/MathCommand.java | 18 +- .../test/commands/UserInfoCommand.java | 8 +- .../traqueur/commands/jda/CommandManager.java | 23 -- .../traqueur/commands/jda/JDAArguments.java | 184 ----------- .../fr/traqueur/commands/jda/JDAExecutor.java | 71 +++- .../fr/traqueur/commands/jda/JDAPlatform.java | 12 +- .../jda/arguments/AttachmentArgument.java | 50 --- .../jda/arguments/ChannelArgument.java | 56 ---- .../jda/arguments/JDAArgumentConverter.java | 49 --- .../jda/arguments/MemberArgument.java | 56 ---- .../commands/jda/arguments/RoleArgument.java | 56 ---- .../commands/jda/arguments/UserArgument.java | 60 ---- .../traqueur/testplugin/Sub2TestCommand.java | 2 +- .../traqueur/testplugin/SubTestCommand.java | 6 +- .../fr/traqueur/testplugin/TestCommand.java | 4 +- spigot/build.gradle | 4 +- .../velocityTestPlugin/Sub2TestCommand.java | 4 +- .../velocityTestPlugin/SubTestCommand.java | 6 +- .../velocityTestPlugin/TestCommand.java | 4 +- 40 files changed, 487 insertions(+), 1513 deletions(-) delete mode 100644 jda/src/main/java/fr/traqueur/commands/jda/arguments/AttachmentArgument.java delete mode 100644 jda/src/main/java/fr/traqueur/commands/jda/arguments/ChannelArgument.java delete mode 100644 jda/src/main/java/fr/traqueur/commands/jda/arguments/JDAArgumentConverter.java delete mode 100644 jda/src/main/java/fr/traqueur/commands/jda/arguments/MemberArgument.java delete mode 100644 jda/src/main/java/fr/traqueur/commands/jda/arguments/RoleArgument.java delete mode 100644 jda/src/main/java/fr/traqueur/commands/jda/arguments/UserArgument.java diff --git a/build.gradle b/build.gradle index eb10988..25ef688 100644 --- a/build.gradle +++ b/build.gradle @@ -6,10 +6,6 @@ allprojects { plugin 'java-library' } - tasks.withType(JavaCompile).configureEach { - options.compilerArgs += ['-nowarn'] - } - repositories { mavenCentral() maven { diff --git a/core/build.gradle b/core/build.gradle index 9f48b74..5bce398 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -4,8 +4,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 withSourcesJar() withJavadocJar() } 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..26e661b 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -5,7 +5,6 @@ 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; @@ -13,6 +12,9 @@ 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; @@ -22,7 +24,6 @@ import fr.traqueur.commands.impl.logging.InternalMessageHandler; import java.util.*; -import java.util.stream.Collectors; /** * This class is the command manager. @@ -33,18 +34,8 @@ */ 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 ArgumentParser parser; private final CommandPlatform platform; /** @@ -55,7 +46,7 @@ public abstract class CommandManager { /** * 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. @@ -96,6 +87,7 @@ public CommandManager(CommandPlatform platform) { this.typeConverters = new HashMap<>(); this.completers = new HashMap<>(); this.invoker = new CommandInvoker<>(this); + this.parser = new ArgumentParser<>(this.typeConverters); this.registerInternalConverters(); } @@ -145,13 +137,9 @@ public boolean isDebug() { * @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); + for (String alias : command.getAliases()) { + this.addCommand(command, alias); + this.registerSubCommands(alias, command.getSubcommands()); } } @@ -171,9 +159,9 @@ public void unregisterCommand(String label) { public void unregisterCommand(String label, boolean subcommands) { String[] rawArgs = label.split("\\."); Optional> commandOptional = this.commands.findNode(rawArgs) - .flatMap(result -> result.node.getCommand()); + .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); @@ -210,19 +198,7 @@ public void unregisterCommand(Command command, boolean subcommands) { * @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)); } /** @@ -234,26 +210,16 @@ public void registerConverter(Class typeClass, String type, ArgumentConv * @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; + ParseResult result = parser.parse(command, args, this.logger); + 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(); } /** @@ -293,9 +259,8 @@ public Logger getLogger() { * 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 { + private void registerSubCommands(String parentLabel, List> subcommands) { if(subcommands == null || subcommands.isEmpty()) { return; } @@ -343,9 +308,8 @@ private void removeCommand(String label, boolean subcommand) { * 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. */ - private void addCommand(Command command, String label) throws TypeArgumentNotExistException { + private void addCommand(Command command, String label) { if(this.isDebug()) { this.logger.info("Register command " + label); } @@ -354,10 +318,6 @@ private void addCommand(Command command, String label) throws TypeArgumentN 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); @@ -392,16 +352,13 @@ private void addCompletionsForLabel(String[] labelParts) { 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); + } 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<>()); } @@ -434,81 +391,6 @@ 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. @@ -526,6 +408,5 @@ private void registerInternalConverters() { 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); } } 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..a00f579 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 @@ -17,4 +17,18 @@ public interface ArgumentConverter extends Function { */ @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/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..59fb550 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. @@ -32,303 +31,40 @@ 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); - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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); - } - - /** - * 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(); - } - } - - /** - * 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); + 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 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(); - } + public int size() { + return arguments.size(); } - /** - * 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(); - } + public boolean isEmpty() { + return arguments.isEmpty(); } - /** - * 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)); + public Set getKeys() { + return Collections.unmodifiableSet(arguments.keySet()); } - /** - * 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); + public void forEach(BiConsumer action) { + arguments.forEach((k, v) -> action.accept(k, v.value())); } /** - * 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. * @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. * @@ -337,7 +73,7 @@ public Optional getAs(String argument, Class typeRef) { * @return The argument. */ public Optional getOptional(String argument) { - if(this.arguments.isEmpty()) { + if (this.isEmpty()) { return Optional.empty(); } @@ -347,8 +83,8 @@ public Optional getOptional(String argument) { return Optional.empty(); } - Class type = argumentValue.getType(); - Object value = argumentValue.getValue(); + Class type = argumentValue.type(); + Object value = argumentValue.value(); Class goodType = (Class) type; try { @@ -370,7 +106,7 @@ public Optional getOptional(String 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/exceptions/ArgumentIncorrectException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentIncorrectException.java index 917a748..adcd3a0 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. 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/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/models/Command.java b/core/src/main/java/fr/traqueur/commands/api/models/Command.java index 22eeb00..2abc942 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,17 +1,16 @@ 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.List; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * This class is the base class for all commands. @@ -300,35 +299,16 @@ public final void addSubCommand(Command... commands) { * @param args The arguments to add. */ public final void addArgs(Object... args) { - if (Arrays.stream(args).allMatch(arg -> arg instanceof String)) { - for (Object arg : args) { - String argStr = (String) arg; - this.addArgs(argStr); - } - return; - } - if (args.length % 2 != 0 && !(args[1] instanceof String)) { - throw new IllegalArgumentException("You must provide a type for the argument."); + if (args.length % 2 != 0) { + throw new IllegalArgumentException("You must use the method like succession of String,Class"); } for (int i = 0; i < args.length; i += 2) { - if(!(args[i] instanceof String && args[i + 1] instanceof Class)) { + if (!(args[i] instanceof String argName && args[i + 1] instanceof Class type)) { throw new IllegalArgumentException("You must provide a type for the argument."); } - this.addArgs((String) args[i], (Class) args[i + 1]); - } - } - - /** - * This method is called to add arguments to the command. - * @param arg The argument to add. - */ - public final void addArgs(String arg) { - if(!arg.contains(CommandManager.TYPE_PARSER)) { - this.addArgs(arg, String.class, null); - } else { - this.addArgs(arg, null, null); + this.addArg(argName, type); } } @@ -337,21 +317,8 @@ public final void addArgs(String arg) { * @param arg The argument to add. * @param type The type of the argument to add. */ - public final void addArgs(String arg, Class type) { - this.addArgs(arg, type,null); - } - - /** - * This method is called to add arguments to the command. - * @param arg The argument to add. - * @param converter The converter of the argument. - */ - public final void addArgs(String arg, TabCompleter converter) { - if(!arg.contains(CommandManager.TYPE_PARSER)) { - this.addArgs(arg, String.class, converter); - } else { - this.addArgs(arg, null, converter); - } + public final void addArg(String arg, Class type) { + this.addArg(arg, type, null); } /** @@ -360,19 +327,8 @@ public final void addArgs(String arg, TabCompleter converter) { * @param converter The converter of the argument. * @param type The type of the argument, can be null if the argument is a string. */ - public final void addArgs(String arg, Class type, TabCompleter converter) { - if (arg.contains(CommandManager.TYPE_PARSER) && type != null) { - throw new IllegalArgumentException("You can't use the type parser in the command arguments."); - } - if(type == null && !arg.contains(CommandManager.TYPE_PARSER)) { - throw new IllegalArgumentException("You must provide a type for the argument."); - } - - if(type != null) { - arg = arg + CommandManager.TYPE_PARSER + type.getSimpleName().toLowerCase(); - } - - this.add(arg, converter, false); + public final void addArg(String arg, Class type, TabCompleter converter) { + this.add(arg, ArgumentType.of(type), converter, false); } /** @@ -380,36 +336,16 @@ public final void addArgs(String arg, Class type, TabCompleter converter) * @param args The arguments to add. */ public final void addOptionalArgs(Object... args) { - if (Arrays.stream(args).allMatch(arg -> arg instanceof String)) { - for (Object arg : args) { - String argStr = (String) arg; - this.addOptionalArgs(argStr); - } - return; - } - - if (args.length % 2 != 0 && !(args[1] instanceof String)) { + if (args.length % 2 != 0) { throw new IllegalArgumentException("You must provide a type for the argument."); } for (int i = 0; i < args.length; i += 2) { - if(!(args[i] instanceof String && args[i + 1] instanceof Class)) { + if (!(args[i] instanceof String argName && args[i + 1] instanceof Class clazz)) { throw new IllegalArgumentException("You must provide a type for the argument."); } - this.addOptionalArgs((String) args[i], (Class) args[i + 1]); - } - } - - /** - * This method is called to add optional arguments to the command. - * @param arg The argument to add. - */ - public final void addOptionalArgs(String arg) { - if (!arg.contains(CommandManager.TYPE_PARSER)) { - this.addOptionalArgs(arg, String.class, null); - return; + this.addOptionalArg(argName, clazz); } - this.addOptionalArgs(arg, null, null); } /** @@ -417,8 +353,8 @@ public final void addOptionalArgs(String arg) { * @param arg The argument to add. * @param type The type of the argument to add. */ - public final void addOptionalArgs(String arg, Class type) { - this.addOptionalArgs(arg, type,null); + public final void addOptionalArg(String arg, Class type) { + this.addOptionalArg(arg, type, null); } /** @@ -426,47 +362,29 @@ public final void addOptionalArgs(String arg, Class type) { * @param arg The argument to add. * @param converter The converter of the argument. */ - public final void addOptionalArgs(String arg, TabCompleter converter) { - if (!arg.contains(CommandManager.TYPE_PARSER)) { - this.addOptionalArgs(arg, String.class, converter); - return; - } - this.addOptionalArgs(arg, null, converter); + public final void addOptionalArg(String arg, Class type, TabCompleter converter) { + this.add(arg, ArgumentType.of(type), converter, true); } - /** - * This method is called to add arguments to the command. - * @param arg The argument to add. - * @param type The type of the argument to add. - * @param converter The converter of the argument. - */ - public final void addOptionalArgs(String arg, Class type, TabCompleter converter) { - if (arg.contains(CommandManager.TYPE_PARSER) && type != null) { - throw new IllegalArgumentException("You can't use the type parser in the command arguments."); - } - if(type != null) { - arg = arg + CommandManager.TYPE_PARSER + type.getSimpleName().toLowerCase(); + private void add(String name, ArgumentType type, TabCompleter completer, boolean optional) { + if (this.infiniteArgs) { + if (this.manager != null) { + String msg = (optional ? "Optional arguments" : "Arguments") + + " cannot follow infinite arguments."; + this.manager.getLogger().error(msg); + } } - this.add(arg, converter, true); - } + if (type.isInfinite()) { + this.infiniteArgs = true; + } - private void add(String arg, TabCompleter converter, boolean opt) { - try { - if (this.infiniteArgs) { - throw new ArgsWithInfiniteArgumentException(false); - } + Argument arg = new Argument<>(name, type, completer); - if (arg.contains(":infinite")) { - this.infiniteArgs = true; - } - if(opt) { - this.optionalArgs.add(new Argument<>(arg, converter)); - } else { - this.args.add(new Argument<>(arg, converter)); - } - } catch (ArgsWithInfiniteArgumentException e) { - this.manager.getLogger().error(e.getMessage()); + if (optional) { + this.optionalArgs.add(arg); + } else { + this.args.add(arg); } } @@ -474,6 +392,7 @@ private void add(String arg, TabCompleter converter, boolean opt) { * This method is called to add requirements to the command. * @param requirement The requirements to add. */ + @SafeVarargs public final void addRequirements(Requirement... requirement) { requirements.addAll(Arrays.asList(requirement)); } @@ -496,12 +415,11 @@ public final T getPlugin() { /** * This method is called to generate a default usage for the command. - * @param platform The platform of the command. * @param sender The sender of the command. * @param label The label of the command. * @return The default usage of the command. */ - public String generateDefaultUsage(CommandPlatform platform, S sender, String label) { + public String generateDefaultUsage(S sender, String label) { StringBuilder usage = new StringBuilder("/"); String[] parts = label.split("\\."); @@ -510,9 +428,9 @@ public String generateDefaultUsage(CommandPlatform platform, S sender, Stri List> directSubs = this.getSubcommands().stream() .filter(sub -> { String perm = sub.getPermission(); - return perm.isEmpty() || platform.hasPermission(sender, perm); + return perm.isEmpty() || this.manager.getPlatform().hasPermission(sender, perm); }) - .collect(Collectors.toList()); + .toList(); if (!directSubs.isEmpty()) { usage.append(" <"); @@ -528,7 +446,7 @@ public String generateDefaultUsage(CommandPlatform platform, S sender, Stri // arguments obligatoires : String req = this.getArgs().stream() - .map(arg -> "<" + arg.arg() + ">") + .map(arg -> "<" + arg.canonicalName() + ">") .collect(Collectors.joining(" ")); usage.append(req); @@ -538,7 +456,7 @@ public String generateDefaultUsage(CommandPlatform platform, S sender, Stri usage.append(" "); } String opt = this.getOptinalArgs().stream() - .map(arg -> "[" + arg.arg() + "]") + .map(arg -> "[" + arg.canonicalName() + "]") .collect(Collectors.joining(" ")); usage.append(opt); } diff --git a/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java b/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java index 1aa3440..bf84508 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java @@ -5,7 +5,6 @@ import fr.traqueur.commands.api.arguments.TabCompleter; import fr.traqueur.commands.api.exceptions.ArgumentIncorrectException; import fr.traqueur.commands.api.exceptions.TypeArgumentNotExistException; -import fr.traqueur.commands.api.logging.MessageHandler; import fr.traqueur.commands.api.models.collections.CommandTree; import fr.traqueur.commands.api.models.collections.CommandTree.MatchResult; import fr.traqueur.commands.api.requirements.Requirement; @@ -42,7 +41,7 @@ public CommandInvoker(CommandManager manager) { */ public boolean invoke(S source, String base, String[] rawArgs) { Optional> contextOpt = findCommandContext(base, rawArgs); - if (!contextOpt.isPresent()) { + if (contextOpt.isEmpty()) { return false; } @@ -63,21 +62,21 @@ public boolean invoke(S source, String base, String[] rawArgs) { */ private Optional> findCommandContext(String base, String[] rawArgs) { Optional> found = manager.getCommands().findNode(base, rawArgs); - if (!found.isPresent()) { + if (found.isEmpty()) { return Optional.empty(); } MatchResult result = found.get(); - CommandTree.CommandNode node = result.node; + CommandTree.CommandNode node = result.node(); Optional> cmdOpt = node.getCommand(); - if (!cmdOpt.isPresent()) { + if (cmdOpt.isEmpty()) { return Optional.empty(); } Command command = cmdOpt.get(); String label = node.getFullLabel() != null ? node.getFullLabel() : base; - String[] args = result.args; + String[] args = result.args(); return Optional.of(new CommandContext<>(command, label, args)); } @@ -185,7 +184,7 @@ private String buildUsageMessage(S source, CommandContext context) { String label = context.label; return command.getUsage().isEmpty() - ? command.generateDefaultUsage(manager.getPlatform(), source, label) + ? command.generateDefaultUsage(source, label) : command.getUsage(); } @@ -229,21 +228,6 @@ private boolean handleArgumentIncorrectError(S source, ArgumentIncorrectExceptio return true; } - /** - * Internal context class to hold command execution data. - */ - private static class CommandContext { - final Command command; - final String label; - final String[] args; - - CommandContext(Command command, String label, String[] args) { - this.command = command; - this.label = label; - this.args = args; - } - } - /** * Suggests command completions based on the provided source, base label, and arguments. * This method checks for available tab completers and filters suggestions based on the current input. @@ -257,8 +241,8 @@ public List suggest(S source, String base, String[] args) { String lastArg = args.length > 0 ? args[args.length - 1] : ""; if (found.isPresent()) { MatchResult result = found.get(); - CommandTree.CommandNode node = result.node; - String[] rawArgs = result.args; + CommandTree.CommandNode node = result.node(); + String[] rawArgs = result.args(); String label = Optional.ofNullable(node.getFullLabel()).orElse(base); Map> map = manager.getCompleters().get(label); if (map != null) { @@ -288,6 +272,15 @@ public List suggest(S source, String base, String[] args) { .collect(Collectors.toList()); } + private boolean allowedSuggestion(S src, String label, String opt) { + String full = label + "." + opt.toLowerCase(); + Optional> copt = manager.getCommands().findNode(full.split("\\.")).flatMap(r -> r.node().getCommand()); + if (!copt.isPresent()) return true; + Command c = copt.get(); + return c.getRequirements().stream().allMatch(r -> r.check(src)) + && (c.getPermission().isEmpty() || manager.getPlatform().hasPermission(src, c.getPermission())); + } + private CommandTree.CommandNode traverseNode(CommandTree.CommandNode node, String[] args) { int index = 0; while (index < args.length - 1) { @@ -308,12 +301,9 @@ private boolean matchesPrefix(String candidate, String current) { return candidate.equalsIgnoreCase(current) || candidate.toLowerCase().startsWith(lower); } - private boolean allowedSuggestion(S src, String label, String opt) { - String full = label + "." + opt.toLowerCase(); - Optional> copt = manager.getCommands().findNode(full.split("\\.")).flatMap(r -> r.node.getCommand()); - if (!copt.isPresent()) return true; - Command c = copt.get(); - return c.getRequirements().stream().allMatch(r -> r.check(src)) - && (c.getPermission().isEmpty() || manager.getPlatform().hasPermission(src, c.getPermission())); + /** + * Internal context class to hold command execution data. + */ + private record CommandContext(Command command, String label, String[] args) { } } \ No newline at end of file diff --git a/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java b/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java index 64d0964..1302e94 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java @@ -10,33 +10,8 @@ * @param type of the command sender */ public class CommandTree { - /** - * Result of a lookup: the deepest matching node and leftover args. - * This is used to find commands based on a base label and raw arguments. - * @param type of the command context - * @param type of the command sender - */ - public static class MatchResult { - /** The node that matched the base label and any subcommands. - * The args are the remaining segments after the match. - */ - public final CommandNode node; - - /** Remaining arguments after the matched node. - * This can be empty if the match was exact. - */ - public final String[] args; - - /** Create a match result with the node and leftover args. - * @param node the matched command node - * @param args remaining arguments after the match - */ - public MatchResult(CommandNode node, String[] args) { - this.node = node; - this.args = args; - } - } + private CommandNode root; /** * A node representing one segment in the command path. @@ -92,7 +67,9 @@ public Map> getChildren() { } } - private final CommandNode root; + public void clear() { + this.root = new CommandNode<>(null, null); + } /** @@ -103,6 +80,20 @@ public CommandTree() { this.root = new CommandNode<>(null, null); } + /** + * Result of a lookup: the deepest matching node and leftover args. + * This is used to find commands based on a base label and raw arguments. + * + * @param type of the command context + * @param type of the command sender + * @param node The node that matched the base label and any subcommands. + * The args are the remaining segments after the match. + * @param args Remaining arguments after the matched node. + * This can be empty if the match was exact. + */ + public record MatchResult(CommandNode node, String[] args) { + } + /** * Add or replace a command at the given full label path (dot-separated). * @param label full path like "hello.sub" diff --git a/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java b/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java index 67a6714..1f01124 100644 --- a/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java @@ -1,31 +1,41 @@ package fr.traqueur.commands.api; import fr.traqueur.commands.api.arguments.Arguments; +import fr.traqueur.commands.api.arguments.Infinite; import fr.traqueur.commands.api.arguments.TabCompleter; import fr.traqueur.commands.api.exceptions.ArgumentIncorrectException; +import fr.traqueur.commands.api.exceptions.ArgumentNotExistException; import fr.traqueur.commands.api.models.Command; import fr.traqueur.commands.api.models.CommandPlatform; import fr.traqueur.commands.api.models.collections.CommandTree; import fr.traqueur.commands.impl.logging.InternalLogger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.contains; -import static org.mockito.Mockito.verify; class CommandManagerTest { - private InternalLogger logger; + private FakeLogger logger; private CommandManager manager; private FakePlatform platform; + @BeforeEach + void setUp() { + platform = new FakePlatform(); + manager = new CommandManager<>(platform) { + }; + platform.injectManager(manager); + logger = new FakeLogger(); + manager.setLogger(logger); + } + static class DummyCommand extends Command { DummyCommand() { super(null, "dummy"); } DummyCommand(String name) { super(null, name); } @@ -44,23 +54,16 @@ static class FakePlatform implements CommandPlatform { @Override public void removeCommand(String label, boolean sub) {} } - @BeforeEach - void setUp() { - platform = new FakePlatform(); - manager = new CommandManager(platform) {}; - platform.injectManager(manager); - logger = Mockito.mock(InternalLogger.class); - manager.setLogger(logger); - } - + // ----- TESTS ----- @Test void testInfiniteArgsParsing() throws Exception { - Command cmd = new Command(null, "test") { + Command cmd = new Command<>(null, "test") { @Override - public void execute(String sender, Arguments arguments) {} + public void execute(String sender, Arguments arguments) { + } }; cmd.setManager(manager); - cmd.addArgs("rest:infinite"); + cmd.addArgs("rest", Infinite.class); String[] input = {"one", "two", "three", "four"}; Arguments args = manager.parse(cmd, input); @@ -75,12 +78,12 @@ void testInfiniteArgsStopsFurtherParsing() throws Exception { Command cmd = new DummyCommand(); cmd.setManager(manager); cmd.addArgs("first", String.class); - cmd.addArgs("rest:infinite"); + cmd.addArgs("rest", Infinite.class); String[] input = {"A", "B", "C", "D"}; Arguments args = manager.parse(cmd, input); - assertEquals("A", args.getAsString("first", null)); + assertEquals("A", args.get("first")); assertEquals("B C D", args.get("rest")); } @@ -88,15 +91,30 @@ void testInfiniteArgsStopsFurtherParsing() throws Exception { void testNoExtraAfterInfinite() throws Exception { Command cmd = new DummyCommand(); cmd.setManager(manager); - cmd.addArgs("x:infinite"); - cmd.addArgs("y", String.class); - verify(logger).error("Arguments cannot follow infinite arguments."); + cmd.addArg("x", Infinite.class); + cmd.addArg("y", String.class); String[] input = {"v1", "v2"}; Arguments args = manager.parse(cmd, input); assertEquals("v1 v2", args.get("x")); - assertNull(args.get("y")); - verify(logger).error(contains("y")); + assertThrows(ArgumentNotExistException.class, () -> args.get("y")); + } + + @Test + void testOptionalArgs_onlyDefault() throws Exception { + Command cmd = new DummyCommand(); + cmd.addArgs("req", String.class); + cmd.addOptionalArgs("opt1", Integer.class); + cmd.addOptionalArgs("opt2", Double.class); + + String[] input = {"reqValue"}; + Arguments args = manager.parse(cmd, input); + assertEquals("reqValue", args.get("req")); + + assertFalse(args.getOptional("opt1").isPresent()); + assertFalse(args.getOptional("opt2").isPresent()); + assertEquals(0, args.getOptional("opt1").orElse(0)); + assertEquals(0.0, args.getOptional("opt2").orElse(0.0)); } @Test @@ -116,20 +134,9 @@ void testBasicArgParsing_correctTypes() throws Exception { } @Test - void testOptionalArgs_onlyDefault() throws Exception { + void addArgs_withOddArgs_shouldThrow() { Command cmd = new DummyCommand(); - cmd.addArgs("req", String.class); - cmd.addOptionalArgs("opt1", Integer.class); - cmd.addOptionalArgs("opt2", Double.class); - - String[] input = {"reqValue"}; - Arguments args = manager.parse(cmd, input); - assertEquals("reqValue", args.getAsString("req", null)); - - assertFalse(args.getOptional("opt1").isPresent()); - assertFalse(args.getOptional("opt2").isPresent()); - assertEquals(0, args.getAsInt("opt1", 0)); - assertEquals(0.0, args.getAsDouble("opt2", 0.0)); + assertThrows(IllegalArgumentException.class, () -> cmd.addArgs("bad")); } @Test @@ -182,11 +189,21 @@ void addCommand_shouldRegisterCompletersForArgs() { assertTrue(map.containsKey(2)); } - @Test - void addCommand_withUnknownType_shouldThrow() { - Command cmd = new DummyCommand(); - cmd.addArgs("bad:typeparser"); - assertThrows(RuntimeException.class, () -> manager.registerCommand(cmd)); + static class FakeLogger extends InternalLogger { + private final List errors = new ArrayList<>(); + + public FakeLogger() { + super(Logger.getLogger("FakeLogger")); + } + + @Override + public void error(String message) { + errors.add(message); + } + + public List getErrors() { + return errors; + } } } diff --git a/core/src/test/java/fr/traqueur/commands/api/arguments/ArgumentsTest.java b/core/src/test/java/fr/traqueur/commands/api/arguments/ArgumentsTest.java index 681cdca..fb716eb 100644 --- a/core/src/test/java/fr/traqueur/commands/api/arguments/ArgumentsTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/arguments/ArgumentsTest.java @@ -1,93 +1,27 @@ package fr.traqueur.commands.api.arguments; + +import fr.traqueur.commands.api.exceptions.ArgumentNotExistException; import fr.traqueur.commands.impl.logging.InternalLogger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import java.util.Optional; +import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.contains; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; class ArgumentsTest { - private InternalLogger logger; private Arguments args; @BeforeEach void setUp() { - logger = Mockito.mock(InternalLogger.class); - args = new Arguments(logger); - } - - @Test - void testIntCast_validAndInvalid() { - args.add("num", String.class, "42"); - assertEquals(42, args.getAsInt("num", 0)); - args.add("bad", String.class, "abc"); - assertEquals(0, args.getAsInt("bad", 0)); - } - - @Test - void testDoubleCast_validAndInvalid() { - args.add("d", String.class, "3.14"); - assertEquals(3.14, args.getAsDouble("d", 0.0)); - args.add("badD", String.class, "pi"); - assertEquals(0.0, args.getAsDouble("badD", 0.0)); - } - - @Test - void testBooleanCast() { - args.add("b1", String.class, "true"); - args.add("b2", String.class, "FALSE"); - assertTrue(args.getAsBoolean("b1", false)); - assertFalse(args.getAsBoolean("b2", true)); - } - - @Test - void testStringCast_default() { - assertEquals("def", args.getAsString("missing", "def")); - args.add("s", String.class, "hello"); - assertEquals("hello", args.getAsString("s", "cfg")); + this.args = new Arguments(new InternalLogger(Logger.getLogger("ArgumentsTest"))); } @Test - void testLongFloatShortByteChar() { - args.add("L", String.class, "1234567890123"); - assertEquals(1234567890123L, args.getAsLong("L", 0L)); - args.add("f", String.class, "2.5"); - assertEquals(2.5f, args.getAsFloat("f", 0f)); - args.add("sh", String.class, "7"); - assertEquals((short)7, args.getAsShort("sh", (short)0)); - args.add("by", String.class, "8"); - assertEquals((byte)8, args.getAsByte("by", (byte)0)); - args.add("c", String.class, "z"); - assertEquals('z', args.getAsChar("c", 'x')); - } - - @Test - void testOptionalPresentAndEmpty() { - args.add("opt", String.class, "val"); - Optional optVal = args.getAsString("opt"); - assertTrue(optVal.isPresent()); - assertEquals("val", optVal.get()); - assertFalse(args.getAsInt("none").isPresent()); - } - - @Test - void testGetGeneric_andErrorLogging() { - args.add("gen", Integer.class, 5); - String wrong = args.getAs("gen", String.class, "def"); - assertEquals("def", wrong); - } - - @Test - void testGetThrowsArgumentNotExistLogged() { - assertNull(args.get("xxx")); - verify(logger).error(contains("xxx")); + void testGetThrowsArgumentNotExistThrow() { + assertThrows(ArgumentNotExistException.class, () -> args.get("xxx")); } @Test @@ -98,22 +32,10 @@ void testInfiniteArgsBehavior() { assertEquals("Infinite arguments test", allArgs); } - @Test - void getAs_logsErrorWhenWrongType() { - InternalLogger mockLogger = Mockito.mock(InternalLogger.class); - Arguments args = new Arguments(mockLogger); - args.add("num", Integer.class, 123); - Optional result = args.getAs("num", String.class); - assertFalse(result.isPresent()); - verify(mockLogger).error(contains("The argument num is not the good type.")); - } - @Test void getOptional_onEmptyMapReturnsEmptyWithoutError() { - InternalLogger mockLogger = Mockito.mock(InternalLogger.class); - Arguments args = new Arguments(mockLogger); Optional opt = args.getOptional("anything"); - assertFalse(opt.isPresent()); - verify(mockLogger, never()).error(anyString()); + assertTrue(opt.isEmpty()); } + } \ No newline at end of file diff --git a/core/src/test/java/fr/traqueur/commands/api/models/CommandInvokerTest.java b/core/src/test/java/fr/traqueur/commands/api/models/CommandInvokerTest.java index a92f761..408aa47 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/CommandInvokerTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandInvokerTest.java @@ -2,9 +2,7 @@ import fr.traqueur.commands.api.CommandManager; import fr.traqueur.commands.api.arguments.Arguments; -import fr.traqueur.commands.api.exceptions.ArgumentIncorrectException; import fr.traqueur.commands.api.logging.MessageHandler; -import fr.traqueur.commands.api.models.collections.CommandTree; import fr.traqueur.commands.api.requirements.Requirement; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,42 +10,44 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; @SuppressWarnings("unchecked") class CommandInvokerTest { - private CommandManager manager; - private CommandTree tree; private CommandPlatform platform; private MessageHandler messageHandler; - private CommandInvoker invoker; + private CommandManager manager; private DummyCommand cmd; @BeforeEach void setup() { platform = mock(CommandPlatform.class); messageHandler = mock(MessageHandler.class); - manager = mock(CommandManager.class); - when(manager.getPlatform()).thenReturn(platform); - when(manager.getMessageHandler()).thenReturn(messageHandler); + + manager = new CommandManager<>(platform) { + @Override + public MessageHandler getMessageHandler() { + return messageHandler; + } + }; + when(platform.isPlayer(anyString())).thenReturn(true); when(platform.hasPermission(anyString(), anyString())).thenReturn(true); - cmd = new DummyCommand(); - tree = new CommandTree<>(); - tree.addCommand("base",cmd); - when(manager.getCommands()).thenReturn(tree); + when(messageHandler.getArgNotRecognized()) + .thenReturn("ARG_ERR %arg%"); - invoker = new CommandInvoker<>(manager); + cmd = new DummyCommand(); + manager.getCommands().addCommand("base", cmd); } + @Test void invoke_unknownCommand_returnsFalse() { - boolean res = invoker.invoke("user", "other", new String[]{"x"}); - assertFalse(res); - verifyNoInteractions(platform); + assertFalse(manager.getInvoker().invoke("user", "unknown", new String[]{"x"})); } @Test @@ -56,7 +56,7 @@ void invoke_inGameOnly_nonPlayer_sendsOnlyInGame() { when(platform.isPlayer("user")).thenReturn(false); when(messageHandler.getOnlyInGameMessage()).thenReturn("ONLY_IN_GAME"); - invoker.invoke("user", "base", new String[]{}); + manager.getInvoker().invoke("user", "base", new String[]{}); verify(platform).sendMessage("user", "ONLY_IN_GAME"); } @@ -66,7 +66,7 @@ void invoke_noPermission_sendsNoPermission() { when(platform.hasPermission("user", "perm")).thenReturn(false); when(messageHandler.getNoPermissionMessage()).thenReturn("NO_PERMISSION"); - invoker.invoke("user", "base", new String[]{}); + manager.getInvoker().invoke("user", "base", new String[]{}); verify(platform).sendMessage("user", "NO_PERMISSION"); } @@ -77,7 +77,7 @@ void invoke_requirementFails_sendsRequirementError() { when(req.errorMessage()).thenReturn("REQ_ERR"); cmd.addRequirements(req); - invoker.invoke("user", "base", new String[]{}); + manager.getInvoker().invoke("user", "base", new String[]{}); verify(platform).sendMessage("user", "REQ_ERR"); } @@ -86,24 +86,22 @@ void invoke_wrongArgCount_sendsUsage() { cmd.addArgs("a", String.class); cmd.setUsage("/base "); - invoker.invoke("user", "base", new String[]{}); + manager.getInvoker().invoke("user", "base", new String[]{}); verify(platform).sendMessage("user", "/base "); } @Test - void invoke_parseThrowsArgumentIncorrect_sendsArgNotRecognized() throws Exception { - cmd.addArgs("a", String.class); - when(manager.parse(eq(cmd), any(String[].class))) - .thenThrow(new ArgumentIncorrectException("bad")); - when(messageHandler.getArgNotRecognized()).thenReturn("ARG_ERR %arg%"); + void invoke_parseError_sendsArgNotRecognized() { + cmd.addArgs("a", Integer.class); - invoker.invoke("user", "base", new String[]{"bad"}); + manager.getInvoker().invoke("user", "base", new String[]{"bad"}); verify(platform).sendMessage("user", "ARG_ERR bad"); } @Test void invoke_valid_executesCommand_andReturnsTrue() { AtomicBoolean executed = new AtomicBoolean(false); + DummyCommand custom = new DummyCommand() { @Override public void execute(String sender, Arguments arguments) { @@ -112,40 +110,34 @@ public void execute(String sender, Arguments arguments) { }; custom.addArgs("x", String.class); - tree = new CommandTree<>(); - tree.addCommand("base",custom); - when(manager.getCommands()).thenReturn(tree); - invoker = new CommandInvoker<>(manager); + manager.getCommands().addCommand("exec", custom); - boolean result = invoker.invoke("user", "base", new String[]{"hello"}); + boolean result = manager.getInvoker().invoke("user", "exec", new String[]{"hello"}); assertTrue(result); assertTrue(executed.get()); } @Test - void valid_AliasWithSubCommand_executesSubCommand() { + void aliasWithSubCommand_executesSubCommand() { cmd.addAlias("base.sub"); DummyCommand sub = new DummyCommand(); cmd.addSubCommand(sub); - tree.addCommand("base.sub", cmd); - tree.addCommand("base.sub.base", sub); - tree.addCommand("base.base", sub); + manager.getCommands().addCommand("base.sub", cmd); + manager.getCommands().addCommand("base.sub.base", sub); - List suggests = invoker.suggest("user", "base", new String[]{""}); + List suggests = manager.getInvoker().suggest("user", "base", new String[]{""}); assertTrue(suggests.contains("sub")); - assertTrue(suggests.contains("base")); - - List suggests3 = invoker.suggest("user", "base", new String[]{"sub"}); - assertTrue(suggests3.contains("sub")); - - List suggests4 = invoker.suggest("user", "base", new String[]{"sub", ""}); - assertTrue(suggests4.contains("base")); } static class DummyCommand extends Command { - DummyCommand() { super(null, "base"); } - @Override public void execute(String sender, Arguments args) {} + DummyCommand() { + super(null, "base"); + } + + @Override + public void execute(String sender, Arguments args) { + } } } diff --git a/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java b/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java index 25853a1..bba621e 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java @@ -1,5 +1,3 @@ -// Placez ce fichier sous core/src/test/java/fr/traqueur/commands/api/ - package fr.traqueur.commands.api.models; import fr.traqueur.commands.api.CommandManager; @@ -30,21 +28,50 @@ public void execute(Object sender, Arguments arguments) { } private DummyCommand cmd; - private CommandPlatform platform; @BeforeEach void setUp() { - platform = new CommandPlatform() { - @Override public String getPlugin() { return null; } - @Override public void injectManager(CommandManager commandManager) {} - @Override public java.util.logging.Logger getLogger() { return java.util.logging.Logger.getAnonymousLogger(); } - @Override public boolean hasPermission(Object sender, String permission) { return true; } - @Override public boolean isPlayer(Object sender) {return false;} - @Override public void sendMessage(Object sender, String message) {} - @Override public void addCommand(Command command, String label) {} - @Override public void removeCommand(String label, boolean subcommand) {} + CommandPlatform platform = new CommandPlatform<>() { + @Override + public String getPlugin() { + return null; + } + + @Override + public void injectManager(CommandManager commandManager) { + } + + @Override + public java.util.logging.Logger getLogger() { + return java.util.logging.Logger.getAnonymousLogger(); + } + + @Override + public boolean hasPermission(Object sender, String permission) { + return true; + } + + @Override + public boolean isPlayer(Object sender) { + return false; + } + + @Override + public void sendMessage(Object sender, String message) { + } + + @Override + public void addCommand(Command command, String label) { + } + + @Override + public void removeCommand(String label, boolean subcommand) { + } + }; + CommandManager manager = new CommandManager<>(platform) { }; cmd = new DummyCommand(); + cmd.setManager(manager); } @Test @@ -92,16 +119,45 @@ void testAddSubCommandAndIsSubcommandFlag() { @Test void testRegisterDelegatesToManager() { AtomicBoolean called = new AtomicBoolean(false); - CommandManager fakeManager = new CommandManager(new CommandPlatform() { - @Override public String getPlugin() { return null; } - @Override public void injectManager(CommandManager commandManager) {} - @Override public java.util.logging.Logger getLogger() { return java.util.logging.Logger.getAnonymousLogger(); } - @Override public boolean hasPermission(Object sender, String permission) { return true; } - @Override public boolean isPlayer(Object sender) {return false;} - @Override public void sendMessage(Object sender, String message) {} - @Override public void addCommand(Command command, String label) {called.set(true);} - @Override public void removeCommand(String label, boolean subcommand) { } - }) {}; + CommandManager fakeManager = new CommandManager<>(new CommandPlatform() { + @Override + public String getPlugin() { + return null; + } + + @Override + public void injectManager(CommandManager commandManager) { + } + + @Override + public java.util.logging.Logger getLogger() { + return java.util.logging.Logger.getAnonymousLogger(); + } + + @Override + public boolean hasPermission(Object sender, String permission) { + return true; + } + + @Override + public boolean isPlayer(Object sender) { + return false; + } + + @Override + public void sendMessage(Object sender, String message) { + } + + @Override + public void addCommand(Command command, String label) { + called.set(true); + } + + @Override + public void removeCommand(String label, boolean subcommand) { + } + }) { + }; cmd.setManager(fakeManager); fakeManager.registerCommand(cmd); assertTrue(called.get()); @@ -125,26 +181,9 @@ void testUnregisterDelegatesToManager() { assertTrue(called.get()); } - @Test - void testAddArgsAndOptionalArgs() { - // add required args - cmd.addArgs("arg1", String.class); - cmd.addArgs("arg2"); // string type - assertEquals(2, cmd.getArgs().size()); - assertEquals("arg1:string", cmd.getArgs().get(0).arg().toLowerCase()); - assertEquals("arg2:string", cmd.getArgs().get(1).arg().toLowerCase()); - - // add optional args - cmd.addOptionalArgs("opt1", Integer.class); - cmd.addOptionalArgs("opt2"); - assertEquals(2, cmd.getOptinalArgs().size()); - assertEquals("opt1:integer", cmd.getOptinalArgs().get(0).arg().toLowerCase()); - assertEquals("opt2:string", cmd.getOptinalArgs().get(1).arg().toLowerCase()); - } - @Test void usage_noSubs_noArgs() { - String usage = cmd.generateDefaultUsage(platform, null, "dummy"); + String usage = cmd.generateDefaultUsage(null, "dummy"); assertEquals("/dummy", usage); } @@ -152,7 +191,7 @@ void usage_noSubs_noArgs() { void usage_onlyRequiredArgs() { cmd.addArgs("arg1", String.class); cmd.addArgs("arg2", Integer.class); - String usage = cmd.generateDefaultUsage(platform, null, "dummy"); + String usage = cmd.generateDefaultUsage(null, "dummy"); assertTrue(usage.startsWith("/dummy ")); } @@ -160,7 +199,8 @@ void usage_onlyRequiredArgs() { void usage_requiredAndOptionalArgs() { cmd.addArgs("arg", String.class); cmd.addOptionalArgs("opt", Double.class); - String usage = cmd.generateDefaultUsage(platform, null, "dummy"); + String usage = cmd.generateDefaultUsage(null, "dummy"); + System.out.println(usage); assertTrue(usage.contains("")); assertTrue(usage.contains("[opt:double]")); } @@ -170,7 +210,7 @@ void usage_withSubcommands() { DummyCommand subA = new DummyCommand("suba"); DummyCommand subB = new DummyCommand("subb"); cmd.addSubCommand(subA, subB); - String usage = cmd.generateDefaultUsage(platform, null, "dummy"); + String usage = cmd.generateDefaultUsage(null, "dummy"); // extract first angle bracket content String inside = usage.substring(usage.indexOf('<')+1, usage.indexOf('>')); List parts = Arrays.asList(inside.split("\\|")); @@ -186,7 +226,7 @@ void usage_subsAndArgsCombined() { cmd.addArgs("req", String.class); cmd.addOptionalArgs("opt", String.class); - String usage = cmd.generateDefaultUsage(platform, null, "dummy"); + String usage = cmd.generateDefaultUsage(null, "dummy"); // expect "/dummy [opt:string]" assertTrue(usage.startsWith("/dummy ")); assertTrue(usage.contains("")); diff --git a/core/src/test/java/fr/traqueur/commands/api/models/collections/CommandTreeTest.java b/core/src/test/java/fr/traqueur/commands/api/models/collections/CommandTreeTest.java index 4f5dcc2..1c5c782 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/collections/CommandTreeTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/collections/CommandTreeTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @@ -29,9 +28,9 @@ void testAddAndFindRoot() { // find base with no args Optional> match = tree.findNode("root", new String[]{}); assertTrue(match.isPresent()); - assertEquals(rootCmd, match.get().node.getCommand().orElse(null)); + assertEquals(rootCmd, match.get().node().getCommand().orElse(null)); // full label - assertEquals("root", match.get().node.getFullLabel()); + assertEquals("root", match.get().node().getFullLabel()); } @Test @@ -46,14 +45,14 @@ void testAddNestedAndFind() { // find sub Optional> m1 = tree.findNode("root", new String[]{"sub"}); assertTrue(m1.isPresent()); - assertEquals(subCmd, m1.get().node.getCommand().orElse(null)); - assertArrayEquals(new String[]{}, m1.get().args); + assertEquals(subCmd, m1.get().node().getCommand().orElse(null)); + assertArrayEquals(new String[]{}, m1.get().args()); // find sub.subsub Optional> m2 = tree.findNode("root", new String[]{"sub", "subsub"}); assertTrue(m2.isPresent()); - assertEquals(subSubCmd, m2.get().node.getCommand().orElse(null)); - assertArrayEquals(new String[]{}, m2.get().args); + assertEquals(subSubCmd, m2.get().node().getCommand().orElse(null)); + assertArrayEquals(new String[]{}, m2.get().args()); } @Test @@ -62,8 +61,8 @@ void testFindNodeWithExtraArgs() { // root takes no args, so extra args are leftover Optional> m = tree.findNode("root", new String[]{"a","b","c"}); assertTrue(m.isPresent()); - assertEquals(rootCmd, m.get().node.getCommand().orElse(null)); - assertArrayEquals(new String[]{"a","b","c"}, m.get().args); + assertEquals(rootCmd, m.get().node().getCommand().orElse(null)); + assertArrayEquals(new String[]{"a", "b", "c"}, m.get().args()); } @Test @@ -93,11 +92,11 @@ void testRemoveCommandKeepChildren() { // root command cleared but sub-tree remains Optional> mSub = tree.findNode("root", new String[]{"sub"}); assertTrue(mSub.isPresent()); - assertEquals(subCmd, mSub.get().node.getCommand().orElse(null)); + assertEquals(subCmd, mSub.get().node().getCommand().orElse(null)); // find root itself => cleared, so no command at root Optional> mRoot = tree.findNode("root", new String[]{}); - assertFalse(mRoot.get().node.getCommand().isPresent()); + assertFalse(mRoot.get().node().getCommand().isPresent()); } @Test @@ -113,10 +112,10 @@ void testRemoveCommandPruneBranch() { tree.removeCommand("root.sub", true); Optional> rootopt = tree.findNode("root", new String[]{"sub"}); assertTrue(rootopt.isPresent()); - assertEquals(rootCmd, rootopt.get().node.getCommand().orElse(null)); + assertEquals(rootCmd, rootopt.get().node().getCommand().orElse(null)); rootopt = tree.findNode("root", new String[]{"sub", "subsub"}); assertTrue(rootopt.isPresent()); - assertEquals(rootCmd, rootopt.get().node.getCommand().orElse(null)); + assertEquals(rootCmd, rootopt.get().node().getCommand().orElse(null)); assertTrue(tree.findNode("root", new String[]{}).isPresent()); } diff --git a/core/src/test/java/fr/traqueur/commands/impl/logging/InternalMessageHandlerTest.java b/core/src/test/java/fr/traqueur/commands/impl/logging/InternalMessageHandlerTest.java index ba53090..7eaf6de 100644 --- a/core/src/test/java/fr/traqueur/commands/impl/logging/InternalMessageHandlerTest.java +++ b/core/src/test/java/fr/traqueur/commands/impl/logging/InternalMessageHandlerTest.java @@ -1,7 +1,6 @@ -// src/test/java/fr/traqueur/commands/api/logging/InternalMessageHandlerTest.java -package fr.traqueur.commands.api.logging; +package fr.traqueur.commands.impl.logging; -import fr.traqueur.commands.impl.logging.InternalMessageHandler; +import fr.traqueur.commands.api.logging.MessageHandler; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java index f892eba..a9a12a5 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java @@ -1,6 +1,5 @@ package fr.traqueur.commands.test.commands; -import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.jda.Command; import fr.traqueur.commands.jda.JDAArguments; import fr.traqueur.commands.test.TestBot; @@ -70,13 +69,8 @@ public KickCommand(TestBot bot) { @Override public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { JDAArguments jdaArgs = jda(arguments); - User user = jdaArgs.getUser("user").orElse(null); - String reason = jdaArgs.getAsString("reason").orElse("No reason provided"); - - if (user == null) { - jdaArgs.replyEphemeral("User not found!"); - return; - } + User user = jdaArgs.get("user"); + String reason = jdaArgs.getOptional("reason").orElse("No reason provided"); jdaArgs.reply(String.format("Would kick user %s for reason: %s", user.getAsMention(), reason)); @@ -96,13 +90,8 @@ public BanCommand(TestBot bot) { @Override public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { - User user = arguments.getUser("user").orElse(null); - String reason = arguments.getAsString("reason").orElse("No reason provided"); - - if (user == null) { - arguments.replyEphemeral("User not found!"); - return; - } + User user = arguments.get("user"); + String reason = arguments.getOptional("reason").orElse("No reason provided"); arguments.reply(String.format("Would ban user %s for reason: %s", user.getAsMention(), reason)); @@ -172,8 +161,8 @@ public ServerSettingsCommand(TestBot bot) { @Override public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { JDAArguments jdaArgs = jda(arguments); - String option = jdaArgs.getAsString("option").orElse("unknown"); - String value = jdaArgs.getAsString("value").orElse("unknown"); + String option = jdaArgs.get("option"); + String value = jdaArgs.get("value"); jdaArgs.reply(String.format("Would set setting '%s' to '%s'", option, value)); } diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/GreetCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/GreetCommand.java index 84dc086..fb69ec4 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/GreetCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/GreetCommand.java @@ -1,6 +1,6 @@ package fr.traqueur.commands.test.commands; -import fr.traqueur.commands.api.arguments.Arguments; +import fr.traqueur.commands.api.arguments.Infinite; import fr.traqueur.commands.jda.Command; import fr.traqueur.commands.jda.JDAArguments; import fr.traqueur.commands.test.TestBot; @@ -17,18 +17,14 @@ public GreetCommand(TestBot bot) { super(bot, "greet"); this.setDescription("Greet a user with a custom message"); this.addArgs("user", User.class); - this.addOptionalArgs("message:infinite"); + this.addOptionalArgs("message", Infinite.class); } @Override public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { - User target = arguments.getUser("user").orElse(null); - if (target == null) { - arguments.replyEphemeral("User not found!"); - return; - } + User target = arguments.get("user"); - String customMessage = arguments.getAsString("message", "Hello there!"); + String customMessage = arguments.getOptional("message").orElse("Hello there!"); String greeting = String.format("%s says to %s: %s", event.getUser().getAsMention(), diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/MathCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/MathCommand.java index 19c8275..7e0f2ad 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/MathCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/MathCommand.java @@ -1,10 +1,8 @@ package fr.traqueur.commands.test.commands; -import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.jda.Command; import fr.traqueur.commands.jda.JDAArguments; import fr.traqueur.commands.test.TestBot; -import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; /** @@ -45,8 +43,8 @@ public AddCommand(TestBot bot) { @Override public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { - double a = arguments.getAsDouble("a").orElse(0.0); - double b = arguments.getAsDouble("b").orElse(0.0); + double a = arguments.get("a"); + double b = arguments.get("b"); double result = a + b; jda(arguments).reply(String.format("%.2f + %.2f = %.2f", a, b, result)); @@ -65,8 +63,8 @@ public SubtractCommand(TestBot bot) { @Override public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { - double a = arguments.getAsDouble("a").orElse(0.0); - double b = arguments.getAsDouble("b").orElse(0.0); + double a = arguments.get("a"); + double b = arguments.get("b"); double result = a - b; jda(arguments).reply(String.format("%.2f - %.2f = %.2f", a, b, result)); @@ -85,8 +83,8 @@ public MultiplyCommand(TestBot bot) { @Override public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { - double a = arguments.getAsDouble("a").orElse(0.0); - double b = arguments.getAsDouble("b").orElse(0.0); + double a = arguments.get("a"); + double b = arguments.get("b"); double result = a * b; jda(arguments).reply(String.format("%.2f × %.2f = %.2f", a, b, result)); @@ -105,8 +103,8 @@ public DivideCommand(TestBot bot) { @Override public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { - double a = arguments.getAsDouble("a").orElse(0.0); - double b = arguments.getAsDouble("b").orElse(0.0); + double a = arguments.get("a"); + double b = arguments.get("b"); if (b == 0) { jda(arguments).replyEphemeral("Cannot divide by zero!"); diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/UserInfoCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/UserInfoCommand.java index 548d490..b22c1dc 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/UserInfoCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/UserInfoCommand.java @@ -1,6 +1,5 @@ package fr.traqueur.commands.test.commands; -import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.jda.Command; import fr.traqueur.commands.jda.JDAArguments; import fr.traqueur.commands.test.TestBot; @@ -22,7 +21,7 @@ public UserInfoCommand(TestBot bot) { super(bot, "userinfo"); this.setDescription("Get information about a user"); this.setGameOnly(true); // Guild-only command - this.addOptionalArgs("user", User.class); + this.addOptionalArgs("user", Member.class); } @Override @@ -30,13 +29,12 @@ public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) JDAArguments jdaArgs = jda(arguments); // Get the target user (defaults to the command executor) - User user = jdaArgs.getUser("user").orElse(event.getUser()); - Member member = event.getGuild().getMember(user); - + Member member = jdaArgs.getOptional("user").orElse(event.getMember()); if (member == null) { jdaArgs.reply("User not found in this server!"); return; } + User user = member.getUser(); // Build embed EmbedBuilder embed = new EmbedBuilder() diff --git a/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java b/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java index c1c7660..d1b9a7f 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java @@ -1,12 +1,6 @@ package fr.traqueur.commands.jda; -import fr.traqueur.commands.jda.arguments.*; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.Role; -import net.dv8tion.jda.api.entities.User; -import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.util.logging.Logger; @@ -34,23 +28,6 @@ public class CommandManager extends fr.traqueur.commands.api.CommandManager(bot, jda, logger)); this.jdaPlatform = (JDAPlatform) getPlatform(); - - // Register JDA-specific argument converters with JDA context - this.registerJDAConverters(jda); - } - - /** - * Register JDA-specific argument converters. - * This method is called automatically in the constructor. - * - * @param jda The JDA instance to inject into converters. - */ - private void registerJDAConverters(JDA jda) { - registerConverter(User.class, new UserArgument(jda)); - registerConverter(Member.class, new MemberArgument(jda)); - registerConverter(Role.class, new RoleArgument(jda)); - registerConverter(GuildChannelUnion.class, new ChannelArgument(jda)); - registerConverter(Message.Attachment.class, new AttachmentArgument(jda)); } /** diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAArguments.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAArguments.java index 2ea09eb..59dfd26 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAArguments.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAArguments.java @@ -2,12 +2,7 @@ import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.api.logging.Logger; -import net.dv8tion.jda.api.entities.*; -import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; - -import java.util.Optional; /** * JDA-specific implementation of Arguments that provides direct access to Discord entities. @@ -41,185 +36,6 @@ public SlashCommandInteractionEvent getEvent() { return event; } - /** - * Get a User from the command options. - * - * @param name The name of the option. - * @return An Optional containing the User if present. - */ - public Optional getUser(String name) { - OptionMapping option = event.getOption(name); - if (option != null) { - return Optional.of(option.getAsUser()); - } - // Fallback to arguments map - return this.getAs(name, User.class); - } - - /** - * Get a User from the command options with a default value. - * - * @param name The name of the option. - * @param defaultValue The default value if not present. - * @return The User or the default value. - */ - public User getUser(String name, User defaultValue) { - return getUser(name).orElse(defaultValue); - } - - /** - * Get a Member from the command options. - * - * @param name The name of the option. - * @return An Optional containing the Member if present. - */ - public Optional getMember(String name) { - OptionMapping option = event.getOption(name); - if (option != null) { - return Optional.ofNullable(option.getAsMember()); - } - // Fallback to arguments map - return this.getAs(name, Member.class); - } - - /** - * Get a Member from the command options with a default value. - * - * @param name The name of the option. - * @param defaultValue The default value if not present. - * @return The Member or the default value. - */ - public Member getMember(String name, Member defaultValue) { - return getMember(name).orElse(defaultValue); - } - - /** - * Get a Role from the command options. - * - * @param name The name of the option. - * @return An Optional containing the Role if present. - */ - public Optional getRole(String name) { - OptionMapping option = event.getOption(name); - if (option != null) { - return Optional.of(option.getAsRole()); - } - // Fallback to arguments map - return this.getAs(name, Role.class); - } - - /** - * Get a Role from the command options with a default value. - * - * @param name The name of the option. - * @param defaultValue The default value if not present. - * @return The Role or the default value. - */ - public Role getRole(String name, Role defaultValue) { - return getRole(name).orElse(defaultValue); - } - - /** - * Get a GuildChannel from the command options. - * - * @param name The name of the option. - * @return An Optional containing the GuildChannel if present. - */ - public Optional getChannel(String name) { - OptionMapping option = event.getOption(name); - if (option != null) { - return Optional.of(option.getAsChannel()); - } - // Fallback to arguments map - return this.getAs(name, GuildChannelUnion.class); - } - - /** - * Get a GuildChannel from the command options with a default value. - * - * @param name The name of the option. - * @param defaultValue The default value if not present. - * @return The GuildChannel or the default value. - */ - public GuildChannelUnion getChannel(String name, GuildChannelUnion defaultValue) { - return getChannel(name).orElse(defaultValue); - } - - /** - * Override getAsString to first check JDA options. - * - * @param argument The key of the argument. - * @return The string or empty if not present. - */ - @Override - public Optional getAsString(String argument) { - OptionMapping option = event.getOption(argument); - if (option != null) { - return Optional.of(option.getAsString()); - } - return super.getAsString(argument); - } - - /** - * Override getAsInt to first check JDA options. - * - * @param argument The key of the argument. - * @return The integer or empty if not present. - */ - @Override - public Optional getAsInt(String argument) { - OptionMapping option = event.getOption(argument); - if (option != null) { - return Optional.of((int) option.getAsLong()); - } - return super.getAsInt(argument); - } - - /** - * Override getAsLong to first check JDA options. - * - * @param argument The key of the argument. - * @return The long or empty if not present. - */ - @Override - public Optional getAsLong(String argument) { - OptionMapping option = event.getOption(argument); - if (option != null) { - return Optional.of(option.getAsLong()); - } - return super.getAsLong(argument); - } - - /** - * Override getAsDouble to first check JDA options. - * - * @param argument The key of the argument. - * @return The double or empty if not present. - */ - @Override - public Optional getAsDouble(String argument) { - OptionMapping option = event.getOption(argument); - if (option != null) { - return Optional.of(option.getAsDouble()); - } - return super.getAsDouble(argument); - } - - /** - * Override getAsBoolean to first check JDA options. - * - * @param argument The key of the argument. - * @return The boolean or empty if not present. - */ - @Override - public Optional getAsBoolean(String argument) { - OptionMapping option = event.getOption(argument); - if (option != null) { - return Optional.of(option.getAsBoolean()); - } - return super.getAsBoolean(argument); - } - /** * Reply to the interaction. * diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java index 5ebf6d1..aa07993 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java @@ -1,6 +1,9 @@ package fr.traqueur.commands.jda; 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.exceptions.ArgumentIncorrectException; import fr.traqueur.commands.api.models.Command; import fr.traqueur.commands.api.models.collections.CommandTree; import fr.traqueur.commands.api.requirements.Requirement; @@ -11,6 +14,7 @@ import net.dv8tion.jda.api.interactions.commands.OptionMapping; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -52,7 +56,7 @@ public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent even return; } - JDAArguments jdaArguments = createArguments(event); + JDAArguments jdaArguments = createArguments(event, command); executeCommand(event, command, jdaArguments, label); } @@ -86,7 +90,7 @@ private Optional> findCommand( } CommandTree.MatchResult result = found.get(); - CommandTree.CommandNode node = result.node; + CommandTree.CommandNode node = result.node(); Optional> cmdOpt = node.getCommand(); if (cmdOpt.isEmpty()) { @@ -180,12 +184,26 @@ private boolean checkRequirements(SlashCommandInteractionEvent event, * @param event The slash command event. * @return The populated JDAArguments. */ - private JDAArguments createArguments(SlashCommandInteractionEvent event) { + private JDAArguments createArguments(SlashCommandInteractionEvent event, Command command) { JDAArguments jdaArguments = new JDAArguments(commandManager.getLogger(), event); List options = event.getOptions(); - for (OptionMapping option : options) { - populateArgument(jdaArguments, option); + List> args = new ArrayList<>(); + args.addAll(command.getArgs()); + args.addAll(command.getOptinalArgs()); + + if (options.size() < command.getArgs().size()) { + throw new IllegalArgumentException("No enough arguments"); + } + int sizeWithoutRequired = options.size() - command.getArgs().size(); + if (sizeWithoutRequired > command.getOptinalArgs().size()) { + throw new IllegalArgumentException("More options than expected"); + } + + for (int i = 0; i < options.size(); i++) { + OptionMapping optionMapping = options.get(i); + Argument arg = args.get(i); + populateArgument(jdaArguments, optionMapping, arg); } return jdaArguments; @@ -197,26 +215,57 @@ private JDAArguments createArguments(SlashCommandInteractionEvent event) { * @param arguments The arguments container. * @param option The option to populate from. */ - private void populateArgument(JDAArguments arguments, OptionMapping option) { + private void populateArgument(JDAArguments arguments, OptionMapping option, Argument arg) { String name = option.getName(); - switch (option.getType()) { case STRING: arguments.add(name, String.class, option.getAsString()); break; case INTEGER: - arguments.add(name, Long.class, option.getAsLong()); + if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { + throw new ArgumentIncorrectException(option.getName()); + } + if (clazz == Integer.class) { + arguments.add(name, Integer.class, option.getAsInt()); + } else if (clazz == int.class) { + arguments.add(name, int.class, option.getAsInt()); + } else if (clazz == Long.class) { + arguments.add(name, Long.class, option.getAsLong()); + } else if (clazz == long.class) { + arguments.add(name, long.class, option.getAsLong()); + } else { + throw new ArgumentIncorrectException(option.getName()); + } break; case NUMBER: - arguments.add(name, Double.class, option.getAsDouble()); + if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { + throw new ArgumentIncorrectException(option.getName()); + } + if (clazz == Double.class) { + arguments.add(name, Double.class, option.getAsDouble()); + } else if (clazz == double.class) { + arguments.add(name, double.class, option.getAsDouble()); + } else if (clazz == Float.class) { + arguments.add(name, Float.class, (float) option.getAsDouble()); + } else if (clazz == float.class) { + arguments.add(name, float.class, (float) option.getAsDouble()); + } else { + throw new ArgumentIncorrectException(option.getName()); + } break; case BOOLEAN: arguments.add(name, Boolean.class, option.getAsBoolean()); break; case USER: - arguments.add(name, User.class, option.getAsUser()); - if (option.getAsMember() != null) { + if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { + throw new ArgumentIncorrectException(option.getName()); + } + if (clazz == Member.class) { arguments.add(name, Member.class, option.getAsMember()); + } else if (clazz == User.class) { + arguments.add(name, User.class, option.getAsUser()); + } else { + throw new ArgumentIncorrectException(option.getName()); } break; case ROLE: diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java index dec2123..1188af9 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java @@ -8,11 +8,7 @@ import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.interactions.commands.build.Commands; -import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; -import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; -import net.dv8tion.jda.api.interactions.commands.build.SubcommandGroupData; +import net.dv8tion.jda.api.interactions.commands.build.*; import java.util.HashMap; import java.util.List; @@ -244,11 +240,9 @@ private void addArgumentsToSubcommand(SubcommandData subcommand, Command arg, boolean required) { - String[] parts = arg.arg().split(CommandManager.TYPE_PARSER); - String name = parts[0].trim().toLowerCase(); - String type = parts.length > 1 ? parts[1].trim() : "string"; + String name = arg.name(); - OptionType optionType = mapToOptionType(type); + OptionType optionType = mapToOptionType(arg.type().key()); return new OptionData(optionType, name, "Argument: " + name, required); } diff --git a/jda/src/main/java/fr/traqueur/commands/jda/arguments/AttachmentArgument.java b/jda/src/main/java/fr/traqueur/commands/jda/arguments/AttachmentArgument.java deleted file mode 100644 index 5e02cd0..0000000 --- a/jda/src/main/java/fr/traqueur/commands/jda/arguments/AttachmentArgument.java +++ /dev/null @@ -1,50 +0,0 @@ -package fr.traqueur.commands.jda.arguments; - -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.Message; - -/** - * Argument converter for JDA {@link Message.Attachment}. - *

- * This converter handles attachment input for Discord messages. - *

- *

- * Note: Attachments are only available in the context of slash commands or message events. - * For slash commands, Discord handles attachment input natively via the ATTACHMENT option type. - *

- *

- * Important: This converter cannot resolve attachments from a string input. - * It returns null when called. For slash commands, attachments are automatically provided - * by Discord through the event options (see JDAExecutor lines 136-138). - *

- */ -public class AttachmentArgument extends JDAArgumentConverter { - - /** - * Creates a new AttachmentArgument. - * - * @param jda The JDA instance. - */ - public AttachmentArgument(JDA jda) { - super(jda); - } - - /** - * {@inheritDoc} - *

- * Note: This implementation cannot resolve attachments from string input. - * For slash commands, attachments are automatically provided by Discord - * through the event options. - *

- * - * @param input The input string (unused for attachments) - * @return Always returns null as attachments cannot be resolved from strings - */ - @Override - public Message.Attachment apply(String input) { - // Attachments cannot be resolved from string input - // They are automatically provided by Discord in slash commands - // through the event options (see JDAExecutor lines 136-138) - return null; - } -} \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/arguments/ChannelArgument.java b/jda/src/main/java/fr/traqueur/commands/jda/arguments/ChannelArgument.java deleted file mode 100644 index 9f19d2b..0000000 --- a/jda/src/main/java/fr/traqueur/commands/jda/arguments/ChannelArgument.java +++ /dev/null @@ -1,56 +0,0 @@ -package fr.traqueur.commands.jda.arguments; - -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; - -/** - * Argument converter for JDA {@link GuildChannelUnion}. - *

- * This converter handles channel input in various formats: - *

- *
    - *
  • Channel ID (e.g., "123456789")
  • - *
  • Channel mention (e.g., "<#123456789>")
  • - *
- *

- * Note: This converter requires a guild context to resolve channels. - * For slash commands, Discord handles channel input natively via the CHANNEL option type. - * The guild context is obtained from the command execution context. - *

- *

- * Important: This converter cannot resolve channels without a guild context. - * It returns null if called outside of a guild context. For slash commands, - * use {@link fr.traqueur.commands.jda.JDAArguments#getChannel(String)} instead, - * which properly handles the guild context from the event. - *

- */ -public class ChannelArgument extends JDAArgumentConverter { - - /** - * Creates a new ChannelArgument. - * - * @param jda The JDA instance. - */ - public ChannelArgument(JDA jda) { - super(jda); - } - - /** - * {@inheritDoc} - *

- * Note: This implementation cannot resolve channels without a guild context. - * For slash commands, the channel is automatically resolved by Discord and - * available through {@link fr.traqueur.commands.jda.JDAArguments#getChannel(String)}. - *

- * - * @param input The channel ID or mention string - * @return Always returns null as guild context is required - */ - @Override - public GuildChannelUnion apply(String input) { - // Channels require guild context which is not available in this converter - // For slash commands, channels are automatically provided by Discord - // through the event options (see JDAExecutor lines 129-131) - return null; - } -} \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/arguments/JDAArgumentConverter.java b/jda/src/main/java/fr/traqueur/commands/jda/arguments/JDAArgumentConverter.java deleted file mode 100644 index 2961345..0000000 --- a/jda/src/main/java/fr/traqueur/commands/jda/arguments/JDAArgumentConverter.java +++ /dev/null @@ -1,49 +0,0 @@ -package fr.traqueur.commands.jda.arguments; - -import fr.traqueur.commands.api.arguments.ArgumentConverter; -import fr.traqueur.commands.api.arguments.TabCompleter; -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; - -import java.util.Collections; -import java.util.List; - -/** - * Abstract base class for JDA argument converters. - *

- * This class provides access to the JDA instance for converters that need it. - * All JDA-specific argument converters should extend this class. - *

- * - * @param The type this converter produces. - */ -public abstract class JDAArgumentConverter implements ArgumentConverter, TabCompleter { - - /** - * The JDA instance used for resolving Discord entities. - */ - protected final JDA jda; - - /** - * Creates a new JDA argument converter. - * - * @param jda The JDA instance. - */ - public JDAArgumentConverter(JDA jda) { - this.jda = jda; - } - - /** - * {@inheritDoc} - *

- * For slash commands, autocomplete is handled by Discord natively. - * This method returns an empty list by default. - * Subclasses can override this if needed for non-slash command contexts. - *

- */ - @Override - public List onCompletion(SlashCommandInteractionEvent sender, List args) { - // Slash commands handle autocomplete natively through Discord - return Collections.emptyList(); - } -} \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/arguments/MemberArgument.java b/jda/src/main/java/fr/traqueur/commands/jda/arguments/MemberArgument.java deleted file mode 100644 index 32a32d1..0000000 --- a/jda/src/main/java/fr/traqueur/commands/jda/arguments/MemberArgument.java +++ /dev/null @@ -1,56 +0,0 @@ -package fr.traqueur.commands.jda.arguments; - -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.Member; - -/** - * Argument converter for JDA {@link Member}. - *

- * This converter handles member input in various formats: - *

- *
    - *
  • User ID (e.g., "123456789")
  • - *
  • User mention (e.g., "<@123456789>" or "<@!123456789>")
  • - *
- *

- * Note: This converter requires a guild context to resolve members. - * For slash commands, Discord handles member input natively via the USER option type. - * The guild context is obtained from the command execution context. - *

- *

- * Important: This converter cannot resolve members without a guild context. - * It returns null if called outside of a guild context. For slash commands, - * use {@link fr.traqueur.commands.jda.JDAArguments#getMember(String)} instead, - * which properly handles the guild context from the event. - *

- */ -public class MemberArgument extends JDAArgumentConverter { - - /** - * Creates a new MemberArgument. - * - * @param jda The JDA instance. - */ - public MemberArgument(JDA jda) { - super(jda); - } - - /** - * {@inheritDoc} - *

- * Note: This implementation cannot resolve members without a guild context. - * For slash commands, the member is automatically resolved by Discord and - * available through {@link fr.traqueur.commands.jda.JDAArguments#getMember(String)}. - *

- * - * @param input The user ID or mention string - * @return Always returns null as guild context is required - */ - @Override - public Member apply(String input) { - // Members require guild context which is not available in this converter - // For slash commands, members are automatically provided by Discord - // through the event options (see JDAExecutor lines 122-124) - return null; - } -} \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/arguments/RoleArgument.java b/jda/src/main/java/fr/traqueur/commands/jda/arguments/RoleArgument.java deleted file mode 100644 index f7785d0..0000000 --- a/jda/src/main/java/fr/traqueur/commands/jda/arguments/RoleArgument.java +++ /dev/null @@ -1,56 +0,0 @@ -package fr.traqueur.commands.jda.arguments; - -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.Role; - -/** - * Argument converter for JDA {@link Role}. - *

- * This converter handles role input in various formats: - *

- *
    - *
  • Role ID (e.g., "123456789")
  • - *
  • Role mention (e.g., "<@&123456789>")
  • - *
- *

- * Note: This converter requires a guild context to resolve roles. - * For slash commands, Discord handles role input natively via the ROLE option type. - * The guild context is obtained from the command execution context. - *

- *

- * Important: This converter cannot resolve roles without a guild context. - * It returns null if called outside of a guild context. For slash commands, - * use {@link fr.traqueur.commands.jda.JDAArguments#getRole(String)} instead, - * which properly handles the guild context from the event. - *

- */ -public class RoleArgument extends JDAArgumentConverter { - - /** - * Creates a new RoleArgument. - * - * @param jda The JDA instance. - */ - public RoleArgument(JDA jda) { - super(jda); - } - - /** - * {@inheritDoc} - *

- * Note: This implementation cannot resolve roles without a guild context. - * For slash commands, the role is automatically resolved by Discord and - * available through {@link fr.traqueur.commands.jda.JDAArguments#getRole(String)}. - *

- * - * @param input The role ID or mention string - * @return Always returns null as guild context is required - */ - @Override - public Role apply(String input) { - // Roles require guild context which is not available in this converter - // For slash commands, roles are automatically provided by Discord - // through the event options (see JDAExecutor line 127) - return null; - } -} \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/arguments/UserArgument.java b/jda/src/main/java/fr/traqueur/commands/jda/arguments/UserArgument.java deleted file mode 100644 index e129fbb..0000000 --- a/jda/src/main/java/fr/traqueur/commands/jda/arguments/UserArgument.java +++ /dev/null @@ -1,60 +0,0 @@ -package fr.traqueur.commands.jda.arguments; - -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.entities.User; - -/** - * Argument converter for JDA {@link User}. - *

- * This converter handles user input in various formats: - *

- *
    - *
  • User ID (e.g., "123456789")
  • - *
  • User mention (e.g., "<@123456789>" or "<@!123456789>")
  • - *
- *

- * For slash commands, Discord handles user input natively via the USER option type. - * This converter is primarily useful for text-based command contexts. - *

- */ -public class UserArgument extends JDAArgumentConverter { - - /** - * Creates a new UserArgument. - * - * @param jda The JDA instance. - */ - public UserArgument(JDA jda) { - super(jda); - } - - /** - * {@inheritDoc} - *

- * Converts a user ID or mention to a User object. - * Accepts both raw IDs (e.g., "123456789") and mentions (e.g., "<@123456789>"). - *

- * - * @param input The user ID or mention string - * @return The User object, or null if not found or invalid - */ - @Override - public User apply(String input) { - if (input == null || input.isEmpty()) { - return null; - } - - // Remove mention formatting if present (<@123456789> or <@!123456789>) - String id = input.replaceAll("[<@!>]", ""); - - try { - long userId = Long.parseLong(id); - return jda.retrieveUserById(userId).complete(); - } catch (NumberFormatException e) { - return null; - } catch (Exception e) { - // User not found or other JDA exception - return null; - } - } -} \ No newline at end of file diff --git a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/Sub2TestCommand.java b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/Sub2TestCommand.java index 5f42c9d..42d7a2a 100644 --- a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/Sub2TestCommand.java +++ b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/Sub2TestCommand.java @@ -15,7 +15,7 @@ public Sub2TestCommand(TestPlugin plugin) { @Override public void execute(CommandSender sender, Arguments args) { - args.getAsInt("test").ifPresent(test -> sender.sendMessage("Test: " + test)); + args.getOptional("test").ifPresent(test -> sender.sendMessage("Test: " + test)); sender.sendMessage(this.getUsage()); } } diff --git a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/SubTestCommand.java b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/SubTestCommand.java index d1ade95..c81ca64 100644 --- a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/SubTestCommand.java +++ b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/SubTestCommand.java @@ -10,8 +10,8 @@ public class SubTestCommand extends Command { public SubTestCommand(TestPlugin plugin) { super(plugin, "sub.inner"); - this.addArgs("test"); - this.addArgs("testStr", String.class, (sender, args) -> { + this.addArgs("test", Integer.class); + this.addArg("testStr", String.class, (sender, args) -> { args.forEach(arg -> { sender.sendMessage("Arg: " + arg); }); @@ -22,7 +22,7 @@ public SubTestCommand(TestPlugin plugin) { @Override public void execute(CommandSender sender, Arguments arguments) { - int test = arguments.getAsInt("test", -1); + int test = arguments.getOptional("test").orElse(-1); String testStr = arguments.get("testStr"); sender.sendMessage("Test: " + test + " TestStr: " + testStr); sender.sendMessage(this.getUsage()); diff --git a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/TestCommand.java b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/TestCommand.java index 6cd38da..40176bd 100644 --- a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/TestCommand.java +++ b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/TestCommand.java @@ -9,13 +9,13 @@ public class TestCommand extends Command { public TestCommand(TestPlugin plugin) { super(plugin, "test"); this.addSubCommand(new SubTestCommand(plugin), new Sub2TestCommand(plugin)); - this.addArgs("test"); + this.addArgs("test", Integer.class); this.addAlias("inner.in"); } @Override public void execute(CommandSender sender, Arguments arguments) { - int test = arguments.getAsInt("test", -1); + int test = arguments.get("test"); sender.sendMessage("Test command executed! " + test); sender.sendMessage(this.getUsage()); } diff --git a/spigot/build.gradle b/spigot/build.gradle index 62d026c..784ef89 100644 --- a/spigot/build.gradle +++ b/spigot/build.gradle @@ -20,8 +20,8 @@ dependencies { } java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 withSourcesJar() withJavadocJar() } diff --git a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/Sub2TestCommand.java b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/Sub2TestCommand.java index 72a3b00..f3e7ff0 100644 --- a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/Sub2TestCommand.java +++ b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/Sub2TestCommand.java @@ -9,13 +9,13 @@ public class Sub2TestCommand extends Command { public Sub2TestCommand(VelocityTestPlugin plugin) { super(plugin, "sub2"); - this.addArgs("test"); + this.addArgs("test", Integer.class); this.addAlias("sub2.inner"); } @Override public void execute(CommandSource sender, Arguments args) { - args.getAsInt("test").ifPresent(test -> sender.sendMessage(Component.text("Test: " + test))); + args.getOptional("test").ifPresent(test -> sender.sendMessage(Component.text("Test: " + test))); sender.sendMessage(Component.text(this.getUsage())); } } diff --git a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/SubTestCommand.java b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/SubTestCommand.java index 445272d..990995b 100644 --- a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/SubTestCommand.java +++ b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/SubTestCommand.java @@ -11,8 +11,8 @@ public class SubTestCommand extends Command { public SubTestCommand(VelocityTestPlugin plugin) { super(plugin, "sub.inner"); - this.addArgs("test"); - this.addArgs("testStr", String.class, (sender, args) -> { + this.addArgs("test", Integer.class); + this.addArg("testStr", String.class, (sender, args) -> { args.forEach(arg -> { sender.sendMessage(Component.text("Arg: " + arg)); }); @@ -23,7 +23,7 @@ public SubTestCommand(VelocityTestPlugin plugin) { @Override public void execute(CommandSource sender, Arguments args) { - int test = args.getAsInt("test", -1); + int test = args.getOptional("test").orElse(-1); String testStr = args.get("testStr"); sender.sendMessage(Component.text("Test: " + test + " TestStr: " + testStr)); sender.sendMessage(Component.text(this.getUsage())); diff --git a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/TestCommand.java b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/TestCommand.java index 74f0603..b26a7d6 100644 --- a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/TestCommand.java +++ b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/TestCommand.java @@ -10,13 +10,13 @@ public class TestCommand extends Command { public TestCommand(VelocityTestPlugin plugin) { super(plugin, "test"); this.addSubCommand(new SubTestCommand(plugin), new Sub2TestCommand(plugin)); - this.addArgs("test"); + this.addArgs("test", Integer.class); this.addAlias("inner.in"); } @Override public void execute(CommandSource sender, Arguments args) { - int test = args.getAsInt("test", -1); + int test = args.get("test"); sender.sendMessage(Component.text("Test command executed! " + test)); sender.sendMessage(Component.text(this.getUsage())); } From d96007896b62b1b5d5228e0a6973525f9868d633 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 23 Dec 2025 19:26:16 +0100 Subject: [PATCH 02/23] feat(jda): remove duplication code Use ArgumentParser as interface --- .../traqueur/commands/api/CommandManager.java | 7 +- .../commands/api/arguments/ArgumentType.java | 41 +++ .../commands/api/arguments/Infinite.java | 3 + .../commands/api/parsing/ArgumentParser.java | 22 ++ .../commands/api/parsing/ParseError.java | 36 +++ .../commands/api/parsing/ParseResult.java | 34 ++ .../impl/parsing/DefaultArgumentParser.java | 133 ++++++++ .../commands/jda/JDAArgumentParser.java | 137 ++++++++ .../fr/traqueur/commands/jda/JDAExecutor.java | 293 ++---------------- 9 files changed, 443 insertions(+), 263 deletions(-) create mode 100644 core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java create mode 100644 core/src/main/java/fr/traqueur/commands/api/arguments/Infinite.java create mode 100644 core/src/main/java/fr/traqueur/commands/api/parsing/ArgumentParser.java create mode 100644 core/src/main/java/fr/traqueur/commands/api/parsing/ParseError.java create mode 100644 core/src/main/java/fr/traqueur/commands/api/parsing/ParseResult.java create mode 100644 core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java create mode 100644 jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java 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 26e661b..1287abf 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -22,6 +22,7 @@ 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.*; @@ -35,7 +36,7 @@ public abstract class CommandManager { - private final ArgumentParser parser; + private final ArgumentParser parser; private final CommandPlatform platform; /** @@ -87,7 +88,7 @@ public CommandManager(CommandPlatform platform) { this.typeConverters = new HashMap<>(); this.completers = new HashMap<>(); this.invoker = new CommandInvoker<>(this); - this.parser = new ArgumentParser<>(this.typeConverters); + this.parser = new DefaultArgumentParser<>(this.typeConverters, this.logger); this.registerInternalConverters(); } @@ -210,7 +211,7 @@ public void registerConverter(Class typeClass, ArgumentConverter conve * @throws ArgumentIncorrectException If the argument is incorrect. */ public Arguments parse(Command command, String[] args) throws TypeArgumentNotExistException, ArgumentIncorrectException { - ParseResult result = parser.parse(command, args, this.logger); + ParseResult result = parser.parse(command, args); if (!result.isSuccess()) { ParseError error = result.error(); switch (error.type()) { 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..fb52514 --- /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 { + + 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"; + } + } + + static ArgumentType of(Class clazz) { + if(clazz.isAssignableFrom(fr.traqueur.commands.api.arguments.Infinite.class)) { + return Infinite.INSTANCE; + } + return new Simple(clazz); + } + +} 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..362e5fc --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/Infinite.java @@ -0,0 +1,3 @@ +package fr.traqueur.commands.api.arguments; + +public interface Infinite { } diff --git a/core/src/main/java/fr/traqueur/commands/api/parsing/ArgumentParser.java b/core/src/main/java/fr/traqueur/commands/api/parsing/ArgumentParser.java new file mode 100644 index 0000000..c45f9bb --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/parsing/ArgumentParser.java @@ -0,0 +1,22 @@ +package fr.traqueur.commands.api.parsing; + +import fr.traqueur.commands.api.models.Command; + +/** + * Interface for platform-specific argument parsing. + * + * @param plugin type + * @param sender type + * @param context type (String[] for text commands, SlashCommandInteractionEvent for JDA) + */ +public interface ArgumentParser { + + /** + * Parse arguments from the given context. + * + * @param command the command being executed + * @param context the parsing context (raw args or event) + * @return the parse result + */ + ParseResult parse(Command command, C context); +} \ No newline at end of file diff --git a/core/src/main/java/fr/traqueur/commands/api/parsing/ParseError.java b/core/src/main/java/fr/traqueur/commands/api/parsing/ParseError.java new file mode 100644 index 0000000..eeeb7d8 --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/parsing/ParseError.java @@ -0,0 +1,36 @@ +package fr.traqueur.commands.api.parsing; + +public record ParseError(Type type, + String argumentName, + String input, + String message) { + + public enum Type { + TYPE_NOT_FOUND, + CONVERSION_FAILED, + ARGUMENT_TOO_LONG, + MISSING_REQUIRED, + INVALID_FORMAT + } + + + public static ParseError typeNotFound(String argName, String typeKey) { + return new ParseError(Type.TYPE_NOT_FOUND, argName, typeKey, + "Unknown argument type: " + typeKey); + } + + public static ParseError conversionFailed(String argName, String input) { + return new ParseError(Type.CONVERSION_FAILED, argName, input, + "Failed to convert '" + input + "' for argument '" + argName + "'"); + } + + public static ParseError tooLong(String argName) { + return new ParseError(Type.ARGUMENT_TOO_LONG, argName, null, + "Argument '" + argName + "' exceeds maximum length"); + } + + public static ParseError missingRequired(String argName) { + return new ParseError(Type.MISSING_REQUIRED, argName, null, + "Missing required argument: " + argName); + } +} diff --git a/core/src/main/java/fr/traqueur/commands/api/parsing/ParseResult.java b/core/src/main/java/fr/traqueur/commands/api/parsing/ParseResult.java new file mode 100644 index 0000000..16a7b0e --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/parsing/ParseResult.java @@ -0,0 +1,34 @@ +package fr.traqueur.commands.api.parsing; + +import fr.traqueur.commands.api.arguments.Arguments; + +public record ParseResult( + Arguments arguments, + ParseError error, + int consumedCount +) { + + public boolean isSuccess() { + return error == null && arguments != null; + } + + public boolean isError() { + return error != null; + } + + /** + * Create a successful result. + */ + public static ParseResult success(Arguments args, int consumed) { + return new ParseResult(args, null, consumed); + } + + /** + * Create an error result. + */ + public static ParseResult error(ParseError error) { + return new ParseResult(null, error, 0); + } + + +} diff --git a/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java b/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java new file mode 100644 index 0000000..04afff0 --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java @@ -0,0 +1,133 @@ +package fr.traqueur.commands.impl.parsing; + +import fr.traqueur.commands.api.arguments.*; +import fr.traqueur.commands.api.logging.Logger; +import fr.traqueur.commands.api.models.Command; +import fr.traqueur.commands.api.parsing.ArgumentParser; +import fr.traqueur.commands.api.parsing.ParseError; +import fr.traqueur.commands.api.parsing.ParseResult; + +import java.util.List; +import java.util.Map; + +/** + * Default parser for text-based commands (Spigot, Velocity). + * Parses String[] arguments using registered converters. + */ +public class DefaultArgumentParser implements ArgumentParser { + + private static final int MAX_INFINITE_LENGTH = 10_000; + + private final Map> typeConverters; + private final Logger logger; + + public DefaultArgumentParser(Map> typeConverters, Logger logger) { + this.typeConverters = typeConverters; + this.logger = logger; + } + + @Override + public ParseResult parse(Command command, String[] rawArgs) { + Arguments arguments = new Arguments(logger); + + List> required = command.getArgs(); + List> optional = command.getOptinalArgs(); + + int argIndex = 0; + + // Parse required arguments + for (Argument arg : required) { + if (arg.isInfinite()) { + return parseInfinite(arguments, arg, rawArgs, argIndex); + } + + if (argIndex >= rawArgs.length) { + return ParseResult.error(new ParseError( + ParseError.Type.MISSING_REQUIRED, + arg.name(), + null, + "Missing required argument: " + arg.name() + )); + } + + ParseResult result = parseSingle(arguments, arg, rawArgs[argIndex]); + if (result.isError()) { + return result; + } + argIndex++; + } + + // Parse optional arguments + for (Argument arg : optional) { + if (argIndex >= rawArgs.length) { + break; + } + + if (arg.isInfinite()) { + return parseInfinite(arguments, arg, rawArgs, argIndex); + } + + ParseResult result = parseSingle(arguments, arg, rawArgs[argIndex]); + if (result.isError()) { + return result; + } + argIndex++; + } + + return ParseResult.success(arguments, argIndex); + } + + private ParseResult parseSingle(Arguments arguments, Argument arg, String input) { + String typeKey = arg.type().key(); + ArgumentConverter.Wrapper wrapper = typeConverters.get(typeKey); + + if (wrapper == null) { + return ParseResult.error(new ParseError( + ParseError.Type.TYPE_NOT_FOUND, + arg.name(), + input, + "No converter for type: " + typeKey + )); + } + + if (!wrapper.convertAndApply(input, arg.name(), arguments)) { + return ParseResult.error(new ParseError( + ParseError.Type.CONVERSION_FAILED, + arg.name(), + input, + "Failed to convert: " + input + )); + } + + return ParseResult.success(arguments, 1); + } + + private ParseResult parseInfinite(Arguments arguments, Argument arg, String[] rawArgs, int startIndex) { + if (startIndex >= rawArgs.length) { + arguments.add(arg.name(), String.class, ""); + return ParseResult.success(arguments, 0); + } + + StringBuilder sb = new StringBuilder(); + int count = 0; + + for (int i = startIndex; i < rawArgs.length; i++) { + if (sb.length() > MAX_INFINITE_LENGTH) { + return ParseResult.error(new ParseError( + ParseError.Type.ARGUMENT_TOO_LONG, + arg.name(), + null, + "Infinite argument exceeds max length" + )); + } + if (i > startIndex) { + sb.append(" "); + } + sb.append(rawArgs[i]); + count++; + } + + arguments.add(arg.name(), String.class, sb.toString()); + return ParseResult.success(arguments, count); + } +} \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java new file mode 100644 index 0000000..695d8b9 --- /dev/null +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java @@ -0,0 +1,137 @@ +package fr.traqueur.commands.jda; + +import fr.traqueur.commands.api.arguments.Argument; +import fr.traqueur.commands.api.arguments.ArgumentType; +import fr.traqueur.commands.api.exceptions.ArgumentIncorrectException; +import fr.traqueur.commands.api.logging.Logger; +import fr.traqueur.commands.api.models.Command; +import fr.traqueur.commands.api.parsing.ArgumentParser; +import fr.traqueur.commands.api.parsing.ParseError; +import fr.traqueur.commands.api.parsing.ParseResult; +import net.dv8tion.jda.api.entities.*; +import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; + +import java.util.ArrayList; +import java.util.List; + +/** + * JDA-specific argument parser that uses Discord's OptionMapping. + * Discord handles type resolution natively, so no string conversion needed. + */ +public class JDAArgumentParser implements ArgumentParser { + + private final Logger logger; + + public JDAArgumentParser(Logger logger) { + this.logger = logger; + } + + @Override + public ParseResult parse(Command command, SlashCommandInteractionEvent event) { + JDAArguments arguments = new JDAArguments(logger, event); + List options = event.getOptions(); + + List> allArgs = new ArrayList<>(); + allArgs.addAll(command.getArgs()); + allArgs.addAll(command.getOptinalArgs()); + + // Validate argument count + if (options.size() < command.getArgs().size()) { + return ParseResult.error(new ParseError( + ParseError.Type.MISSING_REQUIRED, + null, + null, + "Not enough arguments provided" + )); + } + + int maxOptional = command.getOptinalArgs().size(); + int providedOptional = options.size() - command.getArgs().size(); + if (providedOptional > maxOptional) { + return ParseResult.error(new ParseError( + ParseError.Type.INVALID_FORMAT, + null, + null, + "Too many arguments provided" + )); + } + + // Parse each option + for (int i = 0; i < options.size(); i++) { + OptionMapping option = options.get(i); + Argument arg = allArgs.get(i); + + try { + populateArgument(arguments, option, arg); + } catch (ArgumentIncorrectException e) { + return ParseResult.error(new ParseError( + ParseError.Type.CONVERSION_FAILED, + option.getName(), + null, + e.getMessage() + )); + } + } + + return ParseResult.success(arguments, options.size()); + } + + private void populateArgument(JDAArguments arguments, OptionMapping option, + Argument arg) { + String name = option.getName(); + + switch (option.getType()) { + case STRING -> arguments.add(name, String.class, option.getAsString()); + + case INTEGER -> { + if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { + throw new ArgumentIncorrectException(name); + } + if (clazz == Integer.class || clazz == int.class) { + arguments.add(name, Integer.class, option.getAsInt()); + } else if (clazz == Long.class || clazz == long.class) { + arguments.add(name, Long.class, option.getAsLong()); + } else { + throw new ArgumentIncorrectException(name); + } + } + + case NUMBER -> { + if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { + throw new ArgumentIncorrectException(name); + } + if (clazz == Double.class || clazz == double.class) { + arguments.add(name, Double.class, option.getAsDouble()); + } else if (clazz == Float.class || clazz == float.class) { + arguments.add(name, Float.class, (float) option.getAsDouble()); + } else { + throw new ArgumentIncorrectException(name); + } + } + + case BOOLEAN -> arguments.add(name, Boolean.class, option.getAsBoolean()); + + case USER -> { + if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { + throw new ArgumentIncorrectException(name); + } + if (clazz == Member.class) { + arguments.add(name, Member.class, option.getAsMember()); + } else if (clazz == User.class) { + arguments.add(name, User.class, option.getAsUser()); + } else { + throw new ArgumentIncorrectException(name); + } + } + + case ROLE -> arguments.add(name, Role.class, option.getAsRole()); + case CHANNEL -> arguments.add(name, GuildChannelUnion.class, option.getAsChannel()); + case MENTIONABLE -> arguments.add(name, IMentionable.class, option.getAsMentionable()); + case ATTACHMENT -> arguments.add(name, Message.Attachment.class, option.getAsAttachment()); + + default -> { /* Unknown type, skip */ } + } + } +} \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java index aa07993..ac7d14e 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java @@ -1,170 +1,96 @@ package fr.traqueur.commands.jda; 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.exceptions.ArgumentIncorrectException; import fr.traqueur.commands.api.models.Command; import fr.traqueur.commands.api.models.collections.CommandTree; +import fr.traqueur.commands.api.parsing.ParseResult; import fr.traqueur.commands.api.requirements.Requirement; -import net.dv8tion.jda.api.entities.*; -import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; /** * JDA executor that handles slash command events. - * This class listens for SlashCommandInteractionEvent and routes them to the appropriate command. - * - * @param The type of the bot instance. */ public class JDAExecutor extends ListenerAdapter { - /** - * The command manager. - */ private final CommandManager commandManager; + private final JDAArgumentParser parser; - /** - * Constructor for JDAExecutor. - * - * @param commandManager The command manager. - */ public JDAExecutor(CommandManager commandManager) { this.commandManager = commandManager; + this.parser = new JDAArgumentParser<>(commandManager.getLogger()); } @Override public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) { String label = buildLabel(event); - logDebugIfEnabled(label); - Optional> commandOpt = findCommand(event, label); - if (commandOpt.isEmpty()) { - return; - } - - Command command = commandOpt.get(); - - if (!validateCommand(event, command)) { - return; - } - - JDAArguments jdaArguments = createArguments(event, command); - executeCommand(event, command, jdaArguments, label); - } - - /** - * Log debug message if debug mode is enabled. - * - * @param label The command label. - */ - private void logDebugIfEnabled(String label) { if (commandManager.isDebug()) { commandManager.getLogger().info("Received slash command: " + label); } - } - /** - * Find the command in the tree. - * - * @param event The slash command event. - * @param label The command label. - * @return The command if found, empty otherwise. - */ - private Optional> findCommand( - SlashCommandInteractionEvent event, String label) { + // Find command String[] labelParts = label.split("\\."); Optional> found = commandManager.getCommands().findNode(labelParts); if (found.isEmpty()) { event.reply("Command not found!").setEphemeral(true).queue(); - return Optional.empty(); + return; } - CommandTree.MatchResult result = found.get(); - CommandTree.CommandNode node = result.node(); - Optional> cmdOpt = node.getCommand(); - - if (cmdOpt.isEmpty()) { + Command command = found.get().node().getCommand().orElse(null); + if (command == null) { event.reply("Command implementation not found!").setEphemeral(true).queue(); - return Optional.empty(); + return; } - return cmdOpt; - } - - /** - * Validate command execution conditions (game-only, permissions, requirements). - * - * @param event The slash command event. - * @param command The command to validate. - * @return true if validation passed, false otherwise. - */ - private boolean validateCommand(SlashCommandInteractionEvent event, - Command command) { - if (!checkGameOnly(event, command)) { - return false; + // Validate + if (!validateCommand(event, command)) { + return; } - if (!checkPermissions(event, command)) { - return false; + // Parse & Execute + ParseResult result = parser.parse(command, event); + + if (result.isError()) { + String msg = commandManager.getMessageHandler().getArgNotRecognized() + .replace("%arg%", result.error().argumentName() != null ? result.error().argumentName() : "unknown"); + event.reply(msg).setEphemeral(true).queue(); + return; } - return checkRequirements(event, command); + try { + command.execute(event, result.arguments()); + } catch (Exception e) { + commandManager.getLogger().error("Error executing command " + label + ": " + e.getMessage()); + if (!event.isAcknowledged()) { + event.reply("An error occurred!").setEphemeral(true).queue(); + } + } } - /** - * Check if command is game-only (guild-only in Discord context). - * - * @param event The slash command event. - * @param command The command to check. - * @return true if check passed, false otherwise. - */ - private boolean checkGameOnly(SlashCommandInteractionEvent event, - Command command) { + private boolean validateCommand(SlashCommandInteractionEvent event, + Command command) { + // Game-only check if (command.inGameOnly() && !event.isFromGuild()) { event.reply(commandManager.getMessageHandler().getOnlyInGameMessage()) .setEphemeral(true).queue(); return false; } - return true; - } - /** - * Check command permissions. - * - * @param event The slash command event. - * @param command The command to check. - * @return true if check passed, false otherwise. - */ - private boolean checkPermissions(SlashCommandInteractionEvent event, - Command command) { + // Permission check String perm = command.getPermission(); if (!perm.isEmpty() && !commandManager.getPlatform().hasPermission(event, perm)) { event.reply(commandManager.getMessageHandler().getNoPermissionMessage()) .setEphemeral(true).queue(); return false; } - return true; - } - /** - * Check command requirements. - * - * @param event The slash command event. - * @param command The command to check. - * @return true if all requirements passed, false otherwise. - */ - private boolean checkRequirements(SlashCommandInteractionEvent event, - Command command) { + // Requirements check for (Requirement req : command.getRequirements()) { if (!req.check(event)) { String msg = req.errorMessage().isEmpty() @@ -175,171 +101,18 @@ private boolean checkRequirements(SlashCommandInteractionEvent event, return false; } } - return true; - } - - /** - * Create JDAArguments from event options. - * - * @param event The slash command event. - * @return The populated JDAArguments. - */ - private JDAArguments createArguments(SlashCommandInteractionEvent event, Command command) { - JDAArguments jdaArguments = new JDAArguments(commandManager.getLogger(), event); - List options = event.getOptions(); - - List> args = new ArrayList<>(); - args.addAll(command.getArgs()); - args.addAll(command.getOptinalArgs()); - - if (options.size() < command.getArgs().size()) { - throw new IllegalArgumentException("No enough arguments"); - } - int sizeWithoutRequired = options.size() - command.getArgs().size(); - if (sizeWithoutRequired > command.getOptinalArgs().size()) { - throw new IllegalArgumentException("More options than expected"); - } - - for (int i = 0; i < options.size(); i++) { - OptionMapping optionMapping = options.get(i); - Argument arg = args.get(i); - populateArgument(jdaArguments, optionMapping, arg); - } - - return jdaArguments; - } - - /** - * Populate a single argument based on option type. - * - * @param arguments The arguments container. - * @param option The option to populate from. - */ - private void populateArgument(JDAArguments arguments, OptionMapping option, Argument arg) { - String name = option.getName(); - switch (option.getType()) { - case STRING: - arguments.add(name, String.class, option.getAsString()); - break; - case INTEGER: - if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { - throw new ArgumentIncorrectException(option.getName()); - } - if (clazz == Integer.class) { - arguments.add(name, Integer.class, option.getAsInt()); - } else if (clazz == int.class) { - arguments.add(name, int.class, option.getAsInt()); - } else if (clazz == Long.class) { - arguments.add(name, Long.class, option.getAsLong()); - } else if (clazz == long.class) { - arguments.add(name, long.class, option.getAsLong()); - } else { - throw new ArgumentIncorrectException(option.getName()); - } - break; - case NUMBER: - if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { - throw new ArgumentIncorrectException(option.getName()); - } - if (clazz == Double.class) { - arguments.add(name, Double.class, option.getAsDouble()); - } else if (clazz == double.class) { - arguments.add(name, double.class, option.getAsDouble()); - } else if (clazz == Float.class) { - arguments.add(name, Float.class, (float) option.getAsDouble()); - } else if (clazz == float.class) { - arguments.add(name, float.class, (float) option.getAsDouble()); - } else { - throw new ArgumentIncorrectException(option.getName()); - } - break; - case BOOLEAN: - arguments.add(name, Boolean.class, option.getAsBoolean()); - break; - case USER: - if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { - throw new ArgumentIncorrectException(option.getName()); - } - if (clazz == Member.class) { - arguments.add(name, Member.class, option.getAsMember()); - } else if (clazz == User.class) { - arguments.add(name, User.class, option.getAsUser()); - } else { - throw new ArgumentIncorrectException(option.getName()); - } - break; - case ROLE: - arguments.add(name, Role.class, option.getAsRole()); - break; - case CHANNEL: - arguments.add(name, GuildChannelUnion.class, option.getAsChannel()); - break; - case MENTIONABLE: - arguments.add(name, IMentionable.class, option.getAsMentionable()); - break; - case ATTACHMENT: - arguments.add(name, Message.Attachment.class, option.getAsAttachment()); - break; - default: - break; - } - } - - /** - * Execute the command with error handling. - * - * @param event The slash command event. - * @param command The command to execute. - * @param arguments The command arguments. - * @param label The command label for logging. - */ - private void executeCommand(SlashCommandInteractionEvent event, - Command command, - JDAArguments arguments, String label) { - try { - command.execute(event, arguments); - } catch (Exception e) { - handleCommandError(event, label, e); - } - } - /** - * Handle command execution errors. - * - * @param event The slash command event. - * @param label The command label. - * @param e The exception that occurred. - */ - private void handleCommandError(SlashCommandInteractionEvent event, String label, Exception e) { - commandManager.getLogger().error("Error executing command " + label + ": " + e.getMessage()); - e.printStackTrace(); - if (!event.isAcknowledged()) { - event.reply("An error occurred while executing this command!") - .setEphemeral(true).queue(); - } + return true; } - /** - * Build a label from a slash command event. - * Examples: - * - /ping -> "ping" - * - /math add -> "math.add" - * - /admin users kick -> "admin.users.kick" - * - * @param event The slash command event. - * @return The constructed label. - */ private String buildLabel(SlashCommandInteractionEvent event) { StringBuilder label = new StringBuilder(event.getName()); - if (event.getSubcommandGroup() != null) { label.append(".").append(event.getSubcommandGroup()); } - if (event.getSubcommandName() != null) { label.append(".").append(event.getSubcommandName()); } - return label.toString().toLowerCase(); } } \ No newline at end of file From 86f30dbde0d72918182ba89abcb481cb165857ce Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 16:39:51 +0100 Subject: [PATCH 03/23] feat: optimization and aliases management and enable runtime command --- .../traqueur/commands/api/CommandManager.java | 29 +++---- .../commands/api/logging/MessageHandler.java | 6 ++ .../traqueur/commands/api/models/Command.java | 40 +++++++-- .../commands/api/models/CommandInvoker.java | 84 ++++++++++++------- .../impl/logging/InternalMessageHandler.java | 8 ++ .../impl/parsing/DefaultArgumentParser.java | 2 +- .../commands/api/models/CommandTest.java | 4 +- .../commands/jda/JDAArgumentParser.java | 70 +++++++--------- .../fr/traqueur/commands/jda/JDAExecutor.java | 7 ++ .../fr/traqueur/commands/jda/JDAPlatform.java | 6 +- 10 files changed, 158 insertions(+), 98 deletions(-) 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 1287abf..a6ec404 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -138,9 +138,9 @@ public boolean isDebug() { * @param command The command to register. */ public void registerCommand(Command command) { - for (String alias : command.getAliases()) { - this.addCommand(command, alias); - this.registerSubCommands(alias, command.getSubcommands()); + for (String label : command.getAllLabels()) { + this.addCommand(command, label); + this.registerSubCommands(label, command.getSubcommands()); } } @@ -182,12 +182,11 @@ public void unregisterCommand(Command command) { * @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); + List labels = new ArrayList<>(command.getAllLabels()); + for (String label : labels) { + this.removeCommand(label, subcommands); if(subcommands) { - this.unregisterSubCommands(alias, command.getSubcommands()); + this.unregisterSubCommands(label, command.getSubcommands()); } } } @@ -266,8 +265,7 @@ private void registerSubCommands(String parentLabel, List> subcomm return; } for (Command subcommand : subcommands) { - // getAliases() already returns [name, ...aliases], so no need to add the name again - List aliasesSub = new ArrayList<>(subcommand.getAliases()); + List aliasesSub = new ArrayList<>(subcommand.getAllLabels()); for (String aliasSub : aliasesSub) { this.addCommand(subcommand, parentLabel + "." + aliasSub); this.registerSubCommands(parentLabel + "." + aliasSub, subcommand.getSubcommands()); @@ -285,11 +283,10 @@ private void unregisterSubCommands(String parentLabel, List> subcom 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()); + List labelsSub = subcommand.getAllLabels(); + for (String labelSub : labelsSub) { + this.removeCommand(parentLabel + "." + labelSub, true); + this.unregisterSubCommands(parentLabel + "." + labelSub, subcommand.getSubcommands()); } } } @@ -315,7 +312,7 @@ private void addCommand(Command command, String label) { 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; 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..07f6d69 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 @@ -31,4 +31,10 @@ public interface MessageHandler { * @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 2abc942..85d9cbf 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 @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -83,6 +84,8 @@ public abstract class Command { */ private boolean infiniteArgs; + private boolean enable; + /** * If the command is subcommand */ @@ -106,6 +109,7 @@ public Command(T plugin, String name) { this.optionalArgs = new ArrayList<>(); this.requirements = new ArrayList<>(); this.subcommand = false; + this.enable = true; } /** @@ -178,12 +182,16 @@ public final String getUsage() { * @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; } @@ -207,7 +215,7 @@ public final List> getArgs() { * This method is called to get the optional arguments of the command. * @return The optional arguments of the command. */ - public final List> getOptinalArgs() { + public final List> getOptionalArgs() { return optionalArgs; } @@ -441,7 +449,7 @@ public String generateDefaultUsage(S sender, String label) { usage.append(subs).append(">"); } - if (!this.getArgs().isEmpty() || !this.getOptinalArgs().isEmpty()) { + if (!this.getArgs().isEmpty() || !this.getOptionalArgs().isEmpty()) { usage.append(!directSubs.isEmpty() ? "|" : " "); // arguments obligatoires : @@ -451,11 +459,11 @@ public String generateDefaultUsage(S sender, String label) { usage.append(req); // arguments optionnels : [name:type] - if (!this.getOptinalArgs().isEmpty()) { + if (!this.getOptionalArgs().isEmpty()) { if (!req.isEmpty()) { usage.append(" "); } - String opt = this.getOptinalArgs().stream() + String opt = this.getOptionalArgs().stream() .map(arg -> "[" + arg.canonicalName() + "]") .collect(Collectors.joining(" ")); usage.append(opt); @@ -465,6 +473,22 @@ public String generateDefaultUsage(S sender, String label) { return usage.toString(); } + /** + * Change the state of the command + * @param state the new state for the command + */ + public void setEnabled(boolean state) { + this.enable = state; + } + + /** + * Check if the command is enabled + * @return if the command is enabled + */ + public boolean isEnabled() { + return enable; + } + /** * Set if the command is subcommand */ diff --git a/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java b/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java index bf84508..dca9049 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java @@ -10,33 +10,27 @@ import fr.traqueur.commands.api.requirements.Requirement; import java.util.*; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; /** * CommandInvoker is responsible for invoking and suggesting commands. * It performs lookup, permission and requirement checks, usage display, parsing, and execution. - * + * @param manager the command manager to use for command handling * @param plugin type * @param sender type */ -public class CommandInvoker { +public record CommandInvoker(CommandManager manager) { - private final CommandManager manager; - - /** - * Constructs a CommandInvoker with the given command manager. - * @param manager the command manager to use for command handling - */ - public CommandInvoker(CommandManager manager) { - this.manager = manager; - } + private static final Pattern DOT_PATTERN = Pattern.compile("\\."); /** * Invokes a command based on the provided source, base label, and raw arguments. - * @param base the base command label (e.g. "hello") + * + * @param base the base command label (e.g. "hello") * @param rawArgs the arguments of the command - * @param source the command sender (e.g. a player or console) + * @param source the command sender (e.g. a player or console) * @return true if a command handler was executed or a message sent; false if command not found */ public boolean invoke(S source, String base, String[] rawArgs) { @@ -56,7 +50,8 @@ public boolean invoke(S source, String base, String[] rawArgs) { /** * Find and prepare command context. - * @param base the base command label + * + * @param base the base command label * @param rawArgs the raw arguments * @return the command context if found */ @@ -82,21 +77,39 @@ private Optional> findCommandContext(String base, String[] } /** - * Validate command execution conditions (in-game, permissions, requirements, usage). - * @param source the command sender + * Validate command execution conditions (enabled, in-game, permissions, requirements, usage). + * + * @param source the command sender * @param context the command context * @return true if all validations passed, false otherwise (message already sent to user) */ private boolean validateCommandExecution(S source, CommandContext context) { - return checkInGameOnly(source, context.command) + return checkEnabled(source, context.command) + && checkInGameOnly(source, context.command) && checkPermission(source, context.command) && checkRequirements(source, context.command) && checkUsage(source, context); } + /** + * Check if command is enabled. + * + * @param source the command sender + * @param command the command to check + * @return true if command is enabled + */ + private boolean checkEnabled(S source, Command command) { + if (!command.isEnabled()) { + manager.getPlatform().sendMessage(source, manager.getMessageHandler().getCommandDisabledMessage()); + return false; + } + return true; + } + /** * Check if command requires in-game execution. - * @param source the command sender + * + * @param source the command sender * @param command the command to check * @return true if check passed or not applicable */ @@ -110,7 +123,8 @@ private boolean checkInGameOnly(S source, Command command) { /** * Check if sender has required permission. - * @param source the command sender + * + * @param source the command sender * @param command the command to check * @return true if check passed or no permission required */ @@ -125,7 +139,8 @@ private boolean checkPermission(S source, Command command) { /** * Check if all requirements are satisfied. - * @param source the command sender + * + * @param source the command sender * @param command the command to check * @return true if all requirements passed */ @@ -142,6 +157,7 @@ private boolean checkRequirements(S source, Command command) { /** * Build error message for failed requirement. + * * @param req the failed requirement * @return the error message */ @@ -154,7 +170,8 @@ private String buildRequirementMessage(Requirement req) { /** * Check if argument count is valid. - * @param source the command sender + * + * @param source the command sender * @param context the command context * @return true if usage is correct */ @@ -163,7 +180,7 @@ private boolean checkUsage(S source, CommandContext context) { String[] args = context.args; int min = command.getArgs().size(); - int max = command.isInfiniteArgs() ? Integer.MAX_VALUE : min + command.getOptinalArgs().size(); + int max = command.isInfiniteArgs() ? Integer.MAX_VALUE : min + command.getOptionalArgs().size(); if (args.length < min || args.length > max) { String usage = buildUsageMessage(source, context); @@ -175,7 +192,8 @@ private boolean checkUsage(S source, CommandContext context) { /** * Build usage message for command. - * @param source the command sender + * + * @param source the command sender * @param context the command context * @return the usage message */ @@ -190,7 +208,8 @@ private String buildUsageMessage(S source, CommandContext context) { /** * Execute the command with error handling. - * @param source the command sender + * + * @param source the command sender * @param context the command context * @return true if execution succeeded or error was handled, false for internal errors */ @@ -208,6 +227,7 @@ private boolean executeCommand(S source, CommandContext context) { /** * Handle type argument not exist error. + * * @param source the command sender * @return false to indicate internal error */ @@ -218,8 +238,9 @@ private boolean handleTypeArgumentError(S source) { /** * Handle incorrect argument error. + * * @param source the command sender - * @param e the exception + * @param e the exception * @return true to indicate error was handled */ private boolean handleArgumentIncorrectError(S source, ArgumentIncorrectException e) { @@ -231,9 +252,10 @@ private boolean handleArgumentIncorrectError(S source, ArgumentIncorrectExceptio /** * Suggests command completions based on the provided source, base label, and arguments. * This method checks for available tab completers and filters suggestions based on the current input. + * * @param source the command sender (e.g. a player or console) - * @param base the command label (e.g. "hello") - * @param args the arguments provided to the command + * @param base the command label (e.g. "hello") + * @param args the arguments provided to the command * @return the list of suggestion */ public List suggest(S source, String base, String[] args) { @@ -274,9 +296,11 @@ public List suggest(S source, String base, String[] args) { private boolean allowedSuggestion(S src, String label, String opt) { String full = label + "." + opt.toLowerCase(); - Optional> copt = manager.getCommands().findNode(full.split("\\.")).flatMap(r -> r.node().getCommand()); - if (!copt.isPresent()) return true; - Command c = copt.get(); + Optional> copt = manager.getCommands() + .findNode(DOT_PATTERN.split(full)) + .flatMap(r -> r.node().getCommand()); + if (copt.isEmpty()) return true; + Command c = copt.get(); return c.getRequirements().stream().allMatch(r -> r.check(src)) && (c.getPermission().isEmpty() || manager.getPlatform().hasPermission(src, c.getPermission())); } diff --git a/core/src/main/java/fr/traqueur/commands/impl/logging/InternalMessageHandler.java b/core/src/main/java/fr/traqueur/commands/impl/logging/InternalMessageHandler.java index 27101d3..044bcbd 100644 --- a/core/src/main/java/fr/traqueur/commands/impl/logging/InternalMessageHandler.java +++ b/core/src/main/java/fr/traqueur/commands/impl/logging/InternalMessageHandler.java @@ -45,4 +45,12 @@ public String getRequirementMessage() { return "The requirement %requirement% was not met"; } + /** + * {@inheritDoc} + */ + @Override + public String getCommandDisabledMessage() { + return "&cThis command is currently disabled."; + } + } diff --git a/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java b/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java index 04afff0..f66249d 100644 --- a/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java +++ b/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java @@ -31,7 +31,7 @@ public ParseResult parse(Command command, String[] rawArgs) { Arguments arguments = new Arguments(logger); List> required = command.getArgs(); - List> optional = command.getOptinalArgs(); + List> optional = command.getOptionalArgs(); int argIndex = 0; diff --git a/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java b/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java index bba621e..c790446 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java @@ -77,10 +77,10 @@ public void removeCommand(String label, boolean subcommand) { @Test void testAliasesAndName() { assertEquals("dummy", cmd.getName()); - assertEquals(1, cmd.getAliases().size()); + assertEquals(0, cmd.getAliases().size()); cmd.addAlias("d1", "d2"); List aliases = cmd.getAliases(); - assertEquals(3, cmd.getAliases().size()); + assertEquals(2, cmd.getAliases().size()); assertTrue(aliases.contains("d1")); assertTrue(aliases.contains("d2")); } diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java index 695d8b9..7aa3e8b 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java @@ -20,71 +20,65 @@ * JDA-specific argument parser that uses Discord's OptionMapping. * Discord handles type resolution natively, so no string conversion needed. */ -public class JDAArgumentParser implements ArgumentParser { - - private final Logger logger; - - public JDAArgumentParser(Logger logger) { - this.logger = logger; - } - +public record JDAArgumentParser(Logger logger) implements ArgumentParser { + @Override public ParseResult parse(Command command, SlashCommandInteractionEvent event) { JDAArguments arguments = new JDAArguments(logger, event); List options = event.getOptions(); - + List> allArgs = new ArrayList<>(); allArgs.addAll(command.getArgs()); - allArgs.addAll(command.getOptinalArgs()); - + allArgs.addAll(command.getOptionalArgs()); + // Validate argument count if (options.size() < command.getArgs().size()) { return ParseResult.error(new ParseError( - ParseError.Type.MISSING_REQUIRED, - null, - null, - "Not enough arguments provided" + ParseError.Type.MISSING_REQUIRED, + null, + null, + "Not enough arguments provided" )); } - - int maxOptional = command.getOptinalArgs().size(); + + int maxOptional = command.getOptionalArgs().size(); int providedOptional = options.size() - command.getArgs().size(); if (providedOptional > maxOptional) { return ParseResult.error(new ParseError( - ParseError.Type.INVALID_FORMAT, - null, - null, - "Too many arguments provided" + ParseError.Type.INVALID_FORMAT, + null, + null, + "Too many arguments provided" )); } - + // Parse each option for (int i = 0; i < options.size(); i++) { OptionMapping option = options.get(i); Argument arg = allArgs.get(i); - + try { populateArgument(arguments, option, arg); } catch (ArgumentIncorrectException e) { return ParseResult.error(new ParseError( - ParseError.Type.CONVERSION_FAILED, - option.getName(), - null, - e.getMessage() + ParseError.Type.CONVERSION_FAILED, + option.getName(), + null, + e.getMessage() )); } } - + return ParseResult.success(arguments, options.size()); } - - private void populateArgument(JDAArguments arguments, OptionMapping option, - Argument arg) { + + private void populateArgument(JDAArguments arguments, OptionMapping option, + Argument arg) { String name = option.getName(); - + switch (option.getType()) { case STRING -> arguments.add(name, String.class, option.getAsString()); - + case INTEGER -> { if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { throw new ArgumentIncorrectException(name); @@ -97,7 +91,7 @@ private void populateArgument(JDAArguments arguments, OptionMapping option, throw new ArgumentIncorrectException(name); } } - + case NUMBER -> { if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { throw new ArgumentIncorrectException(name); @@ -110,9 +104,9 @@ private void populateArgument(JDAArguments arguments, OptionMapping option, throw new ArgumentIncorrectException(name); } } - + case BOOLEAN -> arguments.add(name, Boolean.class, option.getAsBoolean()); - + case USER -> { if (!(arg.type() instanceof ArgumentType.Simple(Class clazz))) { throw new ArgumentIncorrectException(name); @@ -125,12 +119,12 @@ private void populateArgument(JDAArguments arguments, OptionMapping option, throw new ArgumentIncorrectException(name); } } - + case ROLE -> arguments.add(name, Role.class, option.getAsRole()); case CHANNEL -> arguments.add(name, GuildChannelUnion.class, option.getAsChannel()); case MENTIONABLE -> arguments.add(name, IMentionable.class, option.getAsMentionable()); case ATTACHMENT -> arguments.add(name, Message.Attachment.class, option.getAsAttachment()); - + default -> { /* Unknown type, skip */ } } } diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java index ac7d14e..56a5417 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java @@ -75,6 +75,13 @@ public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent even private boolean validateCommand(SlashCommandInteractionEvent event, Command command) { + // Enabled check + if (!command.isEnabled()) { + event.reply(commandManager.getMessageHandler().getCommandDisabledMessage()) + .setEphemeral(true).queue(); + return false; + } + // Game-only check if (command.inGameOnly() && !event.isFromGuild()) { event.reply(commandManager.getMessageHandler().getOnlyInGameMessage()) diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java index 1188af9..ca38f72 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java @@ -150,7 +150,7 @@ public void addCommand(Command command, String addArgumentsToSubcommand(subcommand, command); slashCommand.addSubcommands(subcommand); - } else if (parts.length >= 3) { + } else { // Subcommand group: /command group subcommand String groupName = parts[1].toLowerCase(); String subName = parts[2].toLowerCase(); @@ -198,7 +198,7 @@ public void removeCommand(String label, boolean subcommand) { */ private void addArgumentsToCommand(SlashCommandData slashCommand, Command command) { List> args = command.getArgs(); - List> optionalArgs = command.getOptinalArgs(); + List> optionalArgs = command.getOptionalArgs(); for (Argument arg : args) { OptionData option = createOptionData(arg, true); @@ -219,7 +219,7 @@ private void addArgumentsToCommand(SlashCommandData slashCommand, Command command) { List> args = command.getArgs(); - List> optionalArgs = command.getOptinalArgs(); + List> optionalArgs = command.getOptionalArgs(); for (Argument arg : args) { OptionData option = createOptionData(arg, true); From 3cc59a87767c3a2d3767a4dded2fc5515c86fc2e Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 16:49:27 +0100 Subject: [PATCH 04/23] feat: continue to use Pattern dot matching precompile --- .../traqueur/commands/api/models/Command.java | 5 +- .../api/models/collections/CommandTree.java | 152 +++++++++++------- 2 files changed, 98 insertions(+), 59 deletions(-) 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 85d9cbf..4f8f8d1 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 @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -22,6 +23,8 @@ */ public abstract class Command { + private static final Pattern DOT_PATTERN = Pattern.compile("\\."); + private CommandManager manager; /** @@ -430,7 +433,7 @@ public final T getPlugin() { public String generateDefaultUsage(S sender, String label) { StringBuilder usage = new StringBuilder("/"); - String[] parts = label.split("\\."); + String[] parts = DOT_PATTERN.split(label); usage.append(String.join(" ", parts)); List> directSubs = this.getSubcommands().stream() diff --git a/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java b/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java index 1302e94..97559cd 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java @@ -3,21 +3,41 @@ import fr.traqueur.commands.api.models.Command; import java.util.*; +import java.util.regex.Pattern; /** * A prefix-tree of commands, supporting nested labels and argument fallback. + * * @param type of the command context * @param type of the command sender */ public class CommandTree { + /** + * Pre-compiled pattern for splitting labels. + */ + private static final Pattern DOT_PATTERN = Pattern.compile("\\."); + + /** + * Valid label pattern: starts with letter, followed by letters, digits, underscores, or dots. + * Each segment must start with a letter. + */ + private static final Pattern VALID_LABEL_SEGMENT = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]*$"); + + /** + * Maximum length for a single label segment. + */ + private static final int MAX_SEGMENT_LENGTH = 64; + + /** + * Maximum depth for nested commands. + */ + private static final int MAX_DEPTH = 10; + private CommandNode root; /** * A node representing one segment in the command path. - * Each node can have a command associated with it, - * @param type of the command context - * @param type of the command sender */ public static class CommandNode { @@ -27,81 +47,56 @@ public static class CommandNode { private Command command; private boolean hadChildren = false; - /** Create a new command node with the given label and optional parent. - * @param label the segment label, e.g. "hello" - * @param parent the parent node, or null for root - */ public CommandNode(String label, CommandNode parent) { this.label = label; this.parent = parent; } - /** Get the label of this node segment. - * @return the label like "hello" - */ public String getLabel() { return label; } - /** - * Get the full label path including parent segments. - * @return the full label like "parent.child" - */ public String getFullLabel() { if (parent == null || parent.label == null) return label; return parent.getFullLabel() + "." + label; } - /** Get the command associated with this node, if any. - * @return the command, or empty if not set - */ public Optional> getCommand() { return Optional.ofNullable(command); } - /** Get the parent node, or null if this is the root. - * @return the parent node, or null for root - */ public Map> getChildren() { return Collections.unmodifiableMap(children); } } - public void clear() { - this.root = new CommandNode<>(null, null); - } - - /** - * Create an empty command tree with a root node. - * The root node has no label and serves as the starting point for all commands. + * Result of a lookup: the deepest matching node and leftover args. */ + public record MatchResult(CommandNode node, String[] args) { + } + public CommandTree() { this.root = new CommandNode<>(null, null); } - /** - * Result of a lookup: the deepest matching node and leftover args. - * This is used to find commands based on a base label and raw arguments. - * - * @param type of the command context - * @param type of the command sender - * @param node The node that matched the base label and any subcommands. - * The args are the remaining segments after the match. - * @param args Remaining arguments after the matched node. - * This can be empty if the match was exact. - */ - public record MatchResult(CommandNode node, String[] args) { + public void clear() { + this.root = new CommandNode<>(null, null); } /** * Add or replace a command at the given full label path (dot-separated). - * @param label full path like "hello.sub" + * + * @param label full path like "hello.sub" * @param command the command to attach at that path + * @throws IllegalArgumentException if label is invalid */ public void addCommand(String label, Command command) { - String[] parts = label.split("\\."); + validateLabel(label); + + String[] parts = DOT_PATTERN.split(label); CommandNode node = root; + for (String seg : parts) { String key = seg.toLowerCase(); node.hadChildren = true; @@ -111,12 +106,60 @@ public void addCommand(String label, Command command) { node.command = command; } + /** + * Validate a command label. + * + * @param label the label to validate + * @throws IllegalArgumentException if invalid + */ + private void validateLabel(String label) { + if (label == null || label.isEmpty()) { + throw new IllegalArgumentException("Command label cannot be null or empty"); + } + + String[] segments = DOT_PATTERN.split(label); + + if (segments.length > MAX_DEPTH) { + throw new IllegalArgumentException( + "Command label exceeds max depth (" + MAX_DEPTH + "): " + label + ); + } + + for (String segment : segments) { + validateSegment(segment, label); + } + } + + /** + * Validate a single segment of a label. + * + * @param segment the segment to validate + * @param fullLabel the full label for error messages + * @throws IllegalArgumentException if invalid + */ + private void validateSegment(String segment, String fullLabel) { + if (segment.isEmpty()) { + throw new IllegalArgumentException( + "Command label contains empty segment: " + fullLabel + ); + } + + if (segment.length() > MAX_SEGMENT_LENGTH) { + throw new IllegalArgumentException( + "Command label segment exceeds max length (" + MAX_SEGMENT_LENGTH + "): " + segment + ); + } + + if (!VALID_LABEL_SEGMENT.matcher(segment).matches()) { + throw new IllegalArgumentException( + "Invalid command label segment '" + segment + "' in: " + fullLabel + + ". Segments must start with a letter and contain only letters, digits, or underscores." + ); + } + } + /** * Lookup a base label and raw arguments, returning matching node and leftover args. - * This allows for partial matches where the command may have subcommands. - * @param base the base command label, e.g. "hello" - * @param rawArgs the raw arguments to match against subcommands - * @return an Optional containing the match result, or empty if not found */ public Optional> findNode(String base, String[] rawArgs) { if (base == null) return Optional.empty(); @@ -144,9 +187,6 @@ public Optional> findNode(String base, String[] rawArgs) { /** * Lookup by full path segments, with no leftover args. - * This finds the exact node matching all segments. - * @param segments the path segments like ["root", "sub"] - * @return an Optional containing the match result, or empty if not found */ public Optional> findNode(String[] segments) { if (segments == null || segments.length == 0) return Optional.empty(); @@ -160,11 +200,11 @@ public Optional> findNode(String[] segments) { /** * Remove a command node by its full label. - * @param label full path like "root.sub" - * @param prune if true, remove entire subtree; otherwise just clear the command at that node */ public void removeCommand(String label, boolean prune) { - CommandNode target = this.findNode(label.split("\\.")).map(result -> result.node).orElse(null); + CommandNode target = this.findNode(DOT_PATTERN.split(label)) + .map(MatchResult::node) + .orElse(null); if (target == null) return; if (prune) { @@ -178,7 +218,7 @@ private void pruneSubtree(CommandNode node) { CommandNode parent = node.parent; if (parent != null) { parent.children.remove(node.label); - if(parent.children.isEmpty()){ + if (parent.children.isEmpty()) { parent.hadChildren = false; } } @@ -190,18 +230,14 @@ private void clearOrPruneEmpty(CommandNode node) { CommandNode parent = node.parent; if (parent != null) { parent.children.remove(node.label); - if(parent.children.isEmpty()){ + if (parent.children.isEmpty()) { parent.hadChildren = false; } } } } - /** - * Get the root command node of this tree. - * @return the root node, which has no label - */ public CommandNode getRoot() { return root; } -} +} \ No newline at end of file From be437b48e8ce33776dce3ea5749fc6cbf15943a3 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 16:52:49 +0100 Subject: [PATCH 05/23] feat: add little cache system for player in arguments --- .../arguments/OfflinePlayerArgument.java | 38 +++++++++++++++---- .../spigot/arguments/PlayerArgument.java | 32 +++++++++++++--- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/OfflinePlayerArgument.java b/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/OfflinePlayerArgument.java index 30f6d5f..c3b0c7d 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/OfflinePlayerArgument.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/OfflinePlayerArgument.java @@ -1,30 +1,46 @@ package fr.traqueur.commands.spigot.arguments; import fr.traqueur.commands.api.arguments.ArgumentConverter; +import fr.traqueur.commands.api.arguments.TabCompleter; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; /** * Argument used to parse an {@link OfflinePlayer} from a string. */ -public class OfflinePlayerArgument implements ArgumentConverter, fr.traqueur.commands.api.arguments.TabCompleter { +public class OfflinePlayerArgument implements ArgumentConverter, TabCompleter { + + /** + * Cache TTL in milliseconds (longer for offline players as it's more expensive). + */ + private static final long CACHE_TTL_MS = 5000; + + /** + * Cached offline player names. + */ + private volatile List cachedNames = Collections.emptyList(); + + /** + * Last cache update time. + */ + private volatile long cacheTime = 0; /** * Creates a new OfflinePlayerArgument. */ public OfflinePlayerArgument() {} - @SuppressWarnings(value = "deprecation") /** * {@inheritDoc} - *

- * This implementation uses {@link Bukkit#getOfflinePlayer(String)} to parse the {@link OfflinePlayer}. */ + @SuppressWarnings("deprecation") @Override public OfflinePlayer apply(String input) { return input != null ? Bukkit.getOfflinePlayer(input) : null; @@ -32,10 +48,18 @@ public OfflinePlayer apply(String input) { /** * {@inheritDoc} - * This implementation returns a list of all player names. + * Returns cached list of offline player names, refreshed every 5 seconds. */ @Override public List onCompletion(CommandSender sender, List args) { - return Arrays.stream(Bukkit.getServer().getOfflinePlayers()).map(OfflinePlayer::getName).collect(Collectors.toList()); + long now = System.currentTimeMillis(); + if (now - cacheTime > CACHE_TTL_MS) { + cachedNames = Arrays.stream(Bukkit.getServer().getOfflinePlayers()) + .map(OfflinePlayer::getName) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + cacheTime = now; + } + return cachedNames; } -} +} \ No newline at end of file diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/PlayerArgument.java b/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/PlayerArgument.java index 5eb8826..3d4b5b4 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/PlayerArgument.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/PlayerArgument.java @@ -1,17 +1,34 @@ package fr.traqueur.commands.spigot.arguments; import fr.traqueur.commands.api.arguments.ArgumentConverter; +import fr.traqueur.commands.api.arguments.TabCompleter; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; /** * Argument converter for {@link Player}. */ -public class PlayerArgument implements ArgumentConverter, fr.traqueur.commands.api.arguments.TabCompleter { +public class PlayerArgument implements ArgumentConverter, TabCompleter { + + /** + * Cache TTL in milliseconds. + */ + private static final long CACHE_TTL_MS = 1000; + + /** + * Cached player names. + */ + private volatile List cachedNames = Collections.emptyList(); + + /** + * Last cache update time. + */ + private volatile long cacheTime = 0; /** * Creates a new PlayerArgument. @@ -20,8 +37,6 @@ public PlayerArgument() {} /** * {@inheritDoc} - *

- * This implementation uses {@link Bukkit#getPlayer(String)} to convert the input to a player. */ @Override public Player apply(String input) { @@ -30,10 +45,17 @@ public Player apply(String input) { /** * {@inheritDoc} - * This implementation returns a list of all online player names. + * Returns cached list of online player names, refreshed every second. */ @Override public List onCompletion(CommandSender sender, List args) { - return Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(Collectors.toList()); + long now = System.currentTimeMillis(); + if (now - cacheTime > CACHE_TTL_MS) { + cachedNames = Bukkit.getOnlinePlayers().stream() + .map(Player::getName) + .collect(Collectors.toList()); + cacheTime = now; + } + return cachedNames; } } \ No newline at end of file From e57c8ee8b056fe902f7f9dddba1fee15d13bcfab Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 17:05:46 +0100 Subject: [PATCH 06/23] feat: add fluent command builder possibility from manager --- .../traqueur/commands/api/CommandManager.java | 12 ++ .../commands/api/models/CommandBuilder.java | 192 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 core/src/main/java/fr/traqueur/commands/api/models/CommandBuilder.java 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 a6ec404..3c3ad83 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -9,6 +9,7 @@ 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; @@ -302,6 +303,17 @@ 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. diff --git a/core/src/main/java/fr/traqueur/commands/api/models/CommandBuilder.java b/core/src/main/java/fr/traqueur/commands/api/models/CommandBuilder.java new file mode 100644 index 0000000..a5b735b --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/models/CommandBuilder.java @@ -0,0 +1,192 @@ +package fr.traqueur.commands.api.models; + +import fr.traqueur.commands.api.CommandManager; +import fr.traqueur.commands.api.arguments.Arguments; +import fr.traqueur.commands.api.arguments.TabCompleter; +import fr.traqueur.commands.api.requirements.Requirement; + +import java.util.function.BiConsumer; + +/** + * Fluent builder for creating commands without subclassing. + * + * @param plugin type + * @param sender type + */ +public class CommandBuilder { + + private final CommandManager manager; + private final SimpleCommand command; + + private String description = ""; + private String usage = ""; + private String permission = ""; + private boolean gameOnly = false; + private BiConsumer executor; + + /** + * Create a builder from a manager (preferred). + * + * @param manager the command manager + * @param name the command name + */ + public CommandBuilder(CommandManager manager, String name) { + this.manager = manager; + this.command = new SimpleCommand(manager.getPlatform().getPlugin(), name); + } + + /** + * Create a standalone builder. + * + * @param plugin the plugin instance + * @param name the command name + */ + public CommandBuilder(T plugin, String name) { + this.manager = null; + this.command = new SimpleCommand(plugin, name); + } + + public CommandBuilder description(String description) { + this.description = description; + return this; + } + + public CommandBuilder usage(String usage) { + this.usage = usage; + return this; + } + + public CommandBuilder permission(String permission) { + this.permission = permission; + return this; + } + + public CommandBuilder gameOnly() { + this.gameOnly = true; + return this; + } + + public CommandBuilder gameOnly(boolean gameOnly) { + this.gameOnly = gameOnly; + return this; + } + + public CommandBuilder alias(String alias) { + this.command.addAlias(alias); + return this; + } + + public CommandBuilder aliases(String... aliases) { + this.command.addAlias(aliases); + return this; + } + + public CommandBuilder arg(String name, Class type) { + this.command.addArg(name, type); + return this; + } + + public CommandBuilder arg(String name, Class type, TabCompleter completer) { + this.command.addArg(name, type, completer); + return this; + } + + public CommandBuilder optionalArg(String name, Class type) { + this.command.addOptionalArg(name, type); + return this; + } + + public CommandBuilder optionalArg(String name, Class type, TabCompleter completer) { + this.command.addOptionalArg(name, type, completer); + return this; + } + + public CommandBuilder requirement(Requirement requirement) { + this.command.addRequirements(requirement); + return this; + } + + @SafeVarargs + public final CommandBuilder requirements(Requirement... requirements) { + this.command.addRequirements(requirements); + return this; + } + + public CommandBuilder subcommand(Command subcommand) { + this.command.addSubCommand(subcommand); + return this; + } + + @SafeVarargs + public final CommandBuilder subcommands(Command... subcommands) { + this.command.addSubCommand(subcommands); + return this; + } + + public CommandBuilder executor(BiConsumer executor) { + this.executor = executor; + return this; + } + + /** + * Build the command. + * + * @return the built command + * @throws IllegalStateException if no executor is set + */ + public Command build() { + if (this.executor == null) { + throw new IllegalStateException("Command executor must be set"); + } + + this.command.setDescription(this.description); + this.command.setUsage(this.usage); + this.command.setPermission(this.permission); + this.command.setGameOnly(this.gameOnly); + this.command.setExecutor(this.executor); + + return this.command; + } + + /** + * Build and register the command. + * + * @return the built and registered command + * @throws IllegalStateException if builder was not created from a CommandManager + */ + public Command register() { + if (this.manager == null) { + throw new IllegalStateException( + "Cannot register: builder was not created from a CommandManager. " + + "Use manager.command(name) instead." + ); + } + + Command cmd = build(); + this.manager.registerCommand(cmd); + return cmd; + } + + /** + * Simple command implementation used internally by the builder. + */ + private class SimpleCommand extends Command { + + private BiConsumer executor; + + SimpleCommand(T plugin, String name) { + super(plugin, name); + } + + void setExecutor(BiConsumer executor) { + this.executor = executor; + } + + @Override + public void execute(S sender, Arguments arguments) { + if (executor != null) { + executor.accept(sender, arguments); + } + } + } +} \ No newline at end of file From 2463b1c3b9d6439e42dea5577aca00585ac9cc97 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 17:07:35 +0100 Subject: [PATCH 07/23] feat: force usage with manager for command builder --- .../commands/api/models/CommandBuilder.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/core/src/main/java/fr/traqueur/commands/api/models/CommandBuilder.java b/core/src/main/java/fr/traqueur/commands/api/models/CommandBuilder.java index a5b735b..6eba4fb 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/CommandBuilder.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/CommandBuilder.java @@ -35,17 +35,6 @@ public CommandBuilder(CommandManager manager, String name) { this.command = new SimpleCommand(manager.getPlatform().getPlugin(), name); } - /** - * Create a standalone builder. - * - * @param plugin the plugin instance - * @param name the command name - */ - public CommandBuilder(T plugin, String name) { - this.manager = null; - this.command = new SimpleCommand(plugin, name); - } - public CommandBuilder description(String description) { this.description = description; return this; @@ -152,16 +141,8 @@ public Command build() { * Build and register the command. * * @return the built and registered command - * @throws IllegalStateException if builder was not created from a CommandManager */ public Command register() { - if (this.manager == null) { - throw new IllegalStateException( - "Cannot register: builder was not created from a CommandManager. " + - "Use manager.command(name) instead." - ); - } - Command cmd = build(); this.manager.registerCommand(cmd); return cmd; From 38d104055a5d71a33b87d2a78d16092a28d85fcd Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 17:18:15 +0100 Subject: [PATCH 08/23] feat: add tests --- .../commands/api/arguments/ArgumentsTest.java | 109 +++++ .../api/models/CommandBuilderTest.java | 409 ++++++++++++++++++ .../api/models/CommandInvokerTest.java | 78 +++- .../commands/api/models/CommandTest.java | 65 ++- .../logging/InternalMessageHandlerTest.java | 24 +- .../parsing/DefaultArgumentParserTest.java | 230 ++++++++++ 6 files changed, 911 insertions(+), 4 deletions(-) create mode 100644 core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java create mode 100644 core/src/test/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParserTest.java diff --git a/core/src/test/java/fr/traqueur/commands/api/arguments/ArgumentsTest.java b/core/src/test/java/fr/traqueur/commands/api/arguments/ArgumentsTest.java index fb716eb..e5f93c2 100644 --- a/core/src/test/java/fr/traqueur/commands/api/arguments/ArgumentsTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/arguments/ArgumentsTest.java @@ -5,7 +5,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.*; @@ -38,4 +41,110 @@ void getOptional_onEmptyMapReturnsEmptyWithoutError() { assertTrue(opt.isEmpty()); } + // --- New utility methods tests --- + + @Test + void toMap_returnsMapWithValues() { + args.add("name", String.class, "Alice"); + args.add("age", Integer.class, 25); + + Map map = args.toMap(); + + assertEquals(2, map.size()); + assertEquals("Alice", map.get("name")); + assertEquals(25, map.get("age")); + } + + @Test + void toMap_emptyArguments_returnsEmptyMap() { + Map map = args.toMap(); + assertTrue(map.isEmpty()); + } + + @Test + void size_returnsCorrectCount() { + assertEquals(0, args.size()); + + args.add("a", String.class, "value1"); + assertEquals(1, args.size()); + + args.add("b", Integer.class, 42); + assertEquals(2, args.size()); + } + + @Test + void isEmpty_returnsTrueWhenEmpty() { + assertTrue(args.isEmpty()); + } + + @Test + void isEmpty_returnsFalseWhenNotEmpty() { + args.add("key", String.class, "value"); + assertFalse(args.isEmpty()); + } + + @Test + void getKeys_returnsAllKeys() { + args.add("first", String.class, "a"); + args.add("second", Integer.class, 1); + args.add("third", Double.class, 1.5); + + Set keys = args.getKeys(); + + assertEquals(3, keys.size()); + assertTrue(keys.contains("first")); + assertTrue(keys.contains("second")); + assertTrue(keys.contains("third")); + } + + @Test + void getKeys_returnsUnmodifiableSet() { + args.add("key", String.class, "value"); + + Set keys = args.getKeys(); + + assertThrows(UnsupportedOperationException.class, () -> keys.add("new")); + } + + @Test + void forEach_iteratesAllEntries() { + args.add("a", String.class, "valueA"); + args.add("b", String.class, "valueB"); + + AtomicInteger count = new AtomicInteger(0); + args.forEach((key, value) -> { + count.incrementAndGet(); + assertTrue(key.equals("a") || key.equals("b")); + }); + + assertEquals(2, count.get()); + } + + @Test + void has_returnsTrueForExistingKey() { + args.add("exists", String.class, "value"); + + assertTrue(args.has("exists")); + } + + @Test + void has_returnsFalseForMissingKey() { + assertFalse(args.has("missing")); + } + + @Test + void get_withDefaultValue_returnsValueWhenPresent() { + args.add("key", String.class, "actual"); + + String result = args.getOptional("key").orElse("default"); + + assertEquals("actual", result); + } + + @Test + void get_withDefaultValue_returnsDefaultWhenMissing() { + String result = args.getOptional("missing").orElse("default"); + + assertEquals("default", result); + } } \ No newline at end of file diff --git a/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java b/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java new file mode 100644 index 0000000..49b612f --- /dev/null +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java @@ -0,0 +1,409 @@ +package fr.traqueur.commands.api.models; + +import fr.traqueur.commands.api.CommandManager; +import fr.traqueur.commands.api.arguments.Arguments; +import fr.traqueur.commands.api.requirements.Requirement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.*; + +class CommandBuilderTest { + + private CommandManager manager; + private FakePlatform platform; + + @BeforeEach + void setUp() { + platform = new FakePlatform(); + manager = new CommandManager<>(platform) { + }; + } + + // --- Basic building --- + + @Test + void build_simpleCommand_success() { + Command cmd = manager.command("test") + .description("Test description") + .usage("/test") + .permission("test.use") + .executor((sender, args) -> { + }) + .build(); + + assertEquals("test", cmd.getName()); + assertEquals("Test description", cmd.getDescription()); + assertEquals("/test", cmd.getUsage()); + assertEquals("test.use", cmd.getPermission()); + } + + @Test + void build_withoutExecutor_throwsException() { + CommandBuilder builder = manager.command("test") + .description("Test"); + + assertThrows(IllegalStateException.class, builder::build); + } + + @Test + void build_withGameOnly_setsFlag() { + Command cmd = manager.command("test") + .gameOnly() + .executor((sender, args) -> { + }) + .build(); + + assertTrue(cmd.inGameOnly()); + } + + @Test + void build_withGameOnlyFalse_clearsFlag() { + Command cmd = manager.command("test") + .gameOnly(false) + .executor((sender, args) -> { + }) + .build(); + + assertFalse(cmd.inGameOnly()); + } + + // --- Arguments --- + + @Test + void build_withArgs_addsArguments() { + Command cmd = manager.command("test") + .arg("name", String.class) + .arg("count", Integer.class) + .executor((sender, args) -> { + }) + .build(); + + assertEquals(2, cmd.getArgs().size()); + assertEquals("name", cmd.getArgs().get(0).name()); + assertEquals("count", cmd.getArgs().get(1).name()); + } + + @Test + void build_withOptionalArgs_addsOptionalArguments() { + Command cmd = manager.command("test") + .arg("required", String.class) + .optionalArg("optional1", Integer.class) + .optionalArg("optional2", Double.class) + .executor((sender, args) -> { + }) + .build(); + + assertEquals(1, cmd.getArgs().size()); + assertEquals(2, cmd.getOptionalArgs().size()); + assertEquals("optional1", cmd.getOptionalArgs().get(0).name()); + assertEquals("optional2", cmd.getOptionalArgs().get(1).name()); + } + + @Test + void build_withTabCompleter_addsCustomCompleter() { + Command cmd = manager.command("test") + .arg("player", String.class, (sender, args) -> List.of("Alice", "Bob")) + .executor((sender, args) -> { + }) + .build(); + + assertNotNull(cmd.getArgs().getFirst().tabCompleter()); + } + + // --- Aliases --- + + @Test + void build_withAlias_addsAlias() { + Command cmd = manager.command("test") + .alias("t") + .executor((sender, args) -> { + }) + .build(); + + assertEquals(1, cmd.getAliases().size()); + assertTrue(cmd.getAliases().contains("t")); + } + + @Test + void build_withAliases_addsMultipleAliases() { + Command cmd = manager.command("test") + .aliases("t", "tst", "te") + .executor((sender, args) -> { + }) + .build(); + + assertEquals(3, cmd.getAliases().size()); + assertTrue(cmd.getAliases().contains("t")); + assertTrue(cmd.getAliases().contains("tst")); + assertTrue(cmd.getAliases().contains("te")); + } + + // --- Subcommands --- + + @Test + void build_withSubcommand_addsSubcommand() { + Command sub = manager.command("sub") + .executor((sender, args) -> { + }) + .build(); + + Command cmd = manager.command("main") + .subcommand(sub) + .executor((sender, args) -> { + }) + .build(); + + assertEquals(1, cmd.getSubcommands().size()); + assertSame(sub, cmd.getSubcommands().get(0)); + } + + @Test + void build_withSubcommands_addsMultipleSubcommands() { + Command sub1 = manager.command("sub1") + .executor((sender, args) -> { + }) + .build(); + + Command sub2 = manager.command("sub2") + .executor((sender, args) -> { + }) + .build(); + + Command cmd = manager.command("main") + .subcommands(sub1, sub2) + .executor((sender, args) -> { + }) + .build(); + + assertEquals(2, cmd.getSubcommands().size()); + } + + // --- Requirements --- + + @Test + void build_withRequirement_addsRequirement() { + Requirement req = new Requirement<>() { + @Override + public boolean check(Object sender) { + return true; + } + + @Override + public String errorMessage() { + return "Error"; + } + }; + + Command cmd = manager.command("test") + .requirement(req) + .executor((sender, args) -> { + }) + .build(); + + assertEquals(1, cmd.getRequirements().size()); + assertSame(req, cmd.getRequirements().get(0)); + } + + @Test + void build_withRequirements_addsMultipleRequirements() { + Command cmd = manager.command("test") + .requirements( + new Requirement<>() { + @Override + public boolean check(Object sender) { + return true; + } + + @Override + public String errorMessage() { + return ""; + } + }, + new Requirement<>() { + @Override + public boolean check(Object sender) { + return false; + } + + @Override + public String errorMessage() { + return ""; + } + } + ) + .executor((sender, args) -> { + }) + .build(); + + assertEquals(2, cmd.getRequirements().size()); + } + + // --- Executor --- + + @Test + void build_executorIsCalled() { + AtomicReference received = new AtomicReference<>(); + + Command cmd = manager.command("test") + .executor((sender, args) -> { + received.set("executed"); + }) + .build(); + + cmd.execute(null, new Arguments(new fr.traqueur.commands.impl.logging.InternalLogger(Logger.getLogger("test")))); + + assertEquals("executed", received.get()); + } + + // --- Register --- + + @Test + void register_registersCommandInManager() { + Command cmd = manager.command("registered") + .executor((sender, args) -> { + }) + .register(); + + assertTrue(platform.registeredLabels.contains("registered")); + assertTrue(manager.getCommands().findNode("registered", new String[]{}).isPresent()); + } + + @Test + void register_returnsBuiltCommand() { + Command cmd = manager.command("test") + .description("Test") + .executor((sender, args) -> { + }) + .register(); + + assertNotNull(cmd); + assertEquals("test", cmd.getName()); + assertEquals("Test", cmd.getDescription()); + } + + // --- Fluent chain --- + + @Test + void build_fluentChain_allOptions() { + Command sub = manager.command("sub") + .executor((sender, args) -> { + }) + .build(); + + Command cmd = manager.command("complex") + .description("Complex command") + .usage("/complex [opt]") + .permission("complex.use") + .gameOnly() + .aliases("c", "cx") + .arg("required", String.class) + .optionalArg("optional", Integer.class) + .subcommand(sub) + .requirement(new Requirement<>() { + @Override + public boolean check(Object sender) { + return true; + } + + @Override + public String errorMessage() { + return ""; + } + }) + .executor((sender, args) -> { + }) + .build(); + + assertEquals("complex", cmd.getName()); + assertEquals("Complex command", cmd.getDescription()); + assertEquals("/complex [opt]", cmd.getUsage()); + assertEquals("complex.use", cmd.getPermission()); + assertTrue(cmd.inGameOnly()); + assertEquals(2, cmd.getAliases().size()); + assertEquals(1, cmd.getArgs().size()); + assertEquals(1, cmd.getOptionalArgs().size()); + assertEquals(1, cmd.getSubcommands().size()); + assertEquals(1, cmd.getRequirements().size()); + } + + // --- Default values --- + + @Test + void build_defaultValues_areEmpty() { + Command cmd = manager.command("minimal") + .executor((sender, args) -> { + }) + .build(); + + assertEquals("minimal", cmd.getName()); + assertEquals("", cmd.getDescription()); + assertEquals("", cmd.getUsage()); + assertEquals("", cmd.getPermission()); + assertFalse(cmd.inGameOnly()); + assertTrue(cmd.getAliases().isEmpty()); + assertTrue(cmd.getArgs().isEmpty()); + assertTrue(cmd.getOptionalArgs().isEmpty()); + assertTrue(cmd.getSubcommands().isEmpty()); + assertTrue(cmd.getRequirements().isEmpty()); + } + + @Test + void build_commandIsEnabled_byDefault() { + Command cmd = manager.command("test") + .executor((sender, args) -> { + }) + .build(); + + assertTrue(cmd.isEnabled()); + } + + // --- Helper classes --- + + static class FakePlatform implements CommandPlatform { + java.util.List registeredLabels = new java.util.ArrayList<>(); + + @Override + public Object getPlugin() { + return new Object(); + } + + @Override + public void injectManager(CommandManager cm) { + } + + @Override + public Logger getLogger() { + return Logger.getAnonymousLogger(); + } + + @Override + public boolean hasPermission(Object sender, String permission) { + return true; + } + + @Override + public boolean isPlayer(Object sender) { + return false; + } + + @Override + public void sendMessage(Object sender, String message) { + } + + @Override + public void addCommand(Command command, String label) { + registeredLabels.add(label); + } + + @Override + public void removeCommand(String label, boolean sub) { + registeredLabels.remove(label); + } + } +} \ No newline at end of file diff --git a/core/src/test/java/fr/traqueur/commands/api/models/CommandInvokerTest.java b/core/src/test/java/fr/traqueur/commands/api/models/CommandInvokerTest.java index 408aa47..a4932c7 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/CommandInvokerTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandInvokerTest.java @@ -131,6 +131,82 @@ void aliasWithSubCommand_executesSubCommand() { assertTrue(suggests.contains("sub")); } + // --- v5.0.0 new tests --- + + @Test + void invoke_disabledCommand_sendsDisabledMessage() { + cmd.setEnabled(false); + when(messageHandler.getCommandDisabledMessage()).thenReturn("COMMAND_DISABLED"); + + boolean result = manager.getInvoker().invoke("user", "base", new String[]{}); + + assertTrue(result); // Command was found, message sent + verify(platform).sendMessage("user", "COMMAND_DISABLED"); + } + + @Test + void invoke_disabledCommand_doesNotExecute() { + AtomicBoolean executed = new AtomicBoolean(false); + + DummyCommand trackedCmd = new DummyCommand() { + @Override + public void execute(String sender, Arguments arguments) { + executed.set(true); + } + }; + trackedCmd.setEnabled(false); + manager.getCommands().addCommand("tracked", trackedCmd); + + when(messageHandler.getCommandDisabledMessage()).thenReturn("DISABLED"); + + manager.getInvoker().invoke("user", "tracked", new String[]{}); + + assertFalse(executed.get()); + } + + @Test + void invoke_reEnabledCommand_executesNormally() { + AtomicBoolean executed = new AtomicBoolean(false); + + DummyCommand trackedCmd = new DummyCommand() { + @Override + public void execute(String sender, Arguments arguments) { + executed.set(true); + } + }; + + // Disable then re-enable + trackedCmd.setEnabled(false); + trackedCmd.setEnabled(true); + + manager.getCommands().addCommand("tracked", trackedCmd); + + boolean result = manager.getInvoker().invoke("user", "tracked", new String[]{}); + + assertTrue(result); + assertTrue(executed.get()); + } + + @Test + void invoke_enabledByDefault_executesNormally() { + AtomicBoolean executed = new AtomicBoolean(false); + + DummyCommand trackedCmd = new DummyCommand() { + @Override + public void execute(String sender, Arguments arguments) { + executed.set(true); + } + }; + + // Don't call setEnabled - should be enabled by default + manager.getCommands().addCommand("tracked", trackedCmd); + + boolean result = manager.getInvoker().invoke("user", "tracked", new String[]{}); + + assertTrue(result); + assertTrue(executed.get()); + } + static class DummyCommand extends Command { DummyCommand() { super(null, "base"); @@ -140,4 +216,4 @@ static class DummyCommand extends Command { public void execute(String sender, Arguments args) { } } -} +} \ No newline at end of file diff --git a/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java b/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java index c790446..fbb507a 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java @@ -232,4 +232,67 @@ void usage_subsAndArgsCombined() { assertTrue(usage.contains("")); assertTrue(usage.contains("[opt:string]")); } -} + + // --- v5.0.0 new tests --- + + @Test + void setEnabled_defaultIsTrue() { + assertTrue(cmd.isEnabled()); + } + + @Test + void setEnabled_canDisable() { + cmd.setEnabled(false); + assertFalse(cmd.isEnabled()); + } + + @Test + void setEnabled_canReEnable() { + cmd.setEnabled(false); + assertFalse(cmd.isEnabled()); + + cmd.setEnabled(true); + assertTrue(cmd.isEnabled()); + } + + @Test + void getAllLabels_returnsNameOnly_whenNoAliases() { + List labels = cmd.getAllLabels(); + + assertEquals(1, labels.size()); + assertEquals("dummy", labels.get(0)); + } + + @Test + void getAllLabels_returnsNameAndAliases() { + cmd.addAlias("d1", "d2", "d3"); + + List labels = cmd.getAllLabels(); + + assertEquals(4, labels.size()); + assertEquals("dummy", labels.getFirst()); // Name first + assertTrue(labels.contains("d1")); + assertTrue(labels.contains("d2")); + assertTrue(labels.contains("d3")); + } + + @Test + void getAliases_doesNotIncludeName() { + cmd.addAlias("alias1"); + + List aliases = cmd.getAliases(); + + assertEquals(1, aliases.size()); + assertFalse(aliases.contains("dummy")); + assertTrue(aliases.contains("alias1")); + } + + @Test + void getAllLabels_nameAlwaysFirst() { + cmd.addAlias("aaa"); // alphabetically before "dummy" + + List labels = cmd.getAllLabels(); + + assertEquals("dummy", labels.get(0)); + } +} \ No newline at end of file diff --git a/core/src/test/java/fr/traqueur/commands/impl/logging/InternalMessageHandlerTest.java b/core/src/test/java/fr/traqueur/commands/impl/logging/InternalMessageHandlerTest.java index 7eaf6de..5c1313f 100644 --- a/core/src/test/java/fr/traqueur/commands/impl/logging/InternalMessageHandlerTest.java +++ b/core/src/test/java/fr/traqueur/commands/impl/logging/InternalMessageHandlerTest.java @@ -3,7 +3,7 @@ import fr.traqueur.commands.api.logging.MessageHandler; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; class InternalMessageHandlerTest { @@ -40,4 +40,24 @@ void testGetRequirementMessage() { handler.getRequirementMessage() ); } -} + + // --- v5.0.0 new tests --- + + @Test + void testGetCommandDisabledMessage() { + assertEquals( + "&cThis command is currently disabled.", + handler.getCommandDisabledMessage() + ); + } + + @Test + void testGetCommandDisabledMessage_notNull() { + assertNotNull(handler.getCommandDisabledMessage()); + } + + @Test + void testGetCommandDisabledMessage_notEmpty() { + assertFalse(handler.getCommandDisabledMessage().isEmpty()); + } +} \ No newline at end of file diff --git a/core/src/test/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParserTest.java b/core/src/test/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParserTest.java new file mode 100644 index 0000000..fb2fcc4 --- /dev/null +++ b/core/src/test/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParserTest.java @@ -0,0 +1,230 @@ +package fr.traqueur.commands.impl.parsing; + +import fr.traqueur.commands.api.arguments.ArgumentConverter; +import fr.traqueur.commands.api.arguments.Arguments; +import fr.traqueur.commands.api.arguments.Infinite; +import fr.traqueur.commands.api.logging.Logger; +import fr.traqueur.commands.api.models.Command; +import fr.traqueur.commands.api.parsing.ParseError; +import fr.traqueur.commands.api.parsing.ParseResult; +import fr.traqueur.commands.impl.logging.InternalLogger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class DefaultArgumentParserTest { + + private DefaultArgumentParser parser; + + @BeforeEach + void setUp() { + Map> converters = new HashMap<>(); + converters.put("string", new ArgumentConverter.Wrapper<>(String.class, s -> s)); + converters.put("integer", new ArgumentConverter.Wrapper<>(Integer.class, s -> { + try { + return Integer.valueOf(s); + } catch (NumberFormatException e) { + return null; + } + })); + converters.put("double", new ArgumentConverter.Wrapper<>(Double.class, s -> { + try { + return Double.valueOf(s); + } catch (NumberFormatException e) { + return null; + } + })); + + Logger logger = new InternalLogger(java.util.logging.Logger.getLogger("ParserTest")); + parser = new DefaultArgumentParser<>(converters, logger); + } + + // --- Required args --- + + @Test + void parse_requiredArgs_success() { + Command cmd = new DummyCommand(); + cmd.addArg("name", String.class); + cmd.addArg("count", Integer.class); + + ParseResult result = parser.parse(cmd, new String[]{"hello", "42"}); + + assertTrue(result.isSuccess()); + assertFalse(result.isError()); + assertEquals(2, result.consumedCount()); + + Arguments args = result.arguments(); + assertEquals("hello", args.get("name")); + assertEquals(42, (int) args.get("count")); + } + + @Test + void parse_missingRequiredArg_returnsError() { + Command cmd = new DummyCommand(); + cmd.addArg("name", String.class); + cmd.addArg("count", Integer.class); + + ParseResult result = parser.parse(cmd, new String[]{"hello"}); + + assertTrue(result.isError()); + assertFalse(result.isSuccess()); + assertEquals(ParseError.Type.MISSING_REQUIRED, result.error().type()); + assertEquals("count", result.error().argumentName()); + } + + // --- Optional args --- + + @Test + void parse_optionalArgs_allProvided() { + Command cmd = new DummyCommand(); + cmd.addArg("req", String.class); + cmd.addOptionalArg("opt1", Integer.class); + cmd.addOptionalArg("opt2", Double.class); + + ParseResult result = parser.parse(cmd, new String[]{"value", "10", "3.14"}); + + assertTrue(result.isSuccess()); + assertEquals(3, result.consumedCount()); + + Arguments args = result.arguments(); + assertEquals("value", args.get("req")); + assertEquals(10, (int) args.get("opt1")); + assertEquals(3.14, args.get("opt2"), 0.001); + } + + @Test + void parse_optionalArgs_partiallyProvided() { + Command cmd = new DummyCommand(); + cmd.addArg("req", String.class); + cmd.addOptionalArg("opt1", Integer.class); + cmd.addOptionalArg("opt2", Double.class); + + ParseResult result = parser.parse(cmd, new String[]{"value", "10"}); + + assertTrue(result.isSuccess()); + assertEquals(2, result.consumedCount()); + + Arguments args = result.arguments(); + assertEquals("value", args.get("req")); + assertEquals(10, (int) args.get("opt1")); + assertFalse(args.has("opt2")); + } + + @Test + void parse_optionalArgs_noneProvided() { + Command cmd = new DummyCommand(); + cmd.addArg("req", String.class); + cmd.addOptionalArg("opt", Integer.class); + + ParseResult result = parser.parse(cmd, new String[]{"value"}); + + assertTrue(result.isSuccess()); + assertEquals(1, result.consumedCount()); + + Arguments args = result.arguments(); + assertEquals("value", args.get("req")); + assertFalse(args.has("opt")); + } + + // --- Infinite args --- + + @Test + void parse_infiniteArg_joinsRemaining() { + Command cmd = new DummyCommand(); + cmd.addArg("prefix", String.class); + cmd.addArg("message", Infinite.class); + + ParseResult result = parser.parse(cmd, new String[]{"hello", "this", "is", "a", "message"}); + + assertTrue(result.isSuccess()); + + Arguments args = result.arguments(); + assertEquals("hello", args.get("prefix")); + assertEquals("this is a message", args.get("message")); + } + + @Test + void parse_infiniteArg_empty() { + Command cmd = new DummyCommand(); + cmd.addArg("message", Infinite.class); + + ParseResult result = parser.parse(cmd, new String[]{}); + + assertTrue(result.isSuccess()); + + Arguments args = result.arguments(); + assertEquals("", args.get("message")); + } + + @Test + void parse_optionalInfiniteArg() { + Command cmd = new DummyCommand(); + cmd.addArg("prefix", String.class); + cmd.addOptionalArg("rest", Infinite.class); + + ParseResult result = parser.parse(cmd, new String[]{"hello", "world", "!"}); + + assertTrue(result.isSuccess()); + + Arguments args = result.arguments(); + assertEquals("hello", args.get("prefix")); + assertEquals("world !", args.get("rest")); + } + + // --- Error cases --- + + @Test + void parse_typeNotFound_returnsError() { + // Register command with unknown type + Command cmd = new DummyCommand(); + cmd.addArg("unknown", UnknownType.class); + + ParseResult result = parser.parse(cmd, new String[]{"value"}); + + assertTrue(result.isError()); + assertEquals(ParseError.Type.TYPE_NOT_FOUND, result.error().type()); + } + + @Test + void parse_conversionFailed_returnsError() { + Command cmd = new DummyCommand(); + cmd.addArg("number", Integer.class); + + ParseResult result = parser.parse(cmd, new String[]{"notAnInteger"}); + + assertTrue(result.isError()); + assertEquals(ParseError.Type.CONVERSION_FAILED, result.error().type()); + assertEquals("number", result.error().argumentName()); + assertEquals("notAnInteger", result.error().input()); + } + + @Test + void parse_noArgs_success() { + Command cmd = new DummyCommand(); + + ParseResult result = parser.parse(cmd, new String[]{}); + + assertTrue(result.isSuccess()); + assertEquals(0, result.consumedCount()); + assertTrue(result.arguments().isEmpty()); + } + + // --- Helper classes --- + + private static class DummyCommand extends Command { + DummyCommand() { + super(null, "dummy"); + } + + @Override + public void execute(Object sender, Arguments arguments) { + } + } + + private static class UnknownType { + } +} \ No newline at end of file From 779e06b38151c06f604e67e6435a9e9d7d3d8ae3 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 17:19:35 +0100 Subject: [PATCH 09/23] feat: cleanup code --- README.md | 9 +- .../commands/CommandLookupBenchmark.java | 19 ++- .../traqueur/commands/api/CommandManager.java | 130 +++++++++++------- .../api/arguments/ArgumentConverter.java | 2 + .../commands/api/arguments/ArgumentType.java | 14 +- .../commands/api/arguments/Arguments.java | 11 +- .../commands/api/arguments/Infinite.java | 3 +- .../commands/api/arguments/TabCompleter.java | 6 +- .../ArgsWithInfiniteArgumentException.java | 1 + .../ArgumentIncorrectException.java | 1 + .../CommandRegistrationException.java | 2 +- .../UpdaterInitializationException.java | 2 +- .../traqueur/commands/api/logging/Logger.java | 2 + .../commands/api/logging/MessageHandler.java | 7 +- .../traqueur/commands/api/models/Command.java | 129 ++++++++++------- .../commands/api/models/CommandInvoker.java | 5 +- .../commands/api/models/CommandPlatform.java | 11 +- .../api/models/collections/CommandTree.java | 82 +++++------ .../commands/api/parsing/ParseError.java | 17 ++- .../commands/api/parsing/ParseResult.java | 16 +-- .../api/requirements/Requirement.java | 5 +- .../commands/api/updater/Updater.java | 19 ++- .../impl/arguments/BooleanArgument.java | 5 +- .../impl/arguments/DoubleArgument.java | 6 +- .../commands/impl/arguments/EnumArgument.java | 24 ++-- .../impl/arguments/IntegerArgument.java | 3 +- .../commands/impl/arguments/LongArgument.java | 3 +- .../impl/logging/InternalMessageHandler.java | 3 +- .../impl/parsing/DefaultArgumentParser.java | 78 ++++++----- .../commands/api/CommandManagerTest.java | 73 +++++++--- .../api/models/CommandBuilderTest.java | 1 - .../commands/api/models/CommandTest.java | 82 +++++++---- .../models/collections/CommandTreeTest.java | 55 ++++---- .../commands/api/updater/UpdaterTest.java | 21 ++- .../impl/arguments/EnumArgumentTest.java | 3 +- .../fr/traqueur/commands/test/TestBot.java | 34 ++--- .../commands/test/commands/AdminCommand.java | 12 +- .../commands/test/commands/PingCommand.java | 3 +- .../commands/jda/JDAArgumentParser.java | 3 +- .../fr/traqueur/commands/jda/JDAPlatform.java | 18 ++- .../jda/requirements/RoleRequirement.java | 1 - .../traqueur/testplugin/Sub2TestCommand.java | 2 +- .../traqueur/testplugin/SubTestCommand.java | 8 +- spigot/build.gradle | 2 +- .../fr/traqueur/commands/spigot/Command.java | 2 +- .../commands/spigot/CommandManager.java | 1 + .../commands/spigot/SpigotExecutor.java | 25 ++-- .../commands/spigot/SpigotPlatform.java | 7 +- .../arguments/OfflinePlayerArgument.java | 3 +- .../spigot/arguments/PlayerArgument.java | 3 +- .../spigot/requirements/WorldRequirement.java | 26 ++-- .../spigot/requirements/ZoneRequirement.java | 78 ++++++----- .../spigot/SpigotIntegrationTest.java | 46 +++++-- velocity-test-plugin/build.gradle | 6 +- .../velocityTestPlugin/SubTestCommand.java | 8 +- .../VelocityTestPlugin.java | 14 +- .../traqueur/commands/velocity/Command.java | 1 + .../commands/velocity/CommandManager.java | 5 +- .../commands/velocity/VelocityPlatform.java | 5 +- 59 files changed, 692 insertions(+), 471 deletions(-) diff --git a/README.md b/README.md index b8a5b41..3c47c3a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # CommandsAPI -**CommandsAPI** is a modular, extensible Java library for building robust, typed command systems across multiple platforms such as **Spigot** and **Velocity**. -As of version `4.0.0`, all core logic has been extracted into a dedicated `core` module, enabling seamless multi-platform support. +**CommandsAPI** is a modular, extensible Java library for building robust, typed command systems across multiple +platforms such as **Spigot** and **Velocity**. +As of version `4.0.0`, all core logic has been extracted into a dedicated `core` module, enabling seamless +multi-platform support. --- @@ -81,7 +83,8 @@ dependencies { ## 💡 Example (Spigot) Be sure to extends all the classes from the platform you are using (Spigot, Velocity, etc.): -`fr.traqueur.commandsapi.spigot.CommandManager` for Spigot, `fr.traqueur.commandsapi.velocity.CommandManager` for Velocity, etc. +`fr.traqueur.commandsapi.spigot.CommandManager` for Spigot, `fr.traqueur.commandsapi.velocity.CommandManager` for +Velocity, etc. ```java public class HelloWorldCommand extends Command { 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 3c3ad83..35cb89a 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -31,6 +31,7 @@ * 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. */ @@ -38,12 +39,12 @@ public abstract class CommandManager { private final ArgumentParser parser; - private final CommandPlatform platform; + 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. @@ -56,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. @@ -76,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); @@ -93,17 +95,18 @@ public CommandManager(CommandPlatform platform) { 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) { @@ -111,34 +114,29 @@ 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) { + public void registerCommand(Command command) { for (String label : command.getAllLabels()) { this.addCommand(command, label); this.registerSubCommands(label, command.getSubcommands()); @@ -147,6 +145,7 @@ public void registerCommand(Command command) { /** * Unregister a command in the command manager. + * * @param label The label of the command to unregister. */ public void unregisterCommand(String label) { @@ -155,12 +154,13 @@ 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) + Optional> commandOptional = this.commands.findNode(rawArgs) .flatMap(result -> result.node().getCommand()); if (commandOptional.isEmpty()) { @@ -171,22 +171,24 @@ 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) { + public void unregisterCommand(Command command, boolean subcommands) { List labels = new ArrayList<>(command.getAllLabels()); for (String label : labels) { this.removeCommand(label, subcommands); - if(subcommands) { + if (subcommands) { this.unregisterSubCommands(label, command.getSubcommands()); } } @@ -194,9 +196,10 @@ public void unregisterCommand(Command command, boolean subcommands) { /** * 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 ArgumentConverter.Wrapper<>(typeClass, converter)); @@ -204,13 +207,14 @@ public void registerConverter(Class typeClass, ArgumentConverter conve /** * 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. + * @throws ArgumentIncorrectException If the argument is incorrect. */ - public Arguments parse(Command command, String[] args) throws TypeArgumentNotExistException, ArgumentIncorrectException { + public Arguments parse(Command command, String[] args) throws TypeArgumentNotExistException, ArgumentIncorrectException { ParseResult result = parser.parse(command, args); if (!result.isSuccess()) { ParseError error = result.error(); @@ -225,15 +229,16 @@ public Arguments parse(Command command, String[] args) throws TypeArgumentN /** * 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() { @@ -242,30 +247,42 @@ public Map>> getCompleters() { /** * 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. */ private void registerSubCommands(String parentLabel, List> subcommands) { - if(subcommands == null || subcommands.isEmpty()) { + if (subcommands == null || subcommands.isEmpty()) { return; } - for (Command subcommand : subcommands) { + for (Command subcommand : subcommands) { List aliasesSub = new ArrayList<>(subcommand.getAllLabels()); for (String aliasSub : aliasesSub) { this.addCommand(subcommand, parentLabel + "." + aliasSub); @@ -276,14 +293,15 @@ private void registerSubCommands(String parentLabel, List> subcomm /** * 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) { + for (Command subcommand : subcommandsList) { List labelsSub = subcommand.getAllLabels(); for (String labelSub : labelsSub) { this.removeCommand(parentLabel + "." + labelSub, true); @@ -294,7 +312,8 @@ private void unregisterSubCommands(String parentLabel, List> subcom /** * 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) { @@ -316,11 +335,12 @@ public CommandBuilder command(String name) { /** * Register a command in the command manager. + * * @param command The command to register. - * @param label The label of the command. + * @param label The label of the command. */ private void addCommand(Command command, String label) { - if(this.isDebug()) { + if (this.isDebug()) { this.logger.info("Register command " + label); } List> args = command.getArgs(); @@ -339,6 +359,7 @@ private void addCommand(Command command, String label) { /** * Register the completions of the command. + * * @param labelParts The parts of the label. */ private void addCompletionsForLabel(String[] labelParts) { @@ -355,9 +376,10 @@ 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. */ private void addCompletionForArgs(String label, int commandSize, List> args) { for (int i = 0; i < args.size(); i++) { @@ -366,7 +388,7 @@ private void addCompletionForArgs(String label, int commandSize, List entry = this.typeConverters.get(type); TabCompleter argConverter = arg.tabCompleter(); if (argConverter != null) { - this.addCompletion(label,commandSize + i, argConverter); + this.addCompletion(label, commandSize + i, argConverter); } else if (entry != null && entry.converter() instanceof TabCompleter completer) { this.addCompletion(label, commandSize + i, (TabCompleter) completer); } else { @@ -377,9 +399,10 @@ private void addCompletionForArgs(String label, int commandSize, List converter) { Map> mapInner = this.completers.getOrDefault(label, new HashMap<>()); @@ -388,9 +411,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 { @@ -403,6 +426,7 @@ private void addCompletion(String label, int commandSize, TabCompleter conver /** * Get the command invoker of the command manager. + * * @return The command invoker of the command manager. */ public CommandInvoker getInvoker() { @@ -413,10 +437,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(Long.class, new LongArgument()); } } 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 a00f579..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,6 +13,7 @@ public interface ArgumentConverter extends Function { /** * Apply the conversion. + * * @param s The string to convert. * @return The object. */ 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 index fb52514..55c958d 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java @@ -2,6 +2,13 @@ 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(); /** @@ -31,11 +38,4 @@ public String key() { } } - static ArgumentType of(Class clazz) { - if(clazz.isAssignableFrom(fr.traqueur.commands.api.arguments.Infinite.class)) { - return Infinite.INSTANCE; - } - return new Simple(clazz); - } - } 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 59fb550..9669be4 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 @@ -24,6 +24,7 @@ public class Arguments { /** * Constructor of the class. + * * @param logger The logger of the class. */ public Arguments(Logger logger) { @@ -57,7 +58,7 @@ public void forEach(BiConsumer action) { * Get an argument from the map. * * @param argument The key of the argument. - * @param The type of the argument. + * @param The type of the argument. * @return The argument. */ public T get(String argument) { @@ -69,7 +70,7 @@ public T get(String argument) { * 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. */ public Optional getOptional(String argument) { @@ -79,7 +80,7 @@ public Optional getOptional(String argument) { ArgumentValue argumentValue = this.arguments.getOrDefault(argument, null); - if(argumentValue == null) { + if (argumentValue == null) { return Optional.empty(); } @@ -102,8 +103,8 @@ 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, T object) { 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 index 362e5fc..c0e3843 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/Infinite.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/Infinite.java @@ -1,3 +1,4 @@ package fr.traqueur.commands.api.arguments; -public interface Infinite { } +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 adcd3a0..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 @@ -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/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/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 07f6d69..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,37 +3,42 @@ /** * 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 4f8f8d1..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 @@ -18,50 +18,42 @@ * 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 static final Pattern DOT_PATTERN = Pattern.compile("\\."); - - private CommandManager manager; - /** * 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. */ @@ -96,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; @@ -117,6 +110,7 @@ public Command(T plugin, String name) { /** * This method is called to set the manager of the command. + * * @param manager The manager of the command. */ public void setManager(CommandManager manager) { @@ -125,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); @@ -139,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); @@ -150,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() { @@ -158,30 +155,61 @@ 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() { @@ -197,9 +225,9 @@ public final List getAllLabels() { return labels; } - /** * This method is called to get the subcommands of the command. + * * @return The subcommands of the command. */ public final List> getSubcommands() { @@ -208,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() { @@ -216,6 +245,7 @@ public final List> getArgs() { /** * This method is called to get the optional arguments of the command. + * * @return The optional arguments of the command. */ public final List> getOptionalArgs() { @@ -224,6 +254,7 @@ public final List> getOptionalArgs() { /** * This method is called to check if the command is only to use in game. + * * @return If the command is only to use in game. */ public final boolean inGameOnly() { @@ -232,6 +263,7 @@ public final boolean inGameOnly() { /** * This method is called to get the requirements of the command. + * * @return The requirements of the command. */ public final List> getRequirements() { @@ -240,46 +272,25 @@ public final List> getRequirements() { /** * This method is called to check if the command has infinite arguments. + * * @return If the command has infinite arguments. */ public final boolean isInfiniteArgs() { return infiniteArgs; } - /** - * 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 set if the command is only to use in game. + * * @param gameOnly If the command is only to use in game. */ public final void setGameOnly(boolean gameOnly) { this.gameOnly = gameOnly; } - /** - * 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 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 add aliases to the command. + * * @param aliases The aliases to add. */ public final void addAlias(String... aliases) { @@ -288,6 +299,7 @@ public final void addAlias(String... aliases) { /** * This method is called to add a alias to the command. + * * @param alias The alias to add. */ public final void addAlias(String alias) { @@ -296,6 +308,7 @@ public final void addAlias(String alias) { /** * This method is called to add subcommands to the command. + * * @param commands The subcommands to add. */ @SafeVarargs @@ -307,6 +320,7 @@ public final void addSubCommand(Command... commands) { /** * This method is called to add arguments to the command. + * * @param args The arguments to add. */ public final void addArgs(Object... args) { @@ -325,7 +339,8 @@ public final void addArgs(Object... args) { /** * This method is called to add arguments to the command. - * @param arg The argument to add. + * + * @param arg The argument to add. * @param type The type of the argument to add. */ public final void addArg(String arg, Class type) { @@ -334,9 +349,10 @@ public final void addArg(String arg, Class type) { /** * This method is called to add arguments to the command. - * @param arg The argument to add. + * + * @param arg The argument to add. * @param converter The converter of the argument. - * @param type The type of the argument, can be null if the argument is a string. + * @param type The type of the argument, can be null if the argument is a string. */ public final void addArg(String arg, Class type, TabCompleter converter) { this.add(arg, ArgumentType.of(type), converter, false); @@ -344,6 +360,7 @@ public final void addArg(String arg, Class type, TabCompleter converter) { /** * This method is called to add arguments to the command. + * * @param args The arguments to add. */ public final void addOptionalArgs(Object... args) { @@ -361,7 +378,8 @@ public final void addOptionalArgs(Object... args) { /** * This method is called to add optional arguments to the command. - * @param arg The argument to add. + * + * @param arg The argument to add. * @param type The type of the argument to add. */ public final void addOptionalArg(String arg, Class type) { @@ -370,7 +388,8 @@ public final void addOptionalArg(String arg, Class type) { /** * This method is called to add optional arguments to the command. - * @param arg The argument to add. + * + * @param arg The argument to add. * @param converter The converter of the argument. */ public final void addOptionalArg(String arg, Class type, TabCompleter converter) { @@ -401,6 +420,7 @@ private void add(String name, ArgumentType type, TabCompleter completer, bool /** * This method is called to add requirements to the command. + * * @param requirement The requirements to add. */ @SafeVarargs @@ -410,6 +430,7 @@ public final void addRequirements(Requirement... requirement) { /** * Check if the command is subcommand + * * @return if the command is subcommand */ public final boolean isSubCommand() { @@ -418,6 +439,7 @@ public final boolean isSubCommand() { /** * This method is called to get the plugin that owns the command. + * * @return The plugin that owns the command. */ public final T getPlugin() { @@ -426,8 +448,9 @@ public final T getPlugin() { /** * This method is called to generate a default usage for the command. + * * @param sender The sender of the command. - * @param label The label of the command. + * @param label The label of the command. * @return The default usage of the command. */ public String generateDefaultUsage(S sender, String label) { @@ -476,22 +499,24 @@ public String generateDefaultUsage(S sender, String label) { return usage.toString(); } - /** - * Change the state of the command - * @param state the new state for the command - */ - public void setEnabled(boolean state) { - this.enable = state; - } - /** * Check if the command is enabled + * * @return if the command is enabled */ public boolean isEnabled() { return enable; } + /** + * Change the state of the command + * + * @param state the new state for the command + */ + public void setEnabled(boolean state) { + this.enable = state; + } + /** * Set if the command is subcommand */ diff --git a/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java b/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java index dca9049..8c1a0df 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/CommandInvoker.java @@ -17,9 +17,10 @@ /** * CommandInvoker is responsible for invoking and suggesting commands. * It performs lookup, permission and requirement checks, usage display, parsing, and execution. + * * @param manager the command manager to use for command handling - * @param plugin type - * @param sender type + * @param plugin type + * @param sender type */ public record CommandInvoker(CommandManager manager) { diff --git a/core/src/main/java/fr/traqueur/commands/api/models/CommandPlatform.java b/core/src/main/java/fr/traqueur/commands/api/models/CommandPlatform.java index 2232542..49a53e2 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/CommandPlatform.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/CommandPlatform.java @@ -4,7 +4,8 @@ import java.util.logging.Logger; -/** * Represents a command platform. +/** + * Represents a command platform. *

This interface is used to manage commands in a specific platform.

* * @param The type of the plugin. @@ -36,7 +37,7 @@ public interface CommandPlatform { /** * Checks if the sender has a specific permission. * - * @param sender The sender to check. + * @param sender The sender to check. * @param permission The permission to check. * @return true if the sender has the permission, false otherwise. */ @@ -53,7 +54,7 @@ public interface CommandPlatform { /** * Sends a message to the sender. * - * @param sender The sender to send the message to. + * @param sender The sender to send the message to. * @param message The message to send. */ void sendMessage(S sender, String message); @@ -62,14 +63,14 @@ public interface CommandPlatform { * Adds a command to the platform. * * @param command The command to add. - * @param label The label of the command. + * @param label The label of the command. */ void addCommand(Command command, String label); /** * Removes a command from the platform. * - * @param label The label of the command to remove. + * @param label The label of the command to remove. * @param subcommand true if the command is a subcommand, false otherwise. */ void removeCommand(String label, boolean subcommand); diff --git a/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java b/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java index 97559cd..4225d94 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java @@ -36,46 +36,6 @@ public class CommandTree { private CommandNode root; - /** - * A node representing one segment in the command path. - */ - public static class CommandNode { - - private final String label; - private final CommandNode parent; - private final Map> children = new HashMap<>(); - private Command command; - private boolean hadChildren = false; - - public CommandNode(String label, CommandNode parent) { - this.label = label; - this.parent = parent; - } - - public String getLabel() { - return label; - } - - public String getFullLabel() { - if (parent == null || parent.label == null) return label; - return parent.getFullLabel() + "." + label; - } - - public Optional> getCommand() { - return Optional.ofNullable(command); - } - - public Map> getChildren() { - return Collections.unmodifiableMap(children); - } - } - - /** - * Result of a lookup: the deepest matching node and leftover args. - */ - public record MatchResult(CommandNode node, String[] args) { - } - public CommandTree() { this.root = new CommandNode<>(null, null); } @@ -133,7 +93,7 @@ private void validateLabel(String label) { /** * Validate a single segment of a label. * - * @param segment the segment to validate + * @param segment the segment to validate * @param fullLabel the full label for error messages * @throws IllegalArgumentException if invalid */ @@ -240,4 +200,44 @@ private void clearOrPruneEmpty(CommandNode node) { public CommandNode getRoot() { return root; } + + /** + * A node representing one segment in the command path. + */ + public static class CommandNode { + + private final String label; + private final CommandNode parent; + private final Map> children = new HashMap<>(); + private Command command; + private boolean hadChildren = false; + + public CommandNode(String label, CommandNode parent) { + this.label = label; + this.parent = parent; + } + + public String getLabel() { + return label; + } + + public String getFullLabel() { + if (parent == null || parent.label == null) return label; + return parent.getFullLabel() + "." + label; + } + + public Optional> getCommand() { + return Optional.ofNullable(command); + } + + public Map> getChildren() { + return Collections.unmodifiableMap(children); + } + } + + /** + * Result of a lookup: the deepest matching node and leftover args. + */ + public record MatchResult(CommandNode node, String[] args) { + } } \ No newline at end of file diff --git a/core/src/main/java/fr/traqueur/commands/api/parsing/ParseError.java b/core/src/main/java/fr/traqueur/commands/api/parsing/ParseError.java index eeeb7d8..485ee27 100644 --- a/core/src/main/java/fr/traqueur/commands/api/parsing/ParseError.java +++ b/core/src/main/java/fr/traqueur/commands/api/parsing/ParseError.java @@ -5,15 +5,6 @@ public record ParseError(Type type, String input, String message) { - public enum Type { - TYPE_NOT_FOUND, - CONVERSION_FAILED, - ARGUMENT_TOO_LONG, - MISSING_REQUIRED, - INVALID_FORMAT - } - - public static ParseError typeNotFound(String argName, String typeKey) { return new ParseError(Type.TYPE_NOT_FOUND, argName, typeKey, "Unknown argument type: " + typeKey); @@ -33,4 +24,12 @@ public static ParseError missingRequired(String argName) { return new ParseError(Type.MISSING_REQUIRED, argName, null, "Missing required argument: " + argName); } + + public enum Type { + TYPE_NOT_FOUND, + CONVERSION_FAILED, + ARGUMENT_TOO_LONG, + MISSING_REQUIRED, + INVALID_FORMAT + } } diff --git a/core/src/main/java/fr/traqueur/commands/api/parsing/ParseResult.java b/core/src/main/java/fr/traqueur/commands/api/parsing/ParseResult.java index 16a7b0e..fb0d376 100644 --- a/core/src/main/java/fr/traqueur/commands/api/parsing/ParseResult.java +++ b/core/src/main/java/fr/traqueur/commands/api/parsing/ParseResult.java @@ -8,14 +8,6 @@ public record ParseResult( int consumedCount ) { - public boolean isSuccess() { - return error == null && arguments != null; - } - - public boolean isError() { - return error != null; - } - /** * Create a successful result. */ @@ -30,5 +22,13 @@ public static ParseResult error(ParseError error) { return new ParseResult(null, error, 0); } + public boolean isSuccess() { + return error == null && arguments != null; + } + + public boolean isError() { + return error != null; + } + } diff --git a/core/src/main/java/fr/traqueur/commands/api/requirements/Requirement.java b/core/src/main/java/fr/traqueur/commands/api/requirements/Requirement.java index e3d5f26..eec0c19 100644 --- a/core/src/main/java/fr/traqueur/commands/api/requirements/Requirement.java +++ b/core/src/main/java/fr/traqueur/commands/api/requirements/Requirement.java @@ -3,14 +3,16 @@ /** * The interface Requirement. *

- * This interface is used to represent a requirement for commandsender externally of command execution environement. + * This interface is used to represent a requirement for commandsender externally of command execution environement. *

+ * * @param The type of the sender that will use this requirement. */ public interface Requirement { /** * Check if the sender meet the requirement. + * * @param sender The sender * @return true if the sender meet the requirement, false otherwise */ @@ -18,6 +20,7 @@ public interface Requirement { /** * Get the error message if the sender doesn't meet the requirement. + * * @return The error message */ String errorMessage(); diff --git a/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java b/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java index 8da7a73..c0510da 100644 --- a/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java +++ b/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java @@ -28,8 +28,15 @@ public class Updater { } } + /** + * Private constructor to prevent instantiation + */ + private Updater() { + } + /** * Set the URL to use to check for the latest release + * * @param URL_LATEST_RELEASE The URL to use */ public static void setUrlLatestRelease(URL URL_LATEST_RELEASE) { @@ -38,28 +45,25 @@ public static void setUrlLatestRelease(URL URL_LATEST_RELEASE) { /** * Set the logger to use for logging messages + * * @param LOGGER The logger to use */ public static void setLogger(Logger LOGGER) { Updater.LOGGER = LOGGER; } - /** - * Private constructor to prevent instantiation - */ - private Updater() {} - /** * Check if the plugin is up to date and log a warning if it's not */ public static void checkUpdates() { - if(!Updater.isUpToDate()) { + if (!Updater.isUpToDate()) { LOGGER.warning("The framework is not up to date, the latest version is " + Updater.fetchLatestVersion()); } } /** * Get the version of the plugin + * * @return The version of the plugin */ public static String getVersion() { @@ -74,6 +78,7 @@ public static String getVersion() { /** * Check if the plugin is up to date + * * @return True if the plugin is up to date, false otherwise */ public static boolean isUpToDate() { @@ -87,6 +92,7 @@ public static boolean isUpToDate() { /** * Get the latest version of the plugin + * * @return The latest version of the plugin */ public static String fetchLatestVersion() { @@ -103,6 +109,7 @@ public static String fetchLatestVersion() { /** * Get the latest version of the plugin + * * @return The latest version of the plugin */ private static String getString() throws IOException { diff --git a/core/src/main/java/fr/traqueur/commands/impl/arguments/BooleanArgument.java b/core/src/main/java/fr/traqueur/commands/impl/arguments/BooleanArgument.java index e651402..0c5bf7d 100644 --- a/core/src/main/java/fr/traqueur/commands/impl/arguments/BooleanArgument.java +++ b/core/src/main/java/fr/traqueur/commands/impl/arguments/BooleanArgument.java @@ -9,6 +9,7 @@ /** * BooleanArgument is the argument that allow to get a boolean from a string. * It's used in the CommandManager to get a boolean from a string. + * * @param the type of the sender */ public class BooleanArgument implements ArgumentConverter, TabCompleter { @@ -22,10 +23,10 @@ public BooleanArgument() { @Override public Boolean apply(String s) { - if(s == null || s.isEmpty()) { + if (s == null || s.isEmpty()) { return null; } - if(s.equalsIgnoreCase("true") || s.equalsIgnoreCase("false")) { + if (s.equalsIgnoreCase("true") || s.equalsIgnoreCase("false")) { return Boolean.parseBoolean(s); } return null; diff --git a/core/src/main/java/fr/traqueur/commands/impl/arguments/DoubleArgument.java b/core/src/main/java/fr/traqueur/commands/impl/arguments/DoubleArgument.java index c355e57..81129f8 100644 --- a/core/src/main/java/fr/traqueur/commands/impl/arguments/DoubleArgument.java +++ b/core/src/main/java/fr/traqueur/commands/impl/arguments/DoubleArgument.java @@ -11,10 +11,12 @@ public class DoubleArgument implements ArgumentConverter { /** * Default constructor. */ - public DoubleArgument() {} + public DoubleArgument() { + } /** * Convert a string to a double + * * @param input the string to convert * @return the double or null if the string is not a double */ @@ -25,7 +27,7 @@ public Double apply(String input) { } try { return Double.valueOf(input); - } catch (NumberFormatException e){ + } catch (NumberFormatException e) { return null; } } diff --git a/core/src/main/java/fr/traqueur/commands/impl/arguments/EnumArgument.java b/core/src/main/java/fr/traqueur/commands/impl/arguments/EnumArgument.java index fb1da16..e3f4591 100644 --- a/core/src/main/java/fr/traqueur/commands/impl/arguments/EnumArgument.java +++ b/core/src/main/java/fr/traqueur/commands/impl/arguments/EnumArgument.java @@ -15,18 +15,6 @@ */ public class EnumArgument, S> implements ArgumentConverter, TabCompleter { - /** - * Creates a new EnumArgument instance for the specified enum class. - * - * @param enumClass The class of the enum - * @param The type of the enum - * @param The type of the sender (e.g., player, console) - * @return A new instance of EnumArgument - */ - public static , S> EnumArgument of(Class enumClass) { - return new EnumArgument<>(enumClass); - } - /** * The class of the enum type this argument converter handles. */ @@ -41,6 +29,18 @@ public EnumArgument(Class clazz) { this.clazz = clazz; } + /** + * Creates a new EnumArgument instance for the specified enum class. + * + * @param enumClass The class of the enum + * @param The type of the enum + * @param The type of the sender (e.g., player, console) + * @return A new instance of EnumArgument + */ + public static , S> EnumArgument of(Class enumClass) { + return new EnumArgument<>(enumClass); + } + /** * Gets the class of the enum type this argument converter handles. * diff --git a/core/src/main/java/fr/traqueur/commands/impl/arguments/IntegerArgument.java b/core/src/main/java/fr/traqueur/commands/impl/arguments/IntegerArgument.java index 9d82234..8ab98f2 100644 --- a/core/src/main/java/fr/traqueur/commands/impl/arguments/IntegerArgument.java +++ b/core/src/main/java/fr/traqueur/commands/impl/arguments/IntegerArgument.java @@ -11,7 +11,8 @@ public class IntegerArgument implements ArgumentConverter { /** * Default constructor. */ - public IntegerArgument() {} + public IntegerArgument() { + } /** * Converts a string to an integer. diff --git a/core/src/main/java/fr/traqueur/commands/impl/arguments/LongArgument.java b/core/src/main/java/fr/traqueur/commands/impl/arguments/LongArgument.java index d19d1f4..6bd3c9b 100644 --- a/core/src/main/java/fr/traqueur/commands/impl/arguments/LongArgument.java +++ b/core/src/main/java/fr/traqueur/commands/impl/arguments/LongArgument.java @@ -11,7 +11,8 @@ public class LongArgument implements ArgumentConverter { /** * Default constructor. */ - public LongArgument() {} + public LongArgument() { + } /** * Convert a string to a long. diff --git a/core/src/main/java/fr/traqueur/commands/impl/logging/InternalMessageHandler.java b/core/src/main/java/fr/traqueur/commands/impl/logging/InternalMessageHandler.java index 044bcbd..80a245c 100644 --- a/core/src/main/java/fr/traqueur/commands/impl/logging/InternalMessageHandler.java +++ b/core/src/main/java/fr/traqueur/commands/impl/logging/InternalMessageHandler.java @@ -11,7 +11,8 @@ public class InternalMessageHandler implements MessageHandler { /** * Default constructor for the InternalMessageHandler. */ - public InternalMessageHandler() {} + public InternalMessageHandler() { + } /** * {@inheritDoc} diff --git a/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java b/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java index f66249d..1abfdfc 100644 --- a/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java +++ b/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java @@ -1,6 +1,8 @@ package fr.traqueur.commands.impl.parsing; -import fr.traqueur.commands.api.arguments.*; +import fr.traqueur.commands.api.arguments.Argument; +import fr.traqueur.commands.api.arguments.ArgumentConverter; +import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.api.logging.Logger; import fr.traqueur.commands.api.models.Command; import fr.traqueur.commands.api.parsing.ArgumentParser; @@ -15,109 +17,109 @@ * Parses String[] arguments using registered converters. */ public class DefaultArgumentParser implements ArgumentParser { - + private static final int MAX_INFINITE_LENGTH = 10_000; - + private final Map> typeConverters; private final Logger logger; - + public DefaultArgumentParser(Map> typeConverters, Logger logger) { this.typeConverters = typeConverters; this.logger = logger; } - + @Override public ParseResult parse(Command command, String[] rawArgs) { Arguments arguments = new Arguments(logger); - + List> required = command.getArgs(); List> optional = command.getOptionalArgs(); - + int argIndex = 0; - + // Parse required arguments for (Argument arg : required) { if (arg.isInfinite()) { return parseInfinite(arguments, arg, rawArgs, argIndex); } - + if (argIndex >= rawArgs.length) { return ParseResult.error(new ParseError( - ParseError.Type.MISSING_REQUIRED, - arg.name(), - null, - "Missing required argument: " + arg.name() + ParseError.Type.MISSING_REQUIRED, + arg.name(), + null, + "Missing required argument: " + arg.name() )); } - + ParseResult result = parseSingle(arguments, arg, rawArgs[argIndex]); if (result.isError()) { return result; } argIndex++; } - + // Parse optional arguments for (Argument arg : optional) { if (argIndex >= rawArgs.length) { break; } - + if (arg.isInfinite()) { return parseInfinite(arguments, arg, rawArgs, argIndex); } - + ParseResult result = parseSingle(arguments, arg, rawArgs[argIndex]); if (result.isError()) { return result; } argIndex++; } - + return ParseResult.success(arguments, argIndex); } - + private ParseResult parseSingle(Arguments arguments, Argument arg, String input) { String typeKey = arg.type().key(); ArgumentConverter.Wrapper wrapper = typeConverters.get(typeKey); - + if (wrapper == null) { return ParseResult.error(new ParseError( - ParseError.Type.TYPE_NOT_FOUND, - arg.name(), - input, - "No converter for type: " + typeKey + ParseError.Type.TYPE_NOT_FOUND, + arg.name(), + input, + "No converter for type: " + typeKey )); } - + if (!wrapper.convertAndApply(input, arg.name(), arguments)) { return ParseResult.error(new ParseError( - ParseError.Type.CONVERSION_FAILED, - arg.name(), - input, - "Failed to convert: " + input + ParseError.Type.CONVERSION_FAILED, + arg.name(), + input, + "Failed to convert: " + input )); } - + return ParseResult.success(arguments, 1); } - + private ParseResult parseInfinite(Arguments arguments, Argument arg, String[] rawArgs, int startIndex) { if (startIndex >= rawArgs.length) { arguments.add(arg.name(), String.class, ""); return ParseResult.success(arguments, 0); } - + StringBuilder sb = new StringBuilder(); int count = 0; - + for (int i = startIndex; i < rawArgs.length; i++) { if (sb.length() > MAX_INFINITE_LENGTH) { return ParseResult.error(new ParseError( - ParseError.Type.ARGUMENT_TOO_LONG, - arg.name(), - null, - "Infinite argument exceeds max length" + ParseError.Type.ARGUMENT_TOO_LONG, + arg.name(), + null, + "Infinite argument exceeds max length" )); } if (i > startIndex) { @@ -126,7 +128,7 @@ private ParseResult parseInfinite(Arguments arguments, Argument arg, String[] sb.append(rawArgs[i]); count++; } - + arguments.add(arg.name(), String.class, sb.toString()); return ParseResult.success(arguments, count); } diff --git a/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java b/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java index 1f01124..3ed9663 100644 --- a/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java @@ -36,24 +36,6 @@ void setUp() { manager.setLogger(logger); } - static class DummyCommand extends Command { - DummyCommand() { super(null, "dummy"); } - DummyCommand(String name) { super(null, name); } - @Override public void execute(String sender, Arguments args) {} - } - - static class FakePlatform implements CommandPlatform { - List added = new ArrayList<>(); - @Override public Object getPlugin() { return null; } - @Override public void injectManager(CommandManager cm) {} - @Override public java.util.logging.Logger getLogger() { return java.util.logging.Logger.getAnonymousLogger(); } - @Override public boolean hasPermission(String sender, String permission) { return true; } - @Override public boolean isPlayer(String sender) {return false;} - @Override public void sendMessage(String sender, String message) {} - @Override public void addCommand(Command command, String label) { added.add(label); } - @Override public void removeCommand(String label, boolean sub) {} - } - // ----- TESTS ----- @Test void testInfiniteArgsParsing() throws Exception { @@ -189,6 +171,61 @@ void addCommand_shouldRegisterCompletersForArgs() { assertTrue(map.containsKey(2)); } + static class DummyCommand extends Command { + DummyCommand() { + super(null, "dummy"); + } + + DummyCommand(String name) { + super(null, name); + } + + @Override + public void execute(String sender, Arguments args) { + } + } + + static class FakePlatform implements CommandPlatform { + List added = new ArrayList<>(); + + @Override + public Object getPlugin() { + return null; + } + + @Override + public void injectManager(CommandManager cm) { + } + + @Override + public java.util.logging.Logger getLogger() { + return java.util.logging.Logger.getAnonymousLogger(); + } + + @Override + public boolean hasPermission(String sender, String permission) { + return true; + } + + @Override + public boolean isPlayer(String sender) { + return false; + } + + @Override + public void sendMessage(String sender, String message) { + } + + @Override + public void addCommand(Command command, String label) { + added.add(label); + } + + @Override + public void removeCommand(String label, boolean sub) { + } + } + static class FakeLogger extends InternalLogger { private final List errors = new ArrayList<>(); diff --git a/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java b/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java index 49b612f..628d0f1 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java @@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; diff --git a/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java b/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java index fbb507a..2a147b1 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java @@ -13,20 +13,6 @@ class CommandTest { - private static class DummyCommand extends Command { - DummyCommand(String name) { - super("plugin", name); - } - DummyCommand() { - super("plugin", "dummy"); - } - - @Override - public void execute(Object sender, Arguments arguments) { - // no-op - } - } - private DummyCommand cmd; @BeforeEach @@ -167,15 +153,44 @@ public void removeCommand(String label, boolean subcommand) { void testUnregisterDelegatesToManager() { AtomicBoolean called = new AtomicBoolean(false); CommandManager fakeManager = new CommandManager(new CommandPlatform() { - @Override public String getPlugin() { return null; } - @Override public void injectManager(CommandManager commandManager) {} - @Override public java.util.logging.Logger getLogger() { return java.util.logging.Logger.getAnonymousLogger(); } - @Override public boolean hasPermission(Object sender, String permission) { return true; } - @Override public boolean isPlayer(Object sender) {return false;} - @Override public void sendMessage(Object sender, String message) {} - @Override public void addCommand(Command command, String label) {} - @Override public void removeCommand(String label, boolean subcommand) { called.set(true); } - }) {}; + @Override + public String getPlugin() { + return null; + } + + @Override + public void injectManager(CommandManager commandManager) { + } + + @Override + public java.util.logging.Logger getLogger() { + return java.util.logging.Logger.getAnonymousLogger(); + } + + @Override + public boolean hasPermission(Object sender, String permission) { + return true; + } + + @Override + public boolean isPlayer(Object sender) { + return false; + } + + @Override + public void sendMessage(Object sender, String message) { + } + + @Override + public void addCommand(Command command, String label) { + } + + @Override + public void removeCommand(String label, boolean subcommand) { + called.set(true); + } + }) { + }; cmd.setManager(fakeManager); cmd.unregister(); assertTrue(called.get()); @@ -212,7 +227,7 @@ void usage_withSubcommands() { cmd.addSubCommand(subA, subB); String usage = cmd.generateDefaultUsage(null, "dummy"); // extract first angle bracket content - String inside = usage.substring(usage.indexOf('<')+1, usage.indexOf('>')); + String inside = usage.substring(usage.indexOf('<') + 1, usage.indexOf('>')); List parts = Arrays.asList(inside.split("\\|")); assertTrue(parts.contains("suba")); assertTrue(parts.contains("subb")); @@ -233,13 +248,13 @@ void usage_subsAndArgsCombined() { assertTrue(usage.contains("[opt:string]")); } - // --- v5.0.0 new tests --- - @Test void setEnabled_defaultIsTrue() { assertTrue(cmd.isEnabled()); } + // --- v5.0.0 new tests --- + @Test void setEnabled_canDisable() { cmd.setEnabled(false); @@ -295,4 +310,19 @@ void getAllLabels_nameAlwaysFirst() { assertEquals("dummy", labels.get(0)); } + + private static class DummyCommand extends Command { + DummyCommand(String name) { + super("plugin", name); + } + + DummyCommand() { + super("plugin", "dummy"); + } + + @Override + public void execute(Object sender, Arguments arguments) { + // no-op + } + } } \ No newline at end of file diff --git a/core/src/test/java/fr/traqueur/commands/api/models/collections/CommandTreeTest.java b/core/src/test/java/fr/traqueur/commands/api/models/collections/CommandTreeTest.java index 1c5c782..bcbb375 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/collections/CommandTreeTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/collections/CommandTreeTest.java @@ -24,9 +24,9 @@ void setup() { @Test void testAddAndFindRoot() { - tree.addCommand("root",rootCmd); + tree.addCommand("root", rootCmd); // find base with no args - Optional> match = tree.findNode("root", new String[]{}); + Optional> match = tree.findNode("root", new String[]{}); assertTrue(match.isPresent()); assertEquals(rootCmd, match.get().node().getCommand().orElse(null)); // full label @@ -38,18 +38,18 @@ void testAddNestedAndFind() { // create root.sub command hierarchy rootCmd.addSubCommand(subCmd); subCmd.addSubCommand(subSubCmd); - tree.addCommand("root",rootCmd); - tree.addCommand("root.sub",subCmd); - tree.addCommand("root.sub.subsub",subSubCmd); + tree.addCommand("root", rootCmd); + tree.addCommand("root.sub", subCmd); + tree.addCommand("root.sub.subsub", subSubCmd); // find sub - Optional> m1 = tree.findNode("root", new String[]{"sub"}); + Optional> m1 = tree.findNode("root", new String[]{"sub"}); assertTrue(m1.isPresent()); assertEquals(subCmd, m1.get().node().getCommand().orElse(null)); assertArrayEquals(new String[]{}, m1.get().args()); // find sub.subsub - Optional> m2 = tree.findNode("root", new String[]{"sub", "subsub"}); + Optional> m2 = tree.findNode("root", new String[]{"sub", "subsub"}); assertTrue(m2.isPresent()); assertEquals(subSubCmd, m2.get().node().getCommand().orElse(null)); assertArrayEquals(new String[]{}, m2.get().args()); @@ -57,9 +57,9 @@ void testAddNestedAndFind() { @Test void testFindNodeWithExtraArgs() { - tree.addCommand("root",rootCmd); + tree.addCommand("root", rootCmd); // root takes no args, so extra args are leftover - Optional> m = tree.findNode("root", new String[]{"a","b","c"}); + Optional> m = tree.findNode("root", new String[]{"a", "b", "c"}); assertTrue(m.isPresent()); assertEquals(rootCmd, m.get().node().getCommand().orElse(null)); assertArrayEquals(new String[]{"a", "b", "c"}, m.get().args()); @@ -67,16 +67,16 @@ void testFindNodeWithExtraArgs() { @Test void testFindNonexistent() { - tree.addCommand("root",rootCmd); - Optional> m = tree.findNode("unknown", new String[]{}); + tree.addCommand("root", rootCmd); + Optional> m = tree.findNode("unknown", new String[]{}); assertFalse(m.isPresent()); } @Test void testRemoveCommandClearOnly() { - tree.addCommand("root",rootCmd); + tree.addCommand("root", rootCmd); tree.removeCommand("root", false); - Optional> m = tree.findNode("root", new String[]{}); + Optional> m = tree.findNode("root", new String[]{}); assertFalse(m.isPresent()); } @@ -85,17 +85,17 @@ void testRemoveCommandKeepChildren() { // add root and sub rootCmd.addSubCommand(subCmd); tree.addCommand("root", rootCmd); - tree.addCommand("root.sub",subCmd); + tree.addCommand("root.sub", subCmd); // remove root only, keep children tree.removeCommand("root", false); // root command cleared but sub-tree remains - Optional> mSub = tree.findNode("root", new String[]{"sub"}); + Optional> mSub = tree.findNode("root", new String[]{"sub"}); assertTrue(mSub.isPresent()); assertEquals(subCmd, mSub.get().node().getCommand().orElse(null)); // find root itself => cleared, so no command at root - Optional> mRoot = tree.findNode("root", new String[]{}); + Optional> mRoot = tree.findNode("root", new String[]{}); assertFalse(mRoot.get().node().getCommand().isPresent()); } @@ -104,13 +104,13 @@ void testRemoveCommandPruneBranch() { // add nested commands rootCmd.addSubCommand(subCmd); subCmd.addSubCommand(subSubCmd); - tree.addCommand("root",rootCmd); - tree.addCommand("root.sub",subCmd); - tree.addCommand("root.sub.subsub",subSubCmd); + tree.addCommand("root", rootCmd); + tree.addCommand("root.sub", subCmd); + tree.addCommand("root.sub.subsub", subSubCmd); // remove entire branch tree.removeCommand("root.sub", true); - Optional> rootopt = tree.findNode("root", new String[]{"sub"}); + Optional> rootopt = tree.findNode("root", new String[]{"sub"}); assertTrue(rootopt.isPresent()); assertEquals(rootCmd, rootopt.get().node().getCommand().orElse(null)); rootopt = tree.findNode("root", new String[]{"sub", "subsub"}); @@ -123,13 +123,13 @@ void testRemoveCommandPruneBranch() { void testRemoveCommandPruneBranchWithoutRoot() { rootCmd.addSubCommand(subCmd); subCmd.addSubCommand(subSubCmd); - tree.addCommand("root.sub",subCmd); - tree.addCommand("root.sub.subsub",subSubCmd); + tree.addCommand("root.sub", subCmd); + tree.addCommand("root.sub.subsub", subSubCmd); // remove entire branch tree.removeCommand("root.sub", true); - Optional> opt = tree.findNode("root", new String[]{"sub"}); + Optional> opt = tree.findNode("root", new String[]{"sub"}); assertFalse(opt.isPresent(), "Expected no command at 'root.sub' after pruning"); opt = tree.findNode("root", new String[]{"sub", "subsub"}); assertFalse(opt.isPresent(), "Expected no command at 'root.sub.subsub' after pruning"); @@ -137,7 +137,12 @@ void testRemoveCommandPruneBranchWithoutRoot() { // stub Command to use in tests static class StubCommand extends Command { - public StubCommand(String name) { super(null, name); } - @Override public void execute(String sender, fr.traqueur.commands.api.arguments.Arguments args) {} + public StubCommand(String name) { + super(null, name); + } + + @Override + public void execute(String sender, fr.traqueur.commands.api.arguments.Arguments args) { + } } } diff --git a/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java b/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java index 7ad288f..39b2a8a 100644 --- a/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java @@ -79,12 +79,25 @@ void testCheckUpdates_logsWarningWhenNotUpToDate() { } } - /** Captures log records for assertions */ + /** + * Captures log records for assertions + */ private static class TestLogHandler extends Handler { private final java.util.List records = new java.util.ArrayList<>(); - @Override public void publish(LogRecord record) { records.add(record); } - @Override public void flush() {} - @Override public void close() {} + + @Override + public void publish(LogRecord record) { + records.add(record); + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + boolean anyMatch(Level lvl, java.util.function.Predicate p) { return records.stream() .anyMatch(r -> r.getLevel().equals(lvl) && p.test(r)); diff --git a/core/src/test/java/fr/traqueur/commands/impl/arguments/EnumArgumentTest.java b/core/src/test/java/fr/traqueur/commands/impl/arguments/EnumArgumentTest.java index d497941..0925509 100644 --- a/core/src/test/java/fr/traqueur/commands/impl/arguments/EnumArgumentTest.java +++ b/core/src/test/java/fr/traqueur/commands/impl/arguments/EnumArgumentTest.java @@ -10,7 +10,6 @@ class EnumArgumentTest { - private enum Sample {ONE, TWO, THREE} private EnumArgument converter; @BeforeEach @@ -40,4 +39,6 @@ void testOnCompletion() { assertTrue(completions.contains("TWO")); assertTrue(completions.contains("THREE")); } + + private enum Sample {ONE, TWO, THREE} } diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java index 71fc4cc..7fb0401 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java @@ -10,7 +10,7 @@ /** * Test bot to demonstrate the JDA CommandsAPI. - * + *

* To run this bot: * 1. Set the DISCORD_BOT_TOKEN environment variable * 2. Set the DISCORD_GUILD_ID environment variable (optional, for testing) @@ -20,22 +20,6 @@ public class TestBot { private static final Logger LOGGER = Logger.getLogger(TestBot.class.getName()); - public static void main(String[] args) { - String token = System.getenv("DISCORD_BOT_TOKEN"); - if (token == null || token.isEmpty()) { - LOGGER.severe("DISCORD_BOT_TOKEN environment variable not set!"); - LOGGER.info("Please set your Discord bot token with: export DISCORD_BOT_TOKEN=your_token_here"); - return; - } - - try { - new TestBot(token); - } catch (Exception e) { - LOGGER.severe("Failed to start bot: " + e.getMessage()); - e.printStackTrace(); - } - } - public TestBot(String token) throws InterruptedException { LOGGER.info("Starting Discord bot..."); @@ -71,4 +55,20 @@ public TestBot(String token) throws InterruptedException { LOGGER.info("Bot is fully operational!"); } + + public static void main(String[] args) { + String token = System.getenv("DISCORD_BOT_TOKEN"); + if (token == null || token.isEmpty()) { + LOGGER.severe("DISCORD_BOT_TOKEN environment variable not set!"); + LOGGER.info("Please set your Discord bot token with: export DISCORD_BOT_TOKEN=your_token_here"); + return; + } + + try { + new TestBot(token); + } catch (Exception e) { + LOGGER.severe("Failed to start bot: " + e.getMessage()); + e.printStackTrace(); + } + } } \ No newline at end of file diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java index a9a12a5..65725cd 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java @@ -73,7 +73,7 @@ public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) String reason = jdaArgs.getOptional("reason").orElse("No reason provided"); jdaArgs.reply(String.format("Would kick user %s for reason: %s", - user.getAsMention(), reason)); + user.getAsMention(), reason)); } } @@ -94,7 +94,7 @@ public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) String reason = arguments.getOptional("reason").orElse("No reason provided"); arguments.reply(String.format("Would ban user %s for reason: %s", - user.getAsMention(), reason)); + user.getAsMention(), reason)); } } @@ -138,10 +138,10 @@ public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) ID: %s Members: %d Owner: %s""", - event.getGuild().getName(), - event.getGuild().getId(), - event.getGuild().getMemberCount(), - event.getGuild().getOwner() != null ? event.getGuild().getOwner().getAsMention() : "Unknown" + event.getGuild().getName(), + event.getGuild().getId(), + event.getGuild().getMemberCount(), + event.getGuild().getOwner() != null ? event.getGuild().getOwner().getAsMention() : "Unknown" ); jdaArgs.reply(info); diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/PingCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/PingCommand.java index f6f1367..0db9158 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/PingCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/PingCommand.java @@ -1,6 +1,5 @@ package fr.traqueur.commands.test.commands; -import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.jda.Command; import fr.traqueur.commands.jda.JDAArguments; import fr.traqueur.commands.test.TestBot; @@ -23,7 +22,7 @@ public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) event.reply("Pong! Gateway ping: " + gatewayPing + "ms").queue(response -> { response.retrieveOriginal().queue(message -> { long restPing = message.getTimeCreated().toInstant().toEpochMilli() - - event.getTimeCreated().toInstant().toEpochMilli(); + event.getTimeCreated().toInstant().toEpochMilli(); response.editOriginal("Pong! Gateway ping: " + gatewayPing + "ms | REST ping: " + restPing + "ms").queue(); }); }); diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java index 7aa3e8b..04175ce 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java @@ -20,7 +20,8 @@ * JDA-specific argument parser that uses Discord's OptionMapping. * Discord handles type resolution natively, so no string conversion needed. */ -public record JDAArgumentParser(Logger logger) implements ArgumentParser { +public record JDAArgumentParser( + Logger logger) implements ArgumentParser { @Override public ParseResult parse(Command command, SlashCommandInteractionEvent event) { diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java index ca38f72..34c6156 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java @@ -37,16 +37,14 @@ public class JDAPlatform implements CommandPlatform commandManager; - /** * Map of root command names to their SlashCommandData. */ private final Map slashCommands; + /** + * The command manager. + */ + private CommandManager commandManager; /** * Constructor for JDAPlatform. @@ -131,10 +129,10 @@ public void addCommand(Command command, String // This is a group, not a leaf subcommand - skip it // Its children will be registered as subcommand groups (parts.length >= 3) logger.warning(String.format( - "Command '%s' has subcommands and will be treated as a subcommand group. " + - "Discord does not support executing intermediate groups. " + - "If you want this group to be executable, create a dedicated subcommand (e.g., '%s.list' or '%s.info').", - label, label, label + "Command '%s' has subcommands and will be treated as a subcommand group. " + + "Discord does not support executing intermediate groups. " + + "If you want this group to be executable, create a dedicated subcommand (e.g., '%s.list' or '%s.info').", + label, label, label )); return; } diff --git a/jda/src/main/java/fr/traqueur/commands/jda/requirements/RoleRequirement.java b/jda/src/main/java/fr/traqueur/commands/jda/requirements/RoleRequirement.java index 59f93a7..06d200e 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/requirements/RoleRequirement.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/requirements/RoleRequirement.java @@ -2,7 +2,6 @@ import fr.traqueur.commands.api.requirements.Requirement; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.util.Arrays; diff --git a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/Sub2TestCommand.java b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/Sub2TestCommand.java index 42d7a2a..dbee917 100644 --- a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/Sub2TestCommand.java +++ b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/Sub2TestCommand.java @@ -16,6 +16,6 @@ public Sub2TestCommand(TestPlugin plugin) { @Override public void execute(CommandSender sender, Arguments args) { args.getOptional("test").ifPresent(test -> sender.sendMessage("Test: " + test)); - sender.sendMessage(this.getUsage()); + sender.sendMessage(this.getUsage()); } } diff --git a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/SubTestCommand.java b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/SubTestCommand.java index c81ca64..7bdeb51 100644 --- a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/SubTestCommand.java +++ b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/SubTestCommand.java @@ -12,10 +12,10 @@ public SubTestCommand(TestPlugin plugin) { super(plugin, "sub.inner"); this.addArgs("test", Integer.class); this.addArg("testStr", String.class, (sender, args) -> { - args.forEach(arg -> { - sender.sendMessage("Arg: " + arg); - }); - return List.of(); + args.forEach(arg -> { + sender.sendMessage("Arg: " + arg); + }); + return List.of(); }); this.addAlias("sub"); } diff --git a/spigot/build.gradle b/spigot/build.gradle index 784ef89..a2e9ffc 100644 --- a/spigot/build.gradle +++ b/spigot/build.gradle @@ -16,7 +16,7 @@ repositories { dependencies { api project(":core") compileOnly "org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT" - testImplementation ("org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT") + testImplementation("org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT") } java { diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/Command.java b/spigot/src/main/java/fr/traqueur/commands/spigot/Command.java index 6349678..cce77f0 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/Command.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/Command.java @@ -5,6 +5,7 @@ /** * This implementation of {@link fr.traqueur.commands.api.models.Command} is used to provide a command in Spigot. + * * @param is the type of the plugin, which must extend the main plugin class. */ public abstract class Command extends fr.traqueur.commands.api.models.Command { @@ -20,5 +21,4 @@ public Command(T plugin, String name) { } - } diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/CommandManager.java b/spigot/src/main/java/fr/traqueur/commands/spigot/CommandManager.java index 4a3fed2..f5b242c 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/CommandManager.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/CommandManager.java @@ -9,6 +9,7 @@ /** * This implementation of {@link fr.traqueur.commands.api.CommandManager} is used to provide the command manager in Spigot context. + * * @param The type of the plugin, must extend JavaPlugin. */ public class CommandManager extends fr.traqueur.commands.api.CommandManager { diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotExecutor.java b/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotExecutor.java index f2cb97a..c545c8b 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotExecutor.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotExecutor.java @@ -10,6 +10,7 @@ /** * Represents the executor of the commands. + * * @param The type of the plugin that owns the executor. */ public class SpigotExecutor implements CommandExecutor, org.bukkit.command.TabCompleter { @@ -26,7 +27,8 @@ public class SpigotExecutor implements CommandExecutor, org.bu /** * The constructor of the executor. - * @param plugin The plugin that owns the executor. + * + * @param plugin The plugin that owns the executor. * @param commandManager The command manager. */ public SpigotExecutor(T plugin, CommandManager commandManager) { @@ -36,14 +38,15 @@ public SpigotExecutor(T plugin, CommandManager commandManager) /** * Parse the label of the command. + * * @param label The label of the command. * @return The parsed label or null if the label is not valid. */ private String parseLabel(String label) { - if(label.contains(":")) { + if (label.contains(":")) { String[] split = label.split(":"); label = split[1]; - if(!split[0].equalsIgnoreCase(plugin.getName())) { + if (!split[0].equalsIgnoreCase(plugin.getName())) { return null; } } @@ -52,10 +55,11 @@ private String parseLabel(String label) { /** * This method is called when a command is executed. - * @param sender The sender of the command. + * + * @param sender The sender of the command. * @param command The command executed. - * @param label The label of the command. - * @param args The arguments of the command. + * @param label The label of the command. + * @param args The arguments of the command. * @return If the command is executed. */ @Override @@ -71,16 +75,17 @@ public boolean onCommand(CommandSender sender, org.bukkit.command.Command comman /** * This method is called when a tab is completed. + * * @param commandSender The sender of the command. - * @param command The command completed. - * @param label The label of the command. - * @param args The arguments of the command. + * @param command The command completed. + * @param label The label of the command. + * @param args The arguments of the command. * @return The list of completions. */ @Override public List onTabComplete(CommandSender commandSender, org.bukkit.command.Command command, String label, String[] args) { String labelLower = this.parseLabel(label); - if(labelLower == null) { + if (labelLower == null) { return Collections.emptyList(); } return this.commandManager.getInvoker().suggest(commandSender, labelLower, args); diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotPlatform.java b/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotPlatform.java index bd0f6d8..83ffb37 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotPlatform.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotPlatform.java @@ -14,9 +14,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -69,7 +67,8 @@ public SpigotPlatform(T plugin) { commandMap = (CommandMap) bukkitCommandMap.get(Bukkit.getServer()); pluginConstructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class); pluginConstructor.setAccessible(true); - } catch (IllegalArgumentException | SecurityException | IllegalAccessException | NoSuchFieldException | NoSuchMethodException e) { + } catch (IllegalArgumentException | SecurityException | IllegalAccessException | NoSuchFieldException | + NoSuchMethodException e) { this.getLogger().severe("Unable to get the command map."); plugin.getServer().getPluginManager().disablePlugin(plugin); } @@ -173,7 +172,7 @@ public void addCommand(Command command, String label) { */ @Override public void removeCommand(String label, boolean subcommand) { - if(subcommand && this.commandMap.getCommand(label) != null) { + if (subcommand && this.commandMap.getCommand(label) != null) { Objects.requireNonNull(this.commandMap.getCommand(label)).unregister(commandMap); } } diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/OfflinePlayerArgument.java b/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/OfflinePlayerArgument.java index c3b0c7d..4115106 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/OfflinePlayerArgument.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/OfflinePlayerArgument.java @@ -35,7 +35,8 @@ public class OfflinePlayerArgument implements ArgumentConverter, /** * Creates a new OfflinePlayerArgument. */ - public OfflinePlayerArgument() {} + public OfflinePlayerArgument() { + } /** * {@inheritDoc} diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/PlayerArgument.java b/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/PlayerArgument.java index 3d4b5b4..3ee6b09 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/PlayerArgument.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/arguments/PlayerArgument.java @@ -33,7 +33,8 @@ public class PlayerArgument implements ArgumentConverter, TabCompleter - * This class is used to represent a world requirement. + * This class is used to represent a world requirement. *

*/ public class WorldRequirement implements Requirement { - /** - * Create a new world requirement. - * @param name The name of the world - * @return The world requirement - */ - public static Requirement of(String name) { - return new WorldRequirement(Bukkit.getWorld(name)); - } - /** * The world. */ @@ -30,19 +21,30 @@ public static Requirement of(String name) { /** * Create a new world requirement. + * * @param world The world */ public WorldRequirement(World world) { this.world = world; } + /** + * Create a new world requirement. + * + * @param name The name of the world + * @return The world requirement + */ + public static Requirement of(String name) { + return new WorldRequirement(Bukkit.getWorld(name)); + } + @Override public boolean check(CommandSender sender) { - return sender instanceof Player && this.world != null && ((Player) sender).getWorld().getUID().equals(this.world.getUID()); + return sender instanceof Player && this.world != null && ((Player) sender).getWorld().getUID().equals(this.world.getUID()); } @Override public String errorMessage() { - return "&cSender must be in world " + this.world.getName()+ "."; + return "&cSender must be in world " + this.world.getName() + "."; } } diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/requirements/ZoneRequirement.java b/spigot/src/main/java/fr/traqueur/commands/spigot/requirements/ZoneRequirement.java index c834539..4673ccf 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/requirements/ZoneRequirement.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/requirements/ZoneRequirement.java @@ -9,16 +9,40 @@ /** * The class ZoneRequirement. *

- * This class is used to represent a zone requirement. - * The sender must be in a specific zone to meet the requirement. - * The zone is defined by two locations. + * This class is used to represent a zone requirement. + * The sender must be in a specific zone to meet the requirement. + * The zone is defined by two locations. *

*/ public class ZoneRequirement implements Requirement { + private final Location locationUp; + private final Location locationDown; + + + /** + * Create a new zone requirement. + * + * @param locationUp The first location of the zone. + * @param locationDown The second location of the zone. + */ + public ZoneRequirement(Location locationUp, Location locationDown) { + if (locationUp.getWorld() == null || locationDown.getWorld() == null) { + throw new IllegalArgumentException("The locations must not be null."); + } + + if (!locationUp.getWorld().getName().equals(locationDown.getWorld().getName())) { + throw new IllegalArgumentException("The locations must be in the same world."); + } + + this.locationUp = new Location(locationUp.getWorld(), Math.max(locationUp.getBlockX(), locationDown.getBlockX()), Math.max(locationUp.getBlockY(), locationDown.getBlockY()), Math.max(locationUp.getBlockZ(), locationDown.getBlockZ())); + this.locationDown = new Location(locationDown.getWorld(), Math.min(locationUp.getBlockX(), locationDown.getBlockX()), Math.min(locationUp.getBlockY(), locationDown.getBlockY()), Math.min(locationUp.getBlockZ(), locationDown.getBlockZ())); + } + /** * Create a new zone requirement. - * @param locationUp The first location of the zone. + * + * @param locationUp The first location of the zone. * @param locationDown The second location of the zone. * @return The zone requirement. */ @@ -28,56 +52,37 @@ public static Requirement of(Location locationUp, Location locati /** * Create a new zone requirement. + * * @param world The world of the zone. - * @param x1 The first x coordinate of the zone. - * @param y1 The first y coordinate of the zone. - * @param z1 The first z coordinate of the zone. - * @param x2 The second x coordinate of the zone. - * @param y2 The second y coordinate of the zone. - * @param z2 The second z coordinate of the zone. + * @param x1 The first x coordinate of the zone. + * @param y1 The first y coordinate of the zone. + * @param z1 The first z coordinate of the zone. + * @param x2 The second x coordinate of the zone. + * @param y2 The second y coordinate of the zone. + * @param z2 The second z coordinate of the zone. * @return The zone requirement. */ public static Requirement of(World world, int x1, int y1, int z1, int x2, int y2, int z2) { return new ZoneRequirement(new Location(world, x1, y1, z1), new Location(world, x2, y2, z2)); } - /** * Create a new zone requirement. + * * @param world The world of the zone. - * @param x1 The first x coordinate of the zone. - * @param z1 The first z coordinate of the zone. - * @param x2 The second x coordinate of the zone. - * @param z2 The second z coordinate of the zone. + * @param x1 The first x coordinate of the zone. + * @param z1 The first z coordinate of the zone. + * @param x2 The second x coordinate of the zone. + * @param z2 The second z coordinate of the zone. * @return The zone requirement. */ public static Requirement of(World world, int x1, int z1, int x2, int z2) { return new ZoneRequirement(new Location(world, x1, world.getMaxHeight(), z1), new Location(world, x2, world.getMinHeight(), z2)); } - private final Location locationUp; - private final Location locationDown; - - /** - * Create a new zone requirement. - * @param locationUp The first location of the zone. - * @param locationDown The second location of the zone. - */ - public ZoneRequirement(Location locationUp, Location locationDown) { - if(locationUp.getWorld() == null || locationDown.getWorld() == null) { - throw new IllegalArgumentException("The locations must not be null."); - } - - if(!locationUp.getWorld().getName().equals(locationDown.getWorld().getName())) { - throw new IllegalArgumentException("The locations must be in the same world."); - } - - this.locationUp = new Location(locationUp.getWorld(), Math.max(locationUp.getBlockX(), locationDown.getBlockX()), Math.max(locationUp.getBlockY(), locationDown.getBlockY()), Math.max(locationUp.getBlockZ(), locationDown.getBlockZ())); - this.locationDown = new Location(locationDown.getWorld(), Math.min(locationUp.getBlockX(), locationDown.getBlockX()), Math.min(locationUp.getBlockY(), locationDown.getBlockY()), Math.min(locationUp.getBlockZ(), locationDown.getBlockZ())); - } - /** * Check if player is inside the zone. + * * @return true if player is inside, false otherwise. */ private boolean isInside(Player player) { @@ -91,6 +96,7 @@ private boolean isInside(Player player) { /** * Get the coordinates of a location. + * * @param location The location. * @return The coordinates of the location. */ diff --git a/spigot/src/test/java/fr/traqueur/commands/spigot/SpigotIntegrationTest.java b/spigot/src/test/java/fr/traqueur/commands/spigot/SpigotIntegrationTest.java index e8b62da..9696b27 100644 --- a/spigot/src/test/java/fr/traqueur/commands/spigot/SpigotIntegrationTest.java +++ b/spigot/src/test/java/fr/traqueur/commands/spigot/SpigotIntegrationTest.java @@ -23,15 +23,43 @@ class SpigotIntegrationTest { @BeforeEach void setUp() { manager = new fr.traqueur.commands.api.CommandManager(new CommandPlatform() { - @Override public Object getPlugin() { return null; } - @Override public void injectManager(fr.traqueur.commands.api.CommandManager cm) {} - @Override public java.util.logging.Logger getLogger() { return java.util.logging.Logger.getAnonymousLogger(); } - @Override public boolean hasPermission(CommandSender sender, String permission) { return true; } - @Override public boolean isPlayer(CommandSender sender) {return sender instanceof Player;} - @Override public void sendMessage(CommandSender sender, String message) {} - @Override public void addCommand(Command command, String label) {} - @Override public void removeCommand(String label, boolean subcommand) {} - }) {}; + @Override + public Object getPlugin() { + return null; + } + + @Override + public void injectManager(fr.traqueur.commands.api.CommandManager cm) { + } + + @Override + public java.util.logging.Logger getLogger() { + return java.util.logging.Logger.getAnonymousLogger(); + } + + @Override + public boolean hasPermission(CommandSender sender, String permission) { + return true; + } + + @Override + public boolean isPlayer(CommandSender sender) { + return sender instanceof Player; + } + + @Override + public void sendMessage(CommandSender sender, String message) { + } + + @Override + public void addCommand(Command command, String label) { + } + + @Override + public void removeCommand(String label, boolean subcommand) { + } + }) { + }; manager.registerConverter(Player.class, new PlayerArgument()); bukkitStatic = Mockito.mockStatic(Bukkit.class); } diff --git a/velocity-test-plugin/build.gradle b/velocity-test-plugin/build.gradle index 3b6d4d7..0eecfd3 100644 --- a/velocity-test-plugin/build.gradle +++ b/velocity-test-plugin/build.gradle @@ -27,9 +27,9 @@ dependencies { } tasks { - runVelocity { - velocityVersion("3.4.0-SNAPSHOT") - } + runVelocity { + velocityVersion("3.4.0-SNAPSHOT") + } } def targetJavaVersion = 21 diff --git a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/SubTestCommand.java b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/SubTestCommand.java index 990995b..aa7021c 100644 --- a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/SubTestCommand.java +++ b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/SubTestCommand.java @@ -13,10 +13,10 @@ public SubTestCommand(VelocityTestPlugin plugin) { super(plugin, "sub.inner"); this.addArgs("test", Integer.class); this.addArg("testStr", String.class, (sender, args) -> { - args.forEach(arg -> { - sender.sendMessage(Component.text("Arg: " + arg)); - }); - return List.of(); + args.forEach(arg -> { + sender.sendMessage(Component.text("Arg: " + arg)); + }); + return List.of(); }); this.addAlias("sub"); } diff --git a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/VelocityTestPlugin.java b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/VelocityTestPlugin.java index 37a9ed1..ec21270 100644 --- a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/VelocityTestPlugin.java +++ b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/VelocityTestPlugin.java @@ -9,15 +9,17 @@ import org.slf4j.Logger; @Plugin( - id = "velocity-test-plugin", - name = "velocity-test-plugin", - version = BuildConstants.VERSION - ,authors = {"Traqueur_"} + id = "velocity-test-plugin", + name = "velocity-test-plugin", + version = BuildConstants.VERSION + , authors = {"Traqueur_"} ) public class VelocityTestPlugin { - @Inject private Logger logger; - @Inject private ProxyServer server; + @Inject + private Logger logger; + @Inject + private ProxyServer server; @Subscribe public void onProxyInitialization(ProxyInitializeEvent event) { diff --git a/velocity/src/main/java/fr/traqueur/commands/velocity/Command.java b/velocity/src/main/java/fr/traqueur/commands/velocity/Command.java index f65e016..06991b9 100644 --- a/velocity/src/main/java/fr/traqueur/commands/velocity/Command.java +++ b/velocity/src/main/java/fr/traqueur/commands/velocity/Command.java @@ -4,6 +4,7 @@ /** * This implementation of {@link fr.traqueur.commands.api.models.Command} is used to provide a command in Spigot. + * * @param is the type of the plugin, which must extend the main plugin class. */ public abstract class Command extends fr.traqueur.commands.api.models.Command { diff --git a/velocity/src/main/java/fr/traqueur/commands/velocity/CommandManager.java b/velocity/src/main/java/fr/traqueur/commands/velocity/CommandManager.java index 5a81f13..d0e93dc 100644 --- a/velocity/src/main/java/fr/traqueur/commands/velocity/CommandManager.java +++ b/velocity/src/main/java/fr/traqueur/commands/velocity/CommandManager.java @@ -7,6 +7,7 @@ /** * This implementation of {@link fr.traqueur.commands.api.CommandManager} is used to provide the command manager in Spigot context. + * * @param The type of the plugin, must extend JavaPlugin. */ public class CommandManager extends fr.traqueur.commands.api.CommandManager { @@ -18,7 +19,7 @@ public class CommandManager extends fr.traqueur.commands.api.CommandManager(instance,server, logger)); + public CommandManager(T instance, ProxyServer server, Logger logger) { + super(new VelocityPlatform<>(instance, server, logger)); } } diff --git a/velocity/src/main/java/fr/traqueur/commands/velocity/VelocityPlatform.java b/velocity/src/main/java/fr/traqueur/commands/velocity/VelocityPlatform.java index 90f83f7..34b7839 100644 --- a/velocity/src/main/java/fr/traqueur/commands/velocity/VelocityPlatform.java +++ b/velocity/src/main/java/fr/traqueur/commands/velocity/VelocityPlatform.java @@ -2,14 +2,13 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.proxy.ProxyServer; -import fr.traqueur.commands.api.models.Command; import fr.traqueur.commands.api.CommandManager; +import fr.traqueur.commands.api.models.Command; import fr.traqueur.commands.api.models.CommandPlatform; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; /** @@ -148,7 +147,7 @@ public void addCommand(Command command, String label) { */ @Override public void removeCommand(String label, boolean subcommand) { - if(subcommand && this.server.getCommandManager().getCommandMeta(label) != null) { + if (subcommand && this.server.getCommandManager().getCommandMeta(label) != null) { this.server.getCommandManager().unregister(this.server.getCommandManager().getCommandMeta(label)); } else { this.server.getCommandManager().unregister(label); From 647eef704011cd660b12f1206748da9c1074036e Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 18:02:58 +0100 Subject: [PATCH 10/23] feat: add ci/cd for publishing --- .github/workflows/{test-all.yml => build.yml} | 36 ++-- build.gradle | 157 +++++++++++++++--- core/build.gradle | 19 --- .../traqueur/commands/api/CommandManager.java | 1 + .../commands/api/arguments/Arguments.java | 1 + .../commands/api/updater/Updater.java | 135 +++++++-------- gradle.properties | 2 +- jda/build.gradle | 15 -- .../traqueur/commands/jda/CommandManager.java | 2 +- spigot/build.gradle | 24 +-- velocity/build.gradle | 22 --- 11 files changed, 224 insertions(+), 190 deletions(-) rename .github/workflows/{test-all.yml => build.yml} (54%) diff --git a/.github/workflows/test-all.yml b/.github/workflows/build.yml similarity index 54% rename from .github/workflows/test-all.yml rename to .github/workflows/build.yml index 45851f5..819392e 100644 --- a/.github/workflows/test-all.yml +++ b/.github/workflows/build.yml @@ -1,14 +1,22 @@ -name: CI +name: Build Action on: push: - branches: [ master ] + branches: [ main, develop ] pull_request: - branches: [ master, develop ] + types: [ opened, synchronize, reopened ] workflow_dispatch: +permissions: + contents: read + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - build: + test: runs-on: ubuntu-latest steps: @@ -36,11 +44,15 @@ jobs: - name: Build & test run: ./gradlew clean testAll --no-daemon - - name: Upload test reports - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: | - **/build/test-results/**/*.xml - **/build/reports/tests/** + build: + name: Build and Publish CommandsAPI + uses: GroupeZ-dev/actions/.github/workflows/build.yml@main + with: + project-name: "CommandsAPI" + publish: true + publish-on-discord: false + project-to-publish: "publishAll" + secrets: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} + WEBHOOK_URL: "" \ No newline at end of file diff --git a/build.gradle b/build.gradle index 25ef688..ace48d7 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,6 +11,13 @@ allprojects { plugin 'java-library' } + 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 { mavenCentral() maven { @@ -15,35 +27,138 @@ 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 { + 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 5bce398..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_21 - targetCompatibility = JavaVersion.VERSION_21 - 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/main/java/fr/traqueur/commands/api/CommandManager.java b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java index 35cb89a..8d6fc96 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -381,6 +381,7 @@ private void addCompletionsForLabel(String[] labelParts) { * @param commandSize The size of the command. * @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); 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 9669be4..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 @@ -73,6 +73,7 @@ public T get(String argument) { * @param The type of the argument. * @return The argument. */ + @SuppressWarnings("unchecked") public Optional getOptional(String argument) { if (this.isEmpty()) { return Optional.empty(); diff --git a/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java b/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java index c0510da..83fd827 100644 --- a/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java +++ b/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java @@ -2,129 +2,112 @@ import fr.traqueur.commands.api.exceptions.UpdaterInitializationException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; import java.io.IOException; +import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.util.Properties; -import java.util.Scanner; import java.util.logging.Logger; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + /** * This class is used to check if the plugin is up to date */ public class Updater { private static final String VERSION_PROPERTY_FILE = "commands.properties"; - private static URL URL_LATEST_RELEASE; + private static final URL URL_LATEST_RELEASE; private static Logger LOGGER = Logger.getLogger("CommandsAPI"); static { try { - URL_LATEST_RELEASE = URI.create("https://api.github.com/repos/Traqueur-dev/CommandsAPI/releases/latest").toURL(); + URL_LATEST_RELEASE = URI.create( + "https://repo.groupez.dev/releases/fr/traqueur/commands/core/maven-metadata.xml" + ).toURL(); } catch (MalformedURLException e) { throw new UpdaterInitializationException("Failed to initialize updater URL", e); } } - /** - * Private constructor to prevent instantiation - */ - private Updater() { - } + private Updater() {} - /** - * Set the URL to use to check for the latest release - * - * @param URL_LATEST_RELEASE The URL to use - */ - public static void setUrlLatestRelease(URL URL_LATEST_RELEASE) { - Updater.URL_LATEST_RELEASE = URL_LATEST_RELEASE; - } - - /** - * Set the logger to use for logging messages - * - * @param LOGGER The logger to use - */ - public static void setLogger(Logger LOGGER) { - Updater.LOGGER = LOGGER; + public static void setLogger(Logger logger) { + Updater.LOGGER = logger; } - /** - * Check if the plugin is up to date and log a warning if it's not - */ public static void checkUpdates() { - if (!Updater.isUpToDate()) { - LOGGER.warning("The framework is not up to date, the latest version is " + Updater.fetchLatestVersion()); + try { + String latest = fetchLatestVersion(); + String current = getVersion(); + + if (latest != null && !latest.equals(current)) { + LOGGER.warning("⚠ CommandsAPI is not up to date!"); + LOGGER.warning("Current: " + current + " | Latest: " + latest); + } + } catch (Exception ignored) { } } - /** - * Get the version of the plugin - * - * @return The version of the plugin - */ public static String getVersion() { Properties prop = new Properties(); - try { - prop.load(Updater.class.getClassLoader().getResourceAsStream(VERSION_PROPERTY_FILE)); + try (InputStream is = Updater.class + .getClassLoader() + .getResourceAsStream(VERSION_PROPERTY_FILE)) { + + if (is == null) { + throw new RuntimeException("commands.properties not found"); + } + + prop.load(is); return prop.getProperty("version"); } catch (IOException e) { throw new RuntimeException(e); } } - /** - * Check if the plugin is up to date - * - * @return True if the plugin is up to date, false otherwise - */ public static boolean isUpToDate() { - try { - String latestVersion = fetchLatestVersion(); - return getVersion().equals(latestVersion); - } catch (Exception e) { - return false; - } + String latest = fetchLatestVersion(); + return latest != null && getVersion().equals(latest); } /** - * Get the latest version of the plugin - * - * @return The latest version of the plugin + * Fetch latest version from Reposilite maven-metadata.xml */ public static String fetchLatestVersion() { try { - String responseString = getString(); - int tagNameIndex = responseString.indexOf("\"tag_name\""); - int start = responseString.indexOf('\"', tagNameIndex + 10) + 1; - int end = responseString.indexOf('\"', start); - return responseString.substring(start, end); - } catch (Exception e) { - return null; - } - } + HttpURLConnection connection = + (HttpURLConnection) URL_LATEST_RELEASE.openConnection(); - /** - * Get the latest version of the plugin - * - * @return The latest version of the plugin - */ - private static String getString() throws IOException { - HttpURLConnection connection = (HttpURLConnection) Updater.URL_LATEST_RELEASE.openConnection(); - connection.setRequestMethod("GET"); - - StringBuilder response = new StringBuilder(); - try (Scanner scanner = new Scanner(connection.getInputStream())) { - while (scanner.hasNext()) { - response.append(scanner.nextLine()); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + connection.setRequestMethod("GET"); + + try (InputStream is = connection.getInputStream()) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(is); + + // Priorité à , fallback + NodeList release = document.getElementsByTagName("release"); + if (release.getLength() > 0) { + return release.item(0).getTextContent(); + } + + NodeList latest = document.getElementsByTagName("latest"); + if (latest.getLength() > 0) { + return latest.item(0).getTextContent(); + } } - } finally { - connection.disconnect(); + } catch (Exception e) { + LOGGER.warning("Failed to fetch latest version: " + e.getMessage()); } - return response.toString(); + return null; } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 5178958..f81469e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=4.3.1 \ No newline at end of file +version=5.0.0 \ No newline at end of file diff --git a/jda/build.gradle b/jda/build.gradle index cdbe16e..50067e4 100644 --- a/jda/build.gradle +++ b/jda/build.gradle @@ -1,7 +1,3 @@ -plugins { - id 'maven-publish' -} - repositories { mavenCentral() } @@ -21,15 +17,4 @@ java { targetCompatibility = JavaVersion.VERSION_21 withSourcesJar() withJavadocJar() -} - -publishing { - publications { - maven(MavenPublication) { - from components.java - groupId = project.group - artifactId = 'platform-jda' - version = project.version - } - } } \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java b/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java index d1b9a7f..c01773b 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java @@ -27,7 +27,7 @@ public class CommandManager extends fr.traqueur.commands.api.CommandManager(bot, jda, logger)); - this.jdaPlatform = (JDAPlatform) getPlatform(); + this.jdaPlatform = (JDAPlatform) super.getPlatform(); } /** diff --git a/spigot/build.gradle b/spigot/build.gradle index a2e9ffc..e8f41a8 100644 --- a/spigot/build.gradle +++ b/spigot/build.gradle @@ -1,7 +1,3 @@ -plugins { - id 'maven-publish' -} - repositories { mavenCentral() maven { @@ -17,22 +13,4 @@ dependencies { api project(":core") compileOnly "org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT" testImplementation("org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT") -} - -java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - withSourcesJar() - withJavadocJar() -} - -publishing { - publications { - maven(MavenPublication) { - from components.java - groupId = project.group - artifactId = 'platform-spigot' - version = project.version - } - } -} +} \ No newline at end of file diff --git a/velocity/build.gradle b/velocity/build.gradle index 6b44755..2dec86f 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -1,7 +1,3 @@ -plugins { - id 'maven-publish' -} - repositories { mavenCentral() maven { @@ -19,21 +15,3 @@ dependencies { compileOnly("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT") annotationProcessor("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT") } - -java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - withSourcesJar() - withJavadocJar() -} - -publishing { - publications { - maven(MavenPublication) { - from components.java - groupId = project.group - artifactId = 'platform-velocity' - version = project.version - } - } -} From 8aeb8bc8367b4f9f7518375d1eeaa8e1dec530ea Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 18:03:11 +0100 Subject: [PATCH 11/23] feat: remove jitpack --- jitpack.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 jitpack.yml diff --git a/jitpack.yml b/jitpack.yml deleted file mode 100644 index 097b769..0000000 --- a/jitpack.yml +++ /dev/null @@ -1,6 +0,0 @@ -jdk: - - openjdk21 - -build: - commands: - - ./gradlew core:publishToMavenLocal spigot:publishToMavenLocal velocity:publishToMavenLocal jda:publishToMavenLocal \ No newline at end of file From 53311b75c9d54073e09139b1ac6e26180c7388be Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 18:05:43 +0100 Subject: [PATCH 12/23] feat: update readme --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3c47c3a..b7d579c 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,11 @@ traqueur-dev-commandsapi/ ```groovy repositories { - maven { url 'https://jitpack.io' } + maven { url 'https://repo.groupez.dev/' } // snapshots or releases } dependencies { - implementation 'com.github.Traqueur-dev.CommandsAPI:platform-spigot:[version]' // or platform-velocity + implementation 'fr.traqueur.commands:platform-spigot:[version]' // or platform-velocity or platform- } ``` @@ -62,15 +62,15 @@ dependencies { ```xml - jitpack.io - https://jitpack.io + groupez-releases + https://repo.groupez.dev/releases - com.github.Traqueur-dev.CommandsAPI - platform-spigot + fr.traqueur.commands + platform-spigot [version] From 6efc4119d61ba7316eb4f5fc498a2200eaf8f0a6 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Tue, 30 Dec 2025 18:08:58 +0100 Subject: [PATCH 13/23] fix: rename master branch in ci --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 819392e..b4031e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Build Action on: push: - branches: [ main, develop ] + branches: [ master, develop ] pull_request: types: [ opened, synchronize, reopened ] workflow_dispatch: From 74968ec810581622d9327b6886f15125b66ad89a Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Wed, 31 Dec 2025 10:18:42 +0100 Subject: [PATCH 14/23] fix(core): improve updater --- .../commands/api/updater/Updater.java | 188 +++++++++++++----- .../commands/api/updater/UpdaterTest.java | 67 ++++--- 2 files changed, 181 insertions(+), 74 deletions(-) diff --git a/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java b/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java index 83fd827..6b6b54e 100644 --- a/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java +++ b/core/src/main/java/fr/traqueur/commands/api/updater/Updater.java @@ -1,59 +1,142 @@ package fr.traqueur.commands.api.updater; import fr.traqueur.commands.api.exceptions.UpdaterInitializationException; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; -import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.util.Properties; +import java.util.concurrent.CompletableFuture; import java.util.logging.Logger; -import org.w3c.dom.Document; -import org.w3c.dom.NodeList; - /** - * This class is used to check if the plugin is up to date + * Non-blocking updater using Reposilite (releases -> snapshots fallback) */ -public class Updater { +public final class Updater { private static final String VERSION_PROPERTY_FILE = "commands.properties"; - private static final URL URL_LATEST_RELEASE; + + private static final URL RELEASES_URL; + private static final URL SNAPSHOTS_URL; + + private static volatile URL RESOLVED_METADATA_URL; + private static Logger LOGGER = Logger.getLogger("CommandsAPI"); static { try { - URL_LATEST_RELEASE = URI.create( + RELEASES_URL = URI.create( "https://repo.groupez.dev/releases/fr/traqueur/commands/core/maven-metadata.xml" ).toURL(); + + SNAPSHOTS_URL = URI.create( + "https://repo.groupez.dev/snapshots/fr/traqueur/commands/core/maven-metadata.xml" + ).toURL(); } catch (MalformedURLException e) { - throw new UpdaterInitializationException("Failed to initialize updater URL", e); + throw new UpdaterInitializationException("Failed to initialize updater URLs", e); } } private Updater() {} public static void setLogger(Logger logger) { - Updater.LOGGER = logger; + LOGGER = logger; } + /* ------------------------------------------------------------ */ + /* Public API */ + /* ------------------------------------------------------------ */ + + /** + * Async update check (non-blocking) + */ public static void checkUpdates() { - try { - String latest = fetchLatestVersion(); + fetchLatestVersionAsync().thenAccept(latest -> { + if (latest == null) { + return; + } + String current = getVersion(); - if (latest != null && !latest.equals(current)) { + if (!current.equals(latest)) { LOGGER.warning("⚠ CommandsAPI is not up to date!"); LOGGER.warning("Current: " + current + " | Latest: " + latest); + } else { + LOGGER.info("✅ CommandsAPI is up to date (" + current + ")"); } - } catch (Exception ignored) { - } + }); + } + + /** + * Async latest version fetch + */ + public static CompletableFuture fetchLatestVersionAsync() { + return CompletableFuture.supplyAsync(() -> { + try { + URL metadataUrl = resolveMetadataUrl(); + if (metadataUrl == null) { + return null; + } + + HttpURLConnection connection = + (HttpURLConnection) metadataUrl.openConnection(); + + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + + try (InputStream is = connection.getInputStream()) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(is); + + // Priority: + NodeList release = document.getElementsByTagName("release"); + if (release.getLength() > 0) { + String value = release.item(0).getTextContent(); + if (!value.isEmpty()) { + return value; + } + } + + // Fallback: + NodeList latest = document.getElementsByTagName("latest"); + if (latest.getLength() > 0) { + String value = latest.item(0).getTextContent(); + if (!value.isEmpty()) { + return value; + } + } + + // Fallback: SNAPSHOT + NodeList versions = document.getElementsByTagName("version"); + String snapshot = null; + + for (int i = 0; i < versions.getLength(); i++) { + String v = versions.item(i).getTextContent(); + if (v.endsWith("-SNAPSHOT")) { + snapshot = v; + } + } + + return snapshot; + } + } catch (Exception e) { + LOGGER.warning("Failed to check updates: " + e.getMessage()); + return null; + } + }); } + /** + * Current version from properties + */ public static String getVersion() { Properties prop = new Properties(); try (InputStream is = Updater.class @@ -61,53 +144,64 @@ public static String getVersion() { .getResourceAsStream(VERSION_PROPERTY_FILE)) { if (is == null) { - throw new RuntimeException("commands.properties not found"); + throw new IllegalStateException("commands.properties not found"); } prop.load(is); return prop.getProperty("version"); - } catch (IOException e) { + } catch (Exception e) { throw new RuntimeException(e); } } - public static boolean isUpToDate() { - String latest = fetchLatestVersion(); - return latest != null && getVersion().equals(latest); + /* ------------------------------------------------------------ */ + /* Internal helpers */ + /* ------------------------------------------------------------ */ + + /** + * Resolve metadata URL once (releases -> snapshots) + */ + private static URL resolveMetadataUrl() { + if (RESOLVED_METADATA_URL != null) { + return RESOLVED_METADATA_URL; + } + + synchronized (Updater.class) { + if (RESOLVED_METADATA_URL != null) { + return RESOLVED_METADATA_URL; + } + + if (isValidMetadata(RELEASES_URL)) { + RESOLVED_METADATA_URL = RELEASES_URL; + LOGGER.info("Update source: releases"); + return RESOLVED_METADATA_URL; + } + + if (isValidMetadata(SNAPSHOTS_URL)) { + RESOLVED_METADATA_URL = SNAPSHOTS_URL; + LOGGER.info("Update source: snapshots"); + return RESOLVED_METADATA_URL; + } + + LOGGER.warning("No valid update source found (releases/snapshots)"); + return null; + } } /** - * Fetch latest version from Reposilite maven-metadata.xml + * Lightweight HEAD check */ - public static String fetchLatestVersion() { + private static boolean isValidMetadata(URL url) { try { - HttpURLConnection connection = - (HttpURLConnection) URL_LATEST_RELEASE.openConnection(); - - connection.setConnectTimeout(5000); - connection.setReadTimeout(5000); - connection.setRequestMethod("GET"); - - try (InputStream is = connection.getInputStream()) { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(is); - - // Priorité à , fallback - NodeList release = document.getElementsByTagName("release"); - if (release.getLength() > 0) { - return release.item(0).getTextContent(); - } + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("HEAD"); + connection.setConnectTimeout(3000); + connection.setReadTimeout(3000); - NodeList latest = document.getElementsByTagName("latest"); - if (latest.getLength() > 0) { - return latest.item(0).getTextContent(); - } - } + int code = connection.getResponseCode(); + return code >= 200 && code < 300; } catch (Exception e) { - LOGGER.warning("Failed to fetch latest version: " + e.getMessage()); + return false; } - - return null; } } diff --git a/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java b/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java index 39b2a8a..5c54739 100644 --- a/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; +import java.util.concurrent.CompletableFuture; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; @@ -34,55 +35,69 @@ void tearDown() { @Test void getVersion_readsFromCommandsProperties() { - // commands.properties in test/resources contains 'version=0.0.1' String version = Updater.getVersion(); assertNotNull(version); assertEquals("1.0.0", version); } @Test - void testIsUpToDate_withStaticMock_equalVersions() { + void checkUpdatesAsync_logsWarningWhenNotUpToDate() { try (MockedStatic mocks = mockStatic(Updater.class)) { - // stub real method to call through - mocks.when(Updater::isUpToDate).thenCallRealMethod(); + + mocks.when(Updater::checkUpdates).thenCallRealMethod(); mocks.when(Updater::getVersion).thenReturn("1.0.0"); - mocks.when(Updater::fetchLatestVersion).thenReturn("1.0.0"); + mocks.when(Updater::fetchLatestVersionAsync) + .thenReturn(CompletableFuture.completedFuture("2.0.0")); + + Updater.checkUpdates(); + + assertTrue(logHandler.anyMatch(Level.WARNING, + r -> r.getMessage().contains("not up to date") + )); - assertTrue(Updater.isUpToDate()); - mocks.verify(Updater::getVersion); - mocks.verify(Updater::fetchLatestVersion); + assertTrue(logHandler.anyMatch(Level.WARNING, + r -> r.getMessage().contains("Latest: 2.0.0") + )); } } @Test - void testIsUpToDate_withStaticMock_differentVersions() { + void checkUpdatesAsync_logsUpToDateWhenVersionsMatch() { try (MockedStatic mocks = mockStatic(Updater.class)) { - mocks.when(Updater::isUpToDate).thenCallRealMethod(); + + mocks.when(Updater::checkUpdates).thenCallRealMethod(); mocks.when(Updater::getVersion).thenReturn("1.0.0"); - mocks.when(Updater::fetchLatestVersion).thenReturn("2.0.0"); + mocks.when(Updater::fetchLatestVersionAsync) + .thenReturn(CompletableFuture.completedFuture("1.0.0")); - assertFalse(Updater.isUpToDate()); + Updater.checkUpdates(); + + assertTrue(logHandler.anyMatch(Level.INFO, + r -> r.getMessage().contains("up to date") + )); } } @Test - void testCheckUpdates_logsWarningWhenNotUpToDate() { + void checkUpdatesAsync_doesNothingWhenLatestIsNull() { try (MockedStatic mocks = mockStatic(Updater.class)) { + mocks.when(Updater::checkUpdates).thenCallRealMethod(); - mocks.when(Updater::getVersion).thenReturn("1.0.0"); - mocks.when(Updater::fetchLatestVersion).thenReturn("2.0.0"); + mocks.when(Updater::fetchLatestVersionAsync) + .thenReturn(CompletableFuture.completedFuture(null)); Updater.checkUpdates(); - assertTrue(logHandler.anyMatch(Level.WARNING, - rec -> rec.getMessage().contains("latest version is 2.0.0") - )); + + assertFalse(logHandler.anyMatch(Level.WARNING, r -> true)); } } - /** - * Captures log records for assertions - */ + /* ------------------------------------------------------------ */ + /* Test log handler */ + /* ------------------------------------------------------------ */ + private static class TestLogHandler extends Handler { + private final java.util.List records = new java.util.ArrayList<>(); @Override @@ -91,16 +106,14 @@ public void publish(LogRecord record) { } @Override - public void flush() { - } + public void flush() {} @Override - public void close() { - } + public void close() {} - boolean anyMatch(Level lvl, java.util.function.Predicate p) { + boolean anyMatch(Level level, java.util.function.Predicate predicate) { return records.stream() - .anyMatch(r -> r.getLevel().equals(lvl) && p.test(r)); + .anyMatch(r -> r.getLevel().equals(level) && predicate.test(r)); } } } From cd1c5284d840c154d80c83c058876291b0b426ff Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Wed, 31 Dec 2025 10:19:43 +0100 Subject: [PATCH 15/23] feat: remove useless job --- .github/workflows/build.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b4031e8..c0c1cd2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,34 +16,6 @@ concurrency: cancel-in-progress: true jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Make Gradle wrapper executable - run: chmod +x gradlew - - - name: Set up JDK 21 - uses: actions/setup-java@v3 - with: - distribution: temurin - java-version: 21 - - - name: Cache Gradle - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*','**/gradle-wrapper.properties') }} - restore-keys: ${{ runner.os }}-gradle- - - - name: Build & test - run: ./gradlew clean testAll --no-daemon - build: name: Build and Publish CommandsAPI uses: GroupeZ-dev/actions/.github/workflows/build.yml@main From 04a163858879923e6655d5ffa574624d394f484e Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Wed, 31 Dec 2025 12:18:11 +0100 Subject: [PATCH 16/23] feat: add annotation addon --- MIGRATION_v4_to_v5.md | 250 +++++++++++ annotations-addon/build.gradle | 3 + .../traqueur/commands/annotations/Alias.java | 39 ++ .../AnnotationCommandProcessor.java | 278 ++++++++++++ .../fr/traqueur/commands/annotations/Arg.java | 40 ++ .../commands/annotations/Command.java | 61 +++ .../annotations/CommandContainer.java | 38 ++ .../commands/annotations/Infinite.java | 39 ++ .../commands/annotations/Optional.java | 30 ++ .../commands/annotations/TabComplete.java | 58 +++ .../AnnotationCommandProcessorTest.java | 400 ++++++++++++++++++ .../annotations/MockCommandManager.java | 21 + .../commands/annotations/MockPlatform.java | 84 ++++ .../commands/annotations/MockPlayer.java | 5 + .../commands/annotations/MockSender.java | 6 + .../annotations/MockSenderResolver.java | 24 ++ .../commands/AliasTestCommands.java | 30 ++ .../commands/HierarchicalTestCommands.java | 39 ++ .../commands/InfiniteArgsTestCommands.java | 27 ++ .../commands/InvalidContainerMissingArg.java | 12 + .../InvalidContainerNoAnnotation.java | 12 + .../commands/OptionalArgsTestCommands.java | 27 ++ .../commands/OrphanTestCommands.java | 29 ++ .../commands/SimpleTestCommands.java | 34 ++ .../commands/TabCompleteTestCommands.java | 55 +++ build.gradle | 4 +- .../commands/api/models/CommandPlatform.java | 11 + .../commands/api/resolver/SenderResolver.java | 55 +++ .../fr/traqueur/commands/jda/JDAPlatform.java | 6 + .../commands/jda/JDASenderResolver.java | 63 +++ settings.gradle | 3 +- .../commands/spigot/SpigotPlatform.java | 6 + .../commands/spigot/SpigotSenderResolver.java | 54 +++ .../commands/velocity/VelocityPlatform.java | 6 + .../velocity/VelocitySenderResolver.java | 54 +++ 35 files changed, 1901 insertions(+), 2 deletions(-) create mode 100644 MIGRATION_v4_to_v5.md create mode 100644 annotations-addon/build.gradle create mode 100644 annotations-addon/src/main/java/fr/traqueur/commands/annotations/Alias.java create mode 100644 annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java create mode 100644 annotations-addon/src/main/java/fr/traqueur/commands/annotations/Arg.java create mode 100644 annotations-addon/src/main/java/fr/traqueur/commands/annotations/Command.java create mode 100644 annotations-addon/src/main/java/fr/traqueur/commands/annotations/CommandContainer.java create mode 100644 annotations-addon/src/main/java/fr/traqueur/commands/annotations/Infinite.java create mode 100644 annotations-addon/src/main/java/fr/traqueur/commands/annotations/Optional.java create mode 100644 annotations-addon/src/main/java/fr/traqueur/commands/annotations/TabComplete.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockCommandManager.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlatform.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlayer.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSender.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSenderResolver.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/AliasTestCommands.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/HierarchicalTestCommands.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InfiniteArgsTestCommands.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerMissingArg.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerNoAnnotation.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OptionalArgsTestCommands.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OrphanTestCommands.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/SimpleTestCommands.java create mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/TabCompleteTestCommands.java create mode 100644 core/src/main/java/fr/traqueur/commands/api/resolver/SenderResolver.java create mode 100644 jda/src/main/java/fr/traqueur/commands/jda/JDASenderResolver.java create mode 100644 spigot/src/main/java/fr/traqueur/commands/spigot/SpigotSenderResolver.java create mode 100644 velocity/src/main/java/fr/traqueur/commands/velocity/VelocitySenderResolver.java diff --git a/MIGRATION_v4_to_v5.md b/MIGRATION_v4_to_v5.md new file mode 100644 index 0000000..606cae9 --- /dev/null +++ b/MIGRATION_v4_to_v5.md @@ -0,0 +1,250 @@ +# Guide de Migration v4.x → v5.0.0 + +## 🚨 Breaking Changes + +### 1. Migration du Repository + +**JitPack supprimé** → Migration vers **repo.groupez.dev** + +```xml + + + jitpack.io + https://jitpack.io + + + com.github.Traqueur-dev.CommandsAPI + platform-spigot + 4.x.x + + + + + groupez-releases + https://repo.groupez.dev/releases + + + fr.traqueur.commands + platform-spigot + 5.0.0 + +``` + +```groovy +// AVANT v4.x +repositories { + maven { url 'https://jitpack.io' } +} +dependencies { + implementation 'com.github.Traqueur-dev.CommandsAPI:platform-spigot:4.x.x' +} + +// APRÈS v5.0.0 +repositories { + maven { url 'https://repo.groupez.dev/releases' } +} +dependencies { + implementation 'fr.traqueur.commands:platform-spigot:5.0.0' +} +``` + +--- + +## ✨ Nouvelles Fonctionnalités + +### 2. ArgumentParser - Parsing Typé + +Nouveau système de parsing avec gestion d'erreurs typée. + +**Interface:** +```java +public interface ArgumentParser { + ParseResult parse(Command command, C context); +} +``` + +**Implémentations fournies:** +- `DefaultArgumentParser` - Pour Spigot/Velocity (String[]) +- `JDAArgumentParser` - Pour JDA (SlashCommandInteractionEvent) + +**Types de résultats:** +```java +public record ParseResult(Arguments arguments, ParseError error, boolean success, int consumed) { + public static ParseResult success(Arguments args, int consumed) { ... } + public static ParseResult error(ParseError error) { ... } +} + +public record ParseError(Type type, String argument, String input, String message) { + public enum Type { + MISSING_REQUIRED, + TYPE_NOT_FOUND, + CONVERSION_FAILED, + ARGUMENT_TOO_LONG + } +} +``` + +--- + +### 3. CommandBuilder - API Fluent + +**Nouvelle façon de créer des commandes sans héritage:** + +```java +// Via le CommandManager +CommandManager manager = new CommandManager<>(platform); + +manager.command("hello") + .description("Commande hello") + .usage("/hello ") + .permission("myplugin.hello") + .arg("player", Player.class) + .executor((sender, args) -> { + Player target = args.get("player"); + sender.sendMessage("Hello " + target.getName()); + }) + .register(); +``` + +**API Complète:** +```java +CommandBuilder builder = manager.command("name") + .description("...") + .usage("...") + .permission("...") + .gameOnly() // Joueurs uniquement + .alias("alias1") + .aliases("alias2", "alias3") + .arg("name", String.class) // Argument requis + .arg("count", Integer.class, customCompleter) // Avec completer + .optionalArg("reason", String.class) // Argument optionnel + .requirement(new MyRequirement()) // Requirement custom + .subcommand(subCmd) // Subcommand + .executor((sender, args) -> { ... }) // Handler + .register(); // Enregistre +``` + +**Méthodes:** +- `.build()` - Construit sans enregistrer +- `.register()` - Construit et enregistre + +--- + +### 4. Cache pour PlayerArgument + +**Optimisation automatique** pour les arguments Player (Spigot). + +```java +public class PlayerArgument implements ArgumentConverter, TabCompleter { + private static final long CACHE_TTL_MS = 1000; // 1 seconde + + @Override + public List onCompletion(CommandSender sender, List args) { + // Cache automatique des noms de joueurs en ligne + // Rafraîchi toutes les secondes + } +} +``` + +**Avantages:** +- Réduit les appels à `Bukkit.getOnlinePlayers()` +- TTL de 1 seconde pour fraîcheur des données +- Thread-safe avec `volatile` + +--- + +### 5. Optimisations de Performance + +**CommandTree amélioré:** +- Validation stricte des labels (max 64 caractères par segment, max 10 niveaux) +- Recherche optimisée avec HashMap +- Gestion intelligente des subcommands + +**Pattern precompile:** +```java +private static final Pattern DOT_PATTERN = Pattern.compile("\\."); +// Utilisé pour split au lieu de String.split("\\.") +``` + +**Updater non-bloquant:** +```java +public static CompletableFuture fetchLatestVersionAsync() { + // Vérification async avec timeout de 5s +} +``` + +**Autres optimisations:** +- Cache pour les online players +- CommandInvoker simplifié +- Gestion optimisée des aliases + +--- + +## 📝 Exemples de Migration + +### Méthode Classique (Héritage) + +```java +// Fonctionne toujours en v5.0.0 +public class HelloCommand extends Command { + + public HelloCommand(MyPlugin plugin) { + super(plugin, "hello"); + setDescription("Say hello"); + addArg("player", Player.class); + } + + @Override + public void execute(CommandSender sender, Arguments args) { + Player target = args.get("player"); + sender.sendMessage("Hello " + target.getName()); + } +} + +// Enregistrement +manager.registerCommand(new HelloCommand(plugin)); +``` + +### Nouvelle Méthode (Builder) + +```java +// Nouveau en v5.0.0 +manager.command("hello") + .description("Say hello") + .arg("player", Player.class) + .executor((sender, args) -> { + Player target = args.get("player"); + sender.sendMessage("Hello " + target.getName()); + }) + .register(); +``` + +--- + +## ✅ Checklist de Migration + +- [ ] Changer repository : `jitpack.io` → `repo.groupez.dev/releases` +- [ ] Changer groupId : `com.github.Traqueur-dev.CommandsAPI` → `fr.traqueur.commands` +- [ ] Version : `5.0.0` +- [ ] (Optionnel) Tester le nouveau CommandBuilder pour simplifier le code +- [ ] Rebuild et tests + +--- + +## 🆕 Ce qui est Nouveau (non breaking) + +✅ **ArgumentParser** - System de parsing extensible +✅ **CommandBuilder** - Alternative fluent à l'héritage +✅ **Cache PlayerArgument** - Autocomplétion optimisée +✅ **ParseResult/ParseError** - Gestion d'erreurs typée +✅ **Updater async** - Vérification non-bloquante +✅ **Tests** - Coverage améliorée + +## 🔄 Ce qui Reste Compatible + +✅ Création de commandes par héritage de `Command` +✅ API `Arguments` (get, getOptional, has, add) +✅ `TabCompleter` interface +✅ `Requirement` system +✅ Converters customs +✅ Subcommands \ No newline at end of file diff --git a/annotations-addon/build.gradle b/annotations-addon/build.gradle new file mode 100644 index 0000000..dfe746b --- /dev/null +++ b/annotations-addon/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':core') +} \ No newline at end of file diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Alias.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Alias.java new file mode 100644 index 0000000..6e9b321 --- /dev/null +++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Alias.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; + +/** + * Defines aliases for a {@link Command} or {@link Subcommand}. + * + *

Example:

+ *
{@code
+ * @Command(name = "gamemode", permission = "admin.gamemode")
+ * @Alias({"gm"})
+ * public void gamemode(Player sender, @Arg("mode") GameMode mode) {
+ *     sender.setGameMode(mode);
+ * }
+ * 
+ * @Command(name = "heal")
+ * @Alias({"h", "soin", "vida"})
+ * public void heal(Player sender) {
+ *     sender.setHealth(20);
+ * }
+ * }
+ * + * @since 5.0.0 + * @see Command + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Alias { + + /** + * The aliases for the command. + * + * @return array of alias names + */ + String[] value(); +} \ No newline at end of file diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java new file mode 100644 index 0000000..316da46 --- /dev/null +++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java @@ -0,0 +1,278 @@ +package fr.traqueur.commands.annotations; + +import fr.traqueur.commands.api.CommandManager; +import fr.traqueur.commands.api.arguments.Arguments; +import fr.traqueur.commands.api.arguments.TabCompleter; +import fr.traqueur.commands.api.models.Command; +import fr.traqueur.commands.api.models.CommandBuilder; +import fr.traqueur.commands.api.resolver.SenderResolver; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; + +/** + * Processes annotated command containers and registers them with the CommandManager. + * + * @param plugin type + * @param sender type + * @since 5.0.0 + */ +public class AnnotationCommandProcessor { + + private final CommandManager manager; + private final SenderResolver senderResolver; + private final Map tabCompleters = new HashMap<>(); + + public AnnotationCommandProcessor(CommandManager manager) { + this.manager = manager; + this.senderResolver = manager.getPlatform().getSenderResolver(); + } + + public void register(Object... handlers) { + for (Object handler : handlers) { + processHandler(handler); + } + } + + private void processHandler(Object handler) { + Class clazz = handler.getClass(); + + if (!clazz.isAnnotationPresent(CommandContainer.class)) { + throw new IllegalArgumentException( + "Class must be annotated with @CommandContainer: " + clazz.getName() + ); + } + + // First pass: collect all @TabComplete methods + tabCompleters.clear(); + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(TabComplete.class)) { + processTabCompleter(handler, method); + } + } + + // Second pass: collect all @Command methods and sort by depth + List commandMethods = new ArrayList<>(); + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(fr.traqueur.commands.annotations.Command.class)) { + fr.traqueur.commands.annotations.Command annotation = + method.getAnnotation(fr.traqueur.commands.annotations.Command.class); + commandMethods.add(new CommandMethodInfo(handler, method, annotation.name())); + } + } + + // Sort by depth (parents first) + commandMethods.sort(Comparator.comparingInt(info -> info.name.split("\\.").length)); + + // Collect all command paths to determine which have parents defined + Set allPaths = new HashSet<>(); + for (CommandMethodInfo info : commandMethods) { + allPaths.add(info.name); + } + + // Third pass: build ALL commands first + // Key: fullPath, Value: command + Map> builtCommands = new LinkedHashMap<>(); + Set rootCommands = new LinkedHashSet<>(); + + for (CommandMethodInfo info : commandMethods) { + String parentPath = getParentPath(info.name); + boolean hasParentInBatch = parentPath != null && allPaths.contains(parentPath); + + // Build command with appropriate name + // - If parent exists in batch: use short name (will be added via addSubCommand) + // - If no parent in batch (orphan): use full path (core will create parents) + Command command = buildCommand(info.handler, info.method, info.name, hasParentInBatch); + builtCommands.put(info.name, command); + } + + // Fourth pass: organize hierarchy (add subcommands to parents) + for (CommandMethodInfo info : commandMethods) { + String parentPath = getParentPath(info.name); + + if (parentPath != null && allPaths.contains(parentPath)) { + // Parent exists in our batch -> add as subcommand + Command parent = builtCommands.get(parentPath); + Command child = builtCommands.get(info.name); + parent.addSubCommand(child); + } else { + // No parent in batch -> this is a root command (or orphan) + rootCommands.add(info.name); + } + } + + // Fifth pass: register only root commands (manager handles subcommands) + for (String rootPath : rootCommands) { + Command rootCommand = builtCommands.get(rootPath); + manager.registerCommand(rootCommand); + } + } + + private String getParentPath(String path) { + int lastDot = path.lastIndexOf('.'); + if (lastDot == -1) { + return null; + } + return path.substring(0, lastDot); + } + + private String getCommandName(String path) { + int lastDot = path.lastIndexOf('.'); + if (lastDot == -1) { + return path; + } + return path.substring(lastDot + 1); + } + + private Command buildCommand(Object handler, Method method, String fullPath, boolean hasParentInBatch) { + fr.traqueur.commands.annotations.Command annotation = + method.getAnnotation(fr.traqueur.commands.annotations.Command.class); + + // If parent exists in batch: use short name (will be added via addSubCommand) + // If no parent (orphan/root): use full path (core will create parent hierarchy) + String commandName = hasParentInBatch ? getCommandName(fullPath) : fullPath; + + CommandBuilder builder = manager.command(commandName) + .description(annotation.description()) + .permission(annotation.permission()) + .usage(annotation.usage()); + + if (method.isAnnotationPresent(Alias.class)) { + Alias aliasAnnotation = method.getAnnotation(Alias.class); + builder.aliases(aliasAnnotation.value()); + } + + processParameters(builder, method, fullPath); + + Parameter[] params = method.getParameters(); + if (params.length > 0 && senderResolver.isGameOnly(params[0].getType())) { + builder.gameOnly(); + } + + method.setAccessible(true); + builder.executor((sender, args) -> invokeMethod(handler, method, sender, args)); + + return builder.build(); + } + + private void processTabCompleter(Object handler, Method method) { + TabComplete annotation = method.getAnnotation(TabComplete.class); + String key = annotation.command() + ":" + annotation.arg(); + + method.setAccessible(true); + tabCompleters.put(key, new TabCompleterMethod(handler, method)); + } + + private void processParameters(CommandBuilder builder, Method method, String commandPath) { + Parameter[] params = method.getParameters(); + + for (int i = 0; i < params.length; i++) { + Parameter param = params[i]; + + if (i == 0 && senderResolver.canResolve(param.getType())) { + continue; + } + + Arg argAnnotation = param.getAnnotation(Arg.class); + if (argAnnotation == null) { + throw new IllegalArgumentException( + "Parameter '" + param.getName() + "' in method '" + method.getName() + + "' must be annotated with @Arg or be the sender type" + ); + } + + String argName = argAnnotation.value(); + Class argType = param.getType(); + boolean isOptional = param.isAnnotationPresent(Optional.class); + boolean isInfinite = param.isAnnotationPresent(fr.traqueur.commands.annotations.Infinite.class); + + if (isInfinite) { + argType = fr.traqueur.commands.api.arguments.Infinite.class; + } + + TabCompleter completer = getTabCompleter(commandPath, argName); + + if (isOptional) { + if (completer != null) { + builder.optionalArg(argName, argType, completer); + } else { + builder.optionalArg(argName, argType); + } + } else { + if (completer != null) { + builder.arg(argName, argType, completer); + } else { + builder.arg(argName, argType); + } + } + } + } + + @SuppressWarnings("unchecked") + private TabCompleter getTabCompleter(String commandPath, String argName) { + String key = commandPath + ":" + argName; + TabCompleterMethod tcMethod = tabCompleters.get(key); + + if (tcMethod == null) { + return null; + } + + return (sender, args) -> { + try { + Object result; + Parameter[] params = tcMethod.method.getParameters(); + + if (params.length == 0) { + result = tcMethod.method.invoke(tcMethod.handler); + } else if (params.length == 1) { + Object resolvedSender = senderResolver.resolve(sender, params[0].getType()); + result = tcMethod.method.invoke(tcMethod.handler, resolvedSender); + } else { + Object resolvedSender = senderResolver.resolve(sender, params[0].getType()); + String current = !args.isEmpty() ? args.getLast() : ""; + result = tcMethod.method.invoke(tcMethod.handler, resolvedSender, current); + } + + return (List) 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]; + + if (i == 0 && senderResolver.canResolve(param.getType())) { + invokeArgs[i] = senderResolver.resolve(sender, param.getType()); + continue; + } + + Arg argAnnotation = param.getAnnotation(Arg.class); + if (argAnnotation != null) { + String argName = argAnnotation.value(); + + if (param.isAnnotationPresent(Optional.class)) { + invokeArgs[i] = args.getOptional(argName).orElse(null); + } 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..ba589e4 --- /dev/null +++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Arg.java @@ -0,0 +1,40 @@ +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 Optional + * @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..ad63b30 --- /dev/null +++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Infinite.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 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 + * @see Optional + */ +@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/Optional.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Optional.java new file mode 100644 index 0000000..2a37431 --- /dev/null +++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Optional.java @@ -0,0 +1,30 @@ +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 optional. + * + *

Optional parameters will be null if not provided by the user. + * Use wrapper types (Integer, Boolean, etc.) instead of primitives + * to allow null values.

+ * + *

Example:

+ *
{@code
+ * @Command(name = "heal")
+ * public void heal(Player sender, @Arg("target") @Optional Player target) {
+ *     Player toHeal = (target != null) ? target : sender;
+ *     toHeal.setHealth(20);
+ * }
+ * }
+ * + * @since 5.0.0 + * @see Arg + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Optional { +} \ 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..69a33a1 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java @@ -0,0 +1,400 @@ +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 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().get(0).name()); + + assertEquals(1, give.getOptionalArgs().size()); + assertEquals("amount", give.getOptionalArgs().get(0).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/MockCommandManager.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockCommandManager.java new file mode 100644 index 0000000..b96f822 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockCommandManager.java @@ -0,0 +1,21 @@ +package fr.traqueur.commands.annotations; + +import fr.traqueur.commands.api.CommandManager; + +public class MockCommandManager extends CommandManager { + + private final MockPlatform mockPlatform; + + public MockCommandManager() { + this(new MockPlatform()); + } + + public MockCommandManager(MockPlatform platform) { + super(platform); + this.mockPlatform = platform; + } + + public MockPlatform getMockPlatform() { + return mockPlatform; + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlatform.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlatform.java new file mode 100644 index 0000000..6f6259e --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlatform.java @@ -0,0 +1,84 @@ +package fr.traqueur.commands.annotations; + +import fr.traqueur.commands.api.CommandManager; +import fr.traqueur.commands.api.models.Command; +import fr.traqueur.commands.api.models.CommandPlatform; +import fr.traqueur.commands.api.resolver.SenderResolver; + +import java.util.*; +import java.util.logging.Logger; + +public class MockPlatform implements CommandPlatform { + + private final Object plugin = new Object(); + private final Logger logger = Logger.getLogger("MockPlatform"); + private final MockSenderResolver senderResolver = new MockSenderResolver(); + private final Map> registeredCommands = new HashMap<>(); + private final List registeredLabels = new ArrayList<>(); + + private CommandManager commandManager; + + @Override + public Object getPlugin() { + return plugin; + } + + @Override + public void injectManager(CommandManager commandManager) { + this.commandManager = commandManager; + } + + @Override + public Logger getLogger() { + return logger; + } + + @Override + public boolean hasPermission(MockSender sender, String permission) { + return sender.hasPermission(permission); + } + + @Override + public boolean isPlayer(MockSender sender) { + return sender instanceof MockPlayer; + } + + @Override + public void sendMessage(MockSender sender, String message) { + sender.sendMessage(message); + } + + @Override + public void addCommand(Command command, String label) { + registeredCommands.put(label, command); + registeredLabels.add(label); + } + + @Override + public void removeCommand(String label, boolean subcommand) { + registeredCommands.remove(label); + registeredLabels.remove(label); + } + + @Override + public SenderResolver getSenderResolver() { + return senderResolver; + } + + // Test helpers + public Map> getRegisteredCommands() { + return registeredCommands; + } + + public List getRegisteredLabels() { + return registeredLabels; + } + + public boolean hasCommand(String label) { + return registeredCommands.containsKey(label); + } + + public Command getCommand(String label) { + return registeredCommands.get(label); + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlayer.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlayer.java new file mode 100644 index 0000000..51a04e3 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlayer.java @@ -0,0 +1,5 @@ +package fr.traqueur.commands.annotations; + +public interface MockPlayer extends MockSender { + String getName(); +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSender.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSender.java new file mode 100644 index 0000000..3656571 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSender.java @@ -0,0 +1,6 @@ +package fr.traqueur.commands.annotations; + +public interface MockSender { + void sendMessage(String message); + boolean hasPermission(String permission); +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSenderResolver.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSenderResolver.java new file mode 100644 index 0000000..a60cf8b --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSenderResolver.java @@ -0,0 +1,24 @@ +package fr.traqueur.commands.annotations; + +import fr.traqueur.commands.api.resolver.SenderResolver; + +public class MockSenderResolver implements SenderResolver { + + @Override + public boolean canResolve(Class type) { + return MockSender.class.isAssignableFrom(type) || MockPlayer.class.isAssignableFrom(type); + } + + @Override + public Object resolve(MockSender sender, Class type) { + if (type.isInstance(sender)) { + return type.cast(sender); + } + return null; + } + + @Override + public boolean isGameOnly(Class type) { + return MockPlayer.class.isAssignableFrom(type); + } +} \ 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..5ac3805 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/AliasTestCommands.java @@ -0,0 +1,30 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.*; + +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..fd45c92 --- /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.annotations.MockSender; + +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..eb61371 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InfiniteArgsTestCommands.java @@ -0,0 +1,27 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.*; + +import java.util.ArrayList; +import java.util.List; + +@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") @Optional @Infinite String reason) { + executedCommands.add("kick"); + executedArgs.add(new Object[]{sender, player, reason}); + } +} \ 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..20e36a1 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerMissingArg.java @@ -0,0 +1,12 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.*; + +@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..af2cf9b --- /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.annotations.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..6fde282 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OptionalArgsTestCommands.java @@ -0,0 +1,27 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.*; + +import java.util.ArrayList; +import java.util.List; + +@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 MockPlayer target) { + executedCommands.add("heal"); + executedArgs.add(new Object[]{sender, target}); + } + + @Command(name = "give", description = "Give items") + public void give(MockPlayer sender, + @Arg("item") String item, + @Arg("amount") @Optional Integer amount) { + executedCommands.add("give"); + executedArgs.add(new Object[]{sender, item, amount}); + } +} \ 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..4fbfa1f --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OrphanTestCommands.java @@ -0,0 +1,29 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.*; + +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..6b6ae53 --- /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.annotations.MockSender; + +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..744edf6 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/TabCompleteTestCommands.java @@ -0,0 +1,55 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.*; + +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 ace48d7..d2e5a4b 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ subprojects { testLogging { showStandardStreams = true events("passed", "skipped", "failed", "standardOut", "standardError") - exceptionFormat "full" + exceptionFormat = "full" } } @@ -101,6 +101,8 @@ subprojects { 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}" diff --git a/core/src/main/java/fr/traqueur/commands/api/models/CommandPlatform.java b/core/src/main/java/fr/traqueur/commands/api/models/CommandPlatform.java index 49a53e2..2197806 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/CommandPlatform.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/CommandPlatform.java @@ -1,6 +1,7 @@ package fr.traqueur.commands.api.models; import fr.traqueur.commands.api.CommandManager; +import fr.traqueur.commands.api.resolver.SenderResolver; import java.util.logging.Logger; @@ -74,4 +75,14 @@ public interface CommandPlatform { * @param subcommand true if the command is a subcommand, false otherwise. */ void removeCommand(String label, boolean subcommand); + + /** + * Gets the sender resolver for this platform. + *

Used by the annotations-addon to resolve method parameter types.

+ * + * @return The sender resolver for this platform. + * @since 5.0.0 + */ + SenderResolver getSenderResolver(); + } diff --git a/core/src/main/java/fr/traqueur/commands/api/resolver/SenderResolver.java b/core/src/main/java/fr/traqueur/commands/api/resolver/SenderResolver.java new file mode 100644 index 0000000..10b9de1 --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/resolver/SenderResolver.java @@ -0,0 +1,55 @@ +package fr.traqueur.commands.api.resolver; + +/** + * Resolves sender types for annotated commands. + * + *

Each platform (Bukkit, JDA, etc.) provides its own implementation + * to handle sender type resolution from method parameters.

+ * + *

Example for Bukkit:

+ *
    + *
  • {@code CommandSender} → the raw sender
  • + *
  • {@code Player} → cast to Player (gameOnly = true)
  • + *
  • {@code ConsoleCommandSender} → cast to Console
  • + *
+ * + *

Example for JDA:

+ *
    + *
  • {@code SlashCommandInteractionEvent} → the raw event
  • + *
  • {@code User} → event.getUser()
  • + *
  • {@code Member} → event.getMember() (gameOnly = true, requires guild)
  • + *
+ * + * @param the base sender type for the platform + * @since 5.0.0 + */ +public interface SenderResolver { + + /** + * Checks if this resolver can handle the given parameter type. + * + * @param type the parameter type from the method signature + * @return true if this resolver can resolve the type + */ + boolean canResolve(Class type); + + /** + * Resolves the sender to the requested type. + * + * @param sender the original sender from the command execution + * @param type the requested type from the method parameter + * @return the resolved object, or null if resolution fails + */ + Object resolve(S sender, Class type); + + /** + * Checks if the given type requires a "game" context. + * + *

For Bukkit, this means the sender must be a Player. + * For JDA, this means the command must be executed in a guild (Member).

+ * + * @param type the parameter type + * @return true if this type requires game-only context + */ + boolean isGameOnly(Class type); +} \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java index 34c6156..a2ab1ce 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java @@ -4,6 +4,7 @@ import fr.traqueur.commands.api.arguments.Argument; import fr.traqueur.commands.api.models.Command; import fr.traqueur.commands.api.models.CommandPlatform; +import fr.traqueur.commands.api.resolver.SenderResolver; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -188,6 +189,11 @@ public void removeCommand(String label, boolean subcommand) { } } + @Override + public SenderResolver getSenderResolver() { + return new JDASenderResolver(); + } + /** * Add arguments to a slash command. * diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDASenderResolver.java b/jda/src/main/java/fr/traqueur/commands/jda/JDASenderResolver.java new file mode 100644 index 0000000..971588a --- /dev/null +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDASenderResolver.java @@ -0,0 +1,63 @@ +package fr.traqueur.commands.jda; + +import fr.traqueur.commands.api.resolver.SenderResolver; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +/** + * Sender resolver for the JDA (Discord) platform. + * + *

Resolves method parameter types to appropriate objects:

+ *
    + *
  • {@link SlashCommandInteractionEvent} → the raw event
  • + *
  • {@link User} → event.getUser()
  • + *
  • {@link Member} → event.getMember() (requires guild, gameOnly = true)
  • + *
  • {@link MessageChannelUnion} → event.getChannel()
  • + *
+ * + * @since 5.0.0 + */ +public class JDASenderResolver implements SenderResolver { + + /** + * {@inheritDoc} + */ + @Override + public boolean canResolve(Class type) { + return SlashCommandInteractionEvent.class.isAssignableFrom(type) + || User.class.isAssignableFrom(type) + || Member.class.isAssignableFrom(type) + || MessageChannelUnion.class.isAssignableFrom(type); + } + + /** + * {@inheritDoc} + */ + @Override + public Object resolve(SlashCommandInteractionEvent event, Class type) { + if (SlashCommandInteractionEvent.class.isAssignableFrom(type)) { + return event; + } + if (User.class.isAssignableFrom(type)) { + return event.getUser(); + } + if (Member.class.isAssignableFrom(type)) { + return event.getMember(); // null if not in guild + } + if (MessageChannelUnion.class.isAssignableFrom(type)) { + return event.getChannel(); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isGameOnly(Class type) { + // Member requires guild context (like Player requires game context) + return Member.class.isAssignableFrom(type); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 2d7fb13..749e16c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,4 +6,5 @@ include 'spigot' include 'core' include 'velocity' include 'jda' -include 'jda-test-bot' \ No newline at end of file +include 'jda-test-bot' +include 'annotations-addon' \ No newline at end of file diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotPlatform.java b/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotPlatform.java index 83ffb37..b8e3598 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotPlatform.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotPlatform.java @@ -4,6 +4,7 @@ import fr.traqueur.commands.api.exceptions.CommandRegistrationException; import fr.traqueur.commands.api.models.Command; import fr.traqueur.commands.api.models.CommandPlatform; +import fr.traqueur.commands.api.resolver.SenderResolver; import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.command.CommandMap; @@ -176,4 +177,9 @@ public void removeCommand(String label, boolean subcommand) { Objects.requireNonNull(this.commandMap.getCommand(label)).unregister(commandMap); } } + + @Override + public SenderResolver getSenderResolver() { + return new SpigotSenderResolver(); + } } diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotSenderResolver.java b/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotSenderResolver.java new file mode 100644 index 0000000..9116a41 --- /dev/null +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/SpigotSenderResolver.java @@ -0,0 +1,54 @@ +package fr.traqueur.commands.spigot; + +import fr.traqueur.commands.api.resolver.SenderResolver; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; + +/** + * Sender resolver for the Bukkit/Spigot platform. + * + *

Resolves method parameter types to appropriate sender objects:

+ *
    + *
  • {@link CommandSender} → the raw sender (any)
  • + *
  • {@link Player} → cast to Player (requires gameOnly)
  • + *
  • {@link ConsoleCommandSender} → cast to Console
  • + *
+ * + * @since 5.0.0 + */ +public class SpigotSenderResolver implements SenderResolver { + + /** + * Creates a new Bukkit sender resolver. + */ + public SpigotSenderResolver() { + } + + /** + * {@inheritDoc} + */ + @Override + public boolean canResolve(Class type) { + return CommandSender.class.isAssignableFrom(type); + } + + /** + * {@inheritDoc} + */ + @Override + public Object resolve(CommandSender sender, Class type) { + if (type.isInstance(sender)) { + return type.cast(sender); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isGameOnly(Class type) { + return Player.class.isAssignableFrom(type); + } +} \ No newline at end of file diff --git a/velocity/src/main/java/fr/traqueur/commands/velocity/VelocityPlatform.java b/velocity/src/main/java/fr/traqueur/commands/velocity/VelocityPlatform.java index 34b7839..3e37df7 100644 --- a/velocity/src/main/java/fr/traqueur/commands/velocity/VelocityPlatform.java +++ b/velocity/src/main/java/fr/traqueur/commands/velocity/VelocityPlatform.java @@ -5,6 +5,7 @@ import fr.traqueur.commands.api.CommandManager; import fr.traqueur.commands.api.models.Command; import fr.traqueur.commands.api.models.CommandPlatform; +import fr.traqueur.commands.api.resolver.SenderResolver; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -154,6 +155,11 @@ public void removeCommand(String label, boolean subcommand) { } } + @Override + public SenderResolver getSenderResolver() { + return new VelocitySenderResolver(); + } + /** * Parses a message from legacy format to Adventure format. * diff --git a/velocity/src/main/java/fr/traqueur/commands/velocity/VelocitySenderResolver.java b/velocity/src/main/java/fr/traqueur/commands/velocity/VelocitySenderResolver.java new file mode 100644 index 0000000..f764c7f --- /dev/null +++ b/velocity/src/main/java/fr/traqueur/commands/velocity/VelocitySenderResolver.java @@ -0,0 +1,54 @@ +package fr.traqueur.commands.velocity; + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.ConsoleCommandSource; +import com.velocitypowered.api.proxy.Player; +import fr.traqueur.commands.api.resolver.SenderResolver; + +/** + * Sender resolver for the Velocity platform. + * + *

Resolves method parameter types to appropriate sender objects:

+ *
    + *
  • {@link CommandSource} → the raw sender (any)
  • + *
  • {@link Player} → cast to Player (requires gameOnly)
  • + *
  • {@link ConsoleCommandSource} → cast to Console
  • + *
+ * + * @since 5.0.0 + */ +public class VelocitySenderResolver implements SenderResolver { + + /** + * Creates a new Velocity sender resolver. + */ + public VelocitySenderResolver() { + } + + /** + * {@inheritDoc} + */ + @Override + public boolean canResolve(Class type) { + return CommandSource.class.isAssignableFrom(type); + } + + /** + * {@inheritDoc} + */ + @Override + public Object resolve(CommandSource sender, Class type) { + if (type.isInstance(sender)) { + return type.cast(sender); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isGameOnly(Class type) { + return Player.class.isAssignableFrom(type); + } +} \ No newline at end of file From b86ff36af51368ba344e02e26ae55618e185150f Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Wed, 31 Dec 2025 12:53:40 +0100 Subject: [PATCH 17/23] feat: rework tests --- MIGRATION_v4_to_v5.md | 175 +++++++++++++++++- annotations-addon/build.gradle | 1 + .../AnnotationCommandProcessorTest.java | 1 + .../commands/annotations/MockPlayer.java | 5 - .../commands/annotations/MockSender.java | 6 - .../commands/AliasTestCommands.java | 1 + .../commands/HierarchicalTestCommands.java | 2 +- .../commands/InfiniteArgsTestCommands.java | 1 + .../commands/InvalidContainerMissingArg.java | 1 + .../InvalidContainerNoAnnotation.java | 2 +- .../commands/OptionalArgsTestCommands.java | 1 + .../commands/OrphanTestCommands.java | 1 + .../commands/SimpleTestCommands.java | 2 +- .../commands/TabCompleteTestCommands.java | 1 + .../commands/api/CommandManagerTest.java | 87 +++------ .../api/models/CommandBuilderTest.java | 123 ++++-------- .../commands/api/models/CommandTest.java | 144 ++------------ .../test/mocks}/MockCommandManager.java | 8 +- .../commands/test/mocks}/MockPlatform.java | 10 +- .../commands/test/mocks/MockPlayer.java | 9 + .../commands/test/mocks/MockSender.java | 10 + .../test/mocks}/MockSenderResolver.java | 7 +- 22 files changed, 294 insertions(+), 304 deletions(-) delete mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlayer.java delete mode 100644 annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSender.java rename {annotations-addon/src/test/java/fr/traqueur/commands/annotations => core/src/test/java/fr/traqueur/commands/test/mocks}/MockCommandManager.java (71%) rename {annotations-addon/src/test/java/fr/traqueur/commands/annotations => core/src/test/java/fr/traqueur/commands/test/mocks}/MockPlatform.java (93%) create mode 100644 core/src/test/java/fr/traqueur/commands/test/mocks/MockPlayer.java create mode 100644 core/src/test/java/fr/traqueur/commands/test/mocks/MockSender.java rename {annotations-addon/src/test/java/fr/traqueur/commands/annotations => core/src/test/java/fr/traqueur/commands/test/mocks}/MockSenderResolver.java (86%) diff --git a/MIGRATION_v4_to_v5.md b/MIGRATION_v4_to_v5.md index 606cae9..dcca08d 100644 --- a/MIGRATION_v4_to_v5.md +++ b/MIGRATION_v4_to_v5.md @@ -52,7 +52,89 @@ dependencies { ## ✨ Nouvelles Fonctionnalités -### 2. ArgumentParser - Parsing Typé +### 2. Annotations Addon - Commandes par Annotations + +**Nouveau module pour créer des commandes via annotations** (alternative à l'héritage et au builder). + +**Installation:** +```xml + + fr.traqueur.commands + annotations-addon + 5.0.0 + +``` + +```groovy +dependencies { + implementation 'fr.traqueur.commands:annotations-addon:5.0.0' +} +``` + +**Exemple Simple:** +```java +@CommandContainer +public class MyCommands { + + @Command(name = "hello", description = "Say hello") + public void helloCommand(CommandSender sender, @Arg("player") Player target) { + sender.sendMessage("Hello " + target.getName()); + } + + @TabComplete(command = "hello", arg = "player") + public List completePlayers(CommandSender sender) { + return Bukkit.getOnlinePlayers().stream() + .map(Player::getName) + .toList(); + } +} + +// Enregistrement +AnnotationCommandProcessor processor = + new AnnotationCommandProcessor<>(manager); +processor.register(new MyCommands()); +``` + +**Fonctionnalités:** +- `@CommandContainer` - Marque une classe contenant des commandes +- `@Command(name, description, permission, usage)` - Définit une commande +- `@Arg("name")` - Marque un paramètre d'argument +- `@Optional` - Marque un argument comme optionnel +- `@Infinite` - Argument infini (prend tout le reste) +- `@Alias("alias1", "alias2")` - Définit des alias +- `@TabComplete(command, arg)` - Définit l'autocomplétion + +**Commandes Hiérarchiques:** +```java +@Command(name = "admin") +public void admin(CommandSender sender) { + sender.sendMessage("Admin menu"); +} + +@Command(name = "admin.kick", description = "Kick a player") +public void adminKick(CommandSender sender, + @Arg("player") Player target, + @Arg("reason") @Optional String reason) { + // Accessible via /admin kick [reason] +} +``` + +**Types de Sender Automatiques:** +```java +// Accepte n'importe quel sender +@Command(name = "broadcast") +public void broadcast(CommandSender sender, @Arg("message") String msg) { } + +// Joueurs uniquement (auto gameOnly) +@Command(name = "heal") +public void heal(Player player) { + player.setHealth(20.0); +} +``` + +--- + +### 3. ArgumentParser - Parsing Typé Nouveau système de parsing avec gestion d'erreurs typée. @@ -86,7 +168,7 @@ public record ParseError(Type type, String argument, String input, String messag --- -### 3. CommandBuilder - API Fluent +### 4. CommandBuilder - API Fluent **Nouvelle façon de créer des commandes sans héritage:** @@ -130,7 +212,7 @@ CommandBuilder builder = manager.command("name") --- -### 4. Cache pour PlayerArgument +### 5. Cache pour PlayerArgument **Optimisation automatique** pour les arguments Player (Spigot). @@ -153,7 +235,7 @@ public class PlayerArgument implements ArgumentConverter, TabCompleter { + public HelloCommand(MyPlugin plugin) { + super(plugin, "hello"); + addArg("player", Player.class); + } + + @Override + public void execute(CommandSender sender, Arguments args) { + Player target = args.get("player"); + sender.sendMessage("Hello " + target.getName()); + } +} +manager.registerCommand(new HelloCommand(plugin)); +``` + +### 2. Builder (Nouveau v5) +```java +manager.command("hello") + .arg("player", Player.class) + .executor((sender, args) -> { + Player target = args.get("player"); + sender.sendMessage("Hello " + target.getName()); + }) + .register(); +``` + +### 3. Annotations (Nouveau v5 - Addon) +```java +@CommandContainer +public class Commands { + @Command(name = "hello") + public void hello(CommandSender sender, @Arg("player") Player target) { + sender.sendMessage("Hello " + target.getName()); + } +} +new AnnotationCommandProcessor<>(manager).register(new Commands()); +``` + +--- + +## 🧪 Tests - Mocks Partagés + +**Nouveau en v5.0.0:** Mocks réutilisables dans `core` pour faciliter les tests. + +```java +import fr.traqueur.commands.test.mocks.*; + +// Dans vos tests +MockCommandManager manager = new MockCommandManager(); +MockPlatform platform = manager.getMockPlatform(); + +// Créer un mock sender +MockSender sender = new MockSender() { + @Override + public void sendMessage(String message) { /* ... */ } + + @Override + public boolean hasPermission(String permission) { return true; } +}; + +// Enregistrer et tester +manager.command("test").executor((s, args) -> { + s.sendMessage("Test"); +}).register(); + +assertTrue(platform.hasCommand("test")); +``` + +**Avantages:** +- Pas besoin de Mockito pour les tests simples +- Mocks cohérents entre modules +- Simplifie les tests platform-agnostic \ No newline at end of file diff --git a/annotations-addon/build.gradle b/annotations-addon/build.gradle index dfe746b..af944bc 100644 --- a/annotations-addon/build.gradle +++ b/annotations-addon/build.gradle @@ -1,3 +1,4 @@ dependencies { api project(':core') + testImplementation project(':core').sourceSets.test.output } \ 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 index 69a33a1..866b90a 100644 --- a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java @@ -3,6 +3,7 @@ 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; diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlayer.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlayer.java deleted file mode 100644 index 51a04e3..0000000 --- a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlayer.java +++ /dev/null @@ -1,5 +0,0 @@ -package fr.traqueur.commands.annotations; - -public interface MockPlayer extends MockSender { - String getName(); -} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSender.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSender.java deleted file mode 100644 index 3656571..0000000 --- a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSender.java +++ /dev/null @@ -1,6 +0,0 @@ -package fr.traqueur.commands.annotations; - -public interface MockSender { - void sendMessage(String message); - boolean hasPermission(String permission); -} \ 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 index 5ac3805..890dd0d 100644 --- 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 @@ -1,6 +1,7 @@ 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; 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 index fd45c92..86f9630 100644 --- 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 @@ -2,7 +2,7 @@ import fr.traqueur.commands.annotations.Command; import fr.traqueur.commands.annotations.CommandContainer; -import fr.traqueur.commands.annotations.MockSender; +import fr.traqueur.commands.test.mocks.*; import java.util.ArrayList; import java.util.List; 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 index eb61371..b65b7bb 100644 --- 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 @@ -1,6 +1,7 @@ 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; 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 index 20e36a1..de44b04 100644 --- 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 @@ -1,6 +1,7 @@ package fr.traqueur.commands.annotations.commands; import fr.traqueur.commands.annotations.*; +import fr.traqueur.commands.test.mocks.*; @CommandContainer public class InvalidContainerMissingArg { 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 index af2cf9b..305981d 100644 --- 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 @@ -1,7 +1,7 @@ package fr.traqueur.commands.annotations.commands; import fr.traqueur.commands.annotations.Command; -import fr.traqueur.commands.annotations.MockSender; +import fr.traqueur.commands.test.mocks.MockSender; // Missing @CommandContainer - should throw error public class InvalidContainerNoAnnotation { 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 index 6fde282..8cf7edc 100644 --- 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 @@ -1,6 +1,7 @@ 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; 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 index 4fbfa1f..43f632d 100644 --- 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 @@ -1,6 +1,7 @@ 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; 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 index 6b6ae53..2786169 100644 --- 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 @@ -3,7 +3,7 @@ import fr.traqueur.commands.annotations.Arg; import fr.traqueur.commands.annotations.Command; import fr.traqueur.commands.annotations.CommandContainer; -import fr.traqueur.commands.annotations.MockSender; +import fr.traqueur.commands.test.mocks.*; import java.util.ArrayList; import java.util.List; 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 index 744edf6..a68c14b 100644 --- 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 @@ -1,6 +1,7 @@ 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; diff --git a/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java b/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java index 3ed9663..3e1148d 100644 --- a/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java @@ -6,9 +6,11 @@ import fr.traqueur.commands.api.exceptions.ArgumentIncorrectException; import fr.traqueur.commands.api.exceptions.ArgumentNotExistException; import fr.traqueur.commands.api.models.Command; -import fr.traqueur.commands.api.models.CommandPlatform; import fr.traqueur.commands.api.models.collections.CommandTree; import fr.traqueur.commands.impl.logging.InternalLogger; +import fr.traqueur.commands.test.mocks.MockCommandManager; +import fr.traqueur.commands.test.mocks.MockPlatform; +import fr.traqueur.commands.test.mocks.MockSender; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -23,15 +25,13 @@ class CommandManagerTest { private FakeLogger logger; - private CommandManager manager; - private FakePlatform platform; + private MockCommandManager manager; + private MockPlatform platform; @BeforeEach void setUp() { - platform = new FakePlatform(); - manager = new CommandManager<>(platform) { - }; - platform.injectManager(manager); + manager = new MockCommandManager(); + platform = manager.getMockPlatform(); logger = new FakeLogger(); manager.setLogger(logger); } @@ -39,9 +39,9 @@ void setUp() { // ----- TESTS ----- @Test void testInfiniteArgsParsing() throws Exception { - Command cmd = new Command<>(null, "test") { + Command cmd = new Command<>(null, "test") { @Override - public void execute(String sender, Arguments arguments) { + public void execute(MockSender sender, Arguments arguments) { } }; cmd.setManager(manager); @@ -57,7 +57,7 @@ public void execute(String sender, Arguments arguments) { @Test void testInfiniteArgsStopsFurtherParsing() throws Exception { - Command cmd = new DummyCommand(); + Command cmd = new DummyCommand(); cmd.setManager(manager); cmd.addArgs("first", String.class); cmd.addArgs("rest", Infinite.class); @@ -71,7 +71,7 @@ void testInfiniteArgsStopsFurtherParsing() throws Exception { @Test void testNoExtraAfterInfinite() throws Exception { - Command cmd = new DummyCommand(); + Command cmd = new DummyCommand(); cmd.setManager(manager); cmd.addArg("x", Infinite.class); cmd.addArg("y", String.class); @@ -84,7 +84,7 @@ void testNoExtraAfterInfinite() throws Exception { @Test void testOptionalArgs_onlyDefault() throws Exception { - Command cmd = new DummyCommand(); + Command cmd = new DummyCommand(); cmd.addArgs("req", String.class); cmd.addOptionalArgs("opt1", Integer.class); cmd.addOptionalArgs("opt2", Double.class); @@ -101,7 +101,7 @@ void testOptionalArgs_onlyDefault() throws Exception { @Test void testBasicArgParsing_correctTypes() throws Exception { - Command cmd = new DummyCommand(); + Command cmd = new DummyCommand(); cmd.addArgs("num", Integer.class); cmd.addOptionalArgs("opt", String.class); @@ -117,13 +117,13 @@ void testBasicArgParsing_correctTypes() throws Exception { @Test void addArgs_withOddArgs_shouldThrow() { - Command cmd = new DummyCommand(); + Command cmd = new DummyCommand(); assertThrows(IllegalArgumentException.class, () -> cmd.addArgs("bad")); } @Test void testArgumentIncorrectException_onBadType() { - Command cmd = new DummyCommand(); + Command cmd = new DummyCommand(); cmd.addArgs("n", Integer.class); String[] input = {"notAnInt"}; assertThrows(ArgumentIncorrectException.class, () -> manager.parse(cmd, input)); @@ -131,12 +131,12 @@ void testArgumentIncorrectException_onBadType() { @Test void testCommandRegistration_entriesInTree() { - Command cmd = new DummyCommand("main"); + Command cmd = new DummyCommand("main"); cmd.addAlias("m"); cmd.addSubCommand(new DummyCommand("sub")); manager.registerCommand(cmd); - CommandTree tree = manager.getCommands(); + CommandTree tree = manager.getCommands(); assertTrue(tree.getRoot().getChildren().containsKey("main")); assertTrue(tree.getRoot().getChildren().containsKey("m")); assertTrue(tree.findNode("main", new String[]{"sub"}).isPresent()); @@ -150,7 +150,7 @@ void registerCommand_shouldAddMainAndAliasAndSubcommands() { main.addSubCommand(sub); manager.registerCommand(main); - List added = platform.added; + List added = platform.getRegisteredLabels(); assertTrue(added.contains("dummy")); assertTrue(added.contains("a1")); assertTrue(added.contains("a2")); @@ -159,19 +159,19 @@ void registerCommand_shouldAddMainAndAliasAndSubcommands() { @Test void addCommand_shouldRegisterCompletersForArgs() { - Command cmd = new DummyCommand(); + Command cmd = new DummyCommand(); cmd.addArgs("intArg", Integer.class); cmd.addOptionalArgs("optArg", Double.class); manager.registerCommand(cmd); - Map>> comps = manager.getCompleters(); + Map>> comps = manager.getCompleters(); assertTrue(comps.containsKey("dummy")); - Map> map = comps.get("dummy"); + Map> map = comps.get("dummy"); assertTrue(map.containsKey(1)); assertTrue(map.containsKey(2)); } - static class DummyCommand extends Command { + static class DummyCommand extends Command { DummyCommand() { super(null, "dummy"); } @@ -181,48 +181,7 @@ static class DummyCommand extends Command { } @Override - public void execute(String sender, Arguments args) { - } - } - - static class FakePlatform implements CommandPlatform { - List added = new ArrayList<>(); - - @Override - public Object getPlugin() { - return null; - } - - @Override - public void injectManager(CommandManager cm) { - } - - @Override - public java.util.logging.Logger getLogger() { - return java.util.logging.Logger.getAnonymousLogger(); - } - - @Override - public boolean hasPermission(String sender, String permission) { - return true; - } - - @Override - public boolean isPlayer(String sender) { - return false; - } - - @Override - public void sendMessage(String sender, String message) { - } - - @Override - public void addCommand(Command command, String label) { - added.add(label); - } - - @Override - public void removeCommand(String label, boolean sub) { + public void execute(MockSender sender, Arguments args) { } } diff --git a/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java b/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java index 628d0f1..2e53b06 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java @@ -1,34 +1,34 @@ package fr.traqueur.commands.api.models; -import fr.traqueur.commands.api.CommandManager; import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.api.requirements.Requirement; +import fr.traqueur.commands.test.mocks.MockCommandManager; +import fr.traqueur.commands.test.mocks.MockPlatform; +import fr.traqueur.commands.test.mocks.MockSender; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.List; import java.util.concurrent.atomic.AtomicReference; -import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.*; class CommandBuilderTest { - private CommandManager manager; - private FakePlatform platform; + private MockCommandManager manager; + private MockPlatform platform; @BeforeEach void setUp() { - platform = new FakePlatform(); - manager = new CommandManager<>(platform) { - }; + manager = new MockCommandManager(); + platform = manager.getMockPlatform(); } // --- Basic building --- @Test void build_simpleCommand_success() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .description("Test description") .usage("/test") .permission("test.use") @@ -44,7 +44,7 @@ void build_simpleCommand_success() { @Test void build_withoutExecutor_throwsException() { - CommandBuilder builder = manager.command("test") + CommandBuilder builder = manager.command("test") .description("Test"); assertThrows(IllegalStateException.class, builder::build); @@ -52,7 +52,7 @@ void build_withoutExecutor_throwsException() { @Test void build_withGameOnly_setsFlag() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .gameOnly() .executor((sender, args) -> { }) @@ -63,7 +63,7 @@ void build_withGameOnly_setsFlag() { @Test void build_withGameOnlyFalse_clearsFlag() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .gameOnly(false) .executor((sender, args) -> { }) @@ -76,7 +76,7 @@ void build_withGameOnlyFalse_clearsFlag() { @Test void build_withArgs_addsArguments() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .arg("name", String.class) .arg("count", Integer.class) .executor((sender, args) -> { @@ -90,7 +90,7 @@ void build_withArgs_addsArguments() { @Test void build_withOptionalArgs_addsOptionalArguments() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .arg("required", String.class) .optionalArg("optional1", Integer.class) .optionalArg("optional2", Double.class) @@ -106,7 +106,7 @@ void build_withOptionalArgs_addsOptionalArguments() { @Test void build_withTabCompleter_addsCustomCompleter() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .arg("player", String.class, (sender, args) -> List.of("Alice", "Bob")) .executor((sender, args) -> { }) @@ -119,7 +119,7 @@ void build_withTabCompleter_addsCustomCompleter() { @Test void build_withAlias_addsAlias() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .alias("t") .executor((sender, args) -> { }) @@ -131,7 +131,7 @@ void build_withAlias_addsAlias() { @Test void build_withAliases_addsMultipleAliases() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .aliases("t", "tst", "te") .executor((sender, args) -> { }) @@ -147,12 +147,12 @@ void build_withAliases_addsMultipleAliases() { @Test void build_withSubcommand_addsSubcommand() { - Command sub = manager.command("sub") + Command sub = manager.command("sub") .executor((sender, args) -> { }) .build(); - Command cmd = manager.command("main") + Command cmd = manager.command("main") .subcommand(sub) .executor((sender, args) -> { }) @@ -164,17 +164,17 @@ void build_withSubcommand_addsSubcommand() { @Test void build_withSubcommands_addsMultipleSubcommands() { - Command sub1 = manager.command("sub1") + Command sub1 = manager.command("sub1") .executor((sender, args) -> { }) .build(); - Command sub2 = manager.command("sub2") + Command sub2 = manager.command("sub2") .executor((sender, args) -> { }) .build(); - Command cmd = manager.command("main") + Command cmd = manager.command("main") .subcommands(sub1, sub2) .executor((sender, args) -> { }) @@ -187,9 +187,9 @@ void build_withSubcommands_addsMultipleSubcommands() { @Test void build_withRequirement_addsRequirement() { - Requirement req = new Requirement<>() { + Requirement req = new Requirement() { @Override - public boolean check(Object sender) { + public boolean check(MockSender sender) { return true; } @@ -199,7 +199,7 @@ public String errorMessage() { } }; - Command cmd = manager.command("test") + Command cmd = manager.command("test") .requirement(req) .executor((sender, args) -> { }) @@ -211,11 +211,11 @@ public String errorMessage() { @Test void build_withRequirements_addsMultipleRequirements() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .requirements( - new Requirement<>() { + new Requirement() { @Override - public boolean check(Object sender) { + public boolean check(MockSender sender) { return true; } @@ -224,9 +224,9 @@ public String errorMessage() { return ""; } }, - new Requirement<>() { + new Requirement() { @Override - public boolean check(Object sender) { + public boolean check(MockSender sender) { return false; } @@ -249,13 +249,13 @@ public String errorMessage() { void build_executorIsCalled() { AtomicReference received = new AtomicReference<>(); - Command cmd = manager.command("test") + Command cmd = manager.command("test") .executor((sender, args) -> { received.set("executed"); }) .build(); - cmd.execute(null, new Arguments(new fr.traqueur.commands.impl.logging.InternalLogger(Logger.getLogger("test")))); + cmd.execute(null, new Arguments(new fr.traqueur.commands.impl.logging.InternalLogger(java.util.logging.Logger.getLogger("test")))); assertEquals("executed", received.get()); } @@ -264,18 +264,18 @@ void build_executorIsCalled() { @Test void register_registersCommandInManager() { - Command cmd = manager.command("registered") + Command cmd = manager.command("registered") .executor((sender, args) -> { }) .register(); - assertTrue(platform.registeredLabels.contains("registered")); + assertTrue(platform.getRegisteredLabels().contains("registered")); assertTrue(manager.getCommands().findNode("registered", new String[]{}).isPresent()); } @Test void register_returnsBuiltCommand() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .description("Test") .executor((sender, args) -> { }) @@ -290,12 +290,12 @@ void register_returnsBuiltCommand() { @Test void build_fluentChain_allOptions() { - Command sub = manager.command("sub") + Command sub = manager.command("sub") .executor((sender, args) -> { }) .build(); - Command cmd = manager.command("complex") + Command cmd = manager.command("complex") .description("Complex command") .usage("/complex [opt]") .permission("complex.use") @@ -304,9 +304,9 @@ void build_fluentChain_allOptions() { .arg("required", String.class) .optionalArg("optional", Integer.class) .subcommand(sub) - .requirement(new Requirement<>() { + .requirement(new Requirement() { @Override - public boolean check(Object sender) { + public boolean check(MockSender sender) { return true; } @@ -335,7 +335,7 @@ public String errorMessage() { @Test void build_defaultValues_areEmpty() { - Command cmd = manager.command("minimal") + Command cmd = manager.command("minimal") .executor((sender, args) -> { }) .build(); @@ -354,7 +354,7 @@ void build_defaultValues_areEmpty() { @Test void build_commandIsEnabled_byDefault() { - Command cmd = manager.command("test") + Command cmd = manager.command("test") .executor((sender, args) -> { }) .build(); @@ -362,47 +362,4 @@ void build_commandIsEnabled_byDefault() { assertTrue(cmd.isEnabled()); } - // --- Helper classes --- - - static class FakePlatform implements CommandPlatform { - java.util.List registeredLabels = new java.util.ArrayList<>(); - - @Override - public Object getPlugin() { - return new Object(); - } - - @Override - public void injectManager(CommandManager cm) { - } - - @Override - public Logger getLogger() { - return Logger.getAnonymousLogger(); - } - - @Override - public boolean hasPermission(Object sender, String permission) { - return true; - } - - @Override - public boolean isPlayer(Object sender) { - return false; - } - - @Override - public void sendMessage(Object sender, String message) { - } - - @Override - public void addCommand(Command command, String label) { - registeredLabels.add(label); - } - - @Override - public void removeCommand(String label, boolean sub) { - registeredLabels.remove(label); - } - } } \ No newline at end of file diff --git a/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java b/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java index 2a147b1..5bcee3e 100644 --- a/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandTest.java @@ -1,7 +1,8 @@ package fr.traqueur.commands.api.models; -import fr.traqueur.commands.api.CommandManager; import fr.traqueur.commands.api.arguments.Arguments; +import fr.traqueur.commands.test.mocks.MockCommandManager; +import fr.traqueur.commands.test.mocks.MockSender; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,48 +15,11 @@ class CommandTest { private DummyCommand cmd; + private MockCommandManager manager; @BeforeEach void setUp() { - CommandPlatform platform = new CommandPlatform<>() { - @Override - public String getPlugin() { - return null; - } - - @Override - public void injectManager(CommandManager commandManager) { - } - - @Override - public java.util.logging.Logger getLogger() { - return java.util.logging.Logger.getAnonymousLogger(); - } - - @Override - public boolean hasPermission(Object sender, String permission) { - return true; - } - - @Override - public boolean isPlayer(Object sender) { - return false; - } - - @Override - public void sendMessage(Object sender, String message) { - } - - @Override - public void addCommand(Command command, String label) { - } - - @Override - public void removeCommand(String label, boolean subcommand) { - } - }; - CommandManager manager = new CommandManager<>(platform) { - }; + manager = new MockCommandManager(); cmd = new DummyCommand(); cmd.setManager(manager); } @@ -96,7 +60,7 @@ void testSettersAndGetters() { void testAddSubCommandAndIsSubcommandFlag() { DummyCommand sub = new DummyCommand(); cmd.addSubCommand(sub); - List> subs = cmd.getSubcommands(); + List> subs = cmd.getSubcommands(); assertEquals(1, subs.size()); assertTrue(subs.contains(sub)); assertTrue(sub.isSubCommand()); @@ -104,96 +68,16 @@ void testAddSubCommandAndIsSubcommandFlag() { @Test void testRegisterDelegatesToManager() { - AtomicBoolean called = new AtomicBoolean(false); - CommandManager fakeManager = new CommandManager<>(new CommandPlatform() { - @Override - public String getPlugin() { - return null; - } - - @Override - public void injectManager(CommandManager commandManager) { - } - - @Override - public java.util.logging.Logger getLogger() { - return java.util.logging.Logger.getAnonymousLogger(); - } - - @Override - public boolean hasPermission(Object sender, String permission) { - return true; - } - - @Override - public boolean isPlayer(Object sender) { - return false; - } - - @Override - public void sendMessage(Object sender, String message) { - } - - @Override - public void addCommand(Command command, String label) { - called.set(true); - } - - @Override - public void removeCommand(String label, boolean subcommand) { - } - }) { - }; - cmd.setManager(fakeManager); - fakeManager.registerCommand(cmd); - assertTrue(called.get()); + manager.registerCommand(cmd); + assertTrue(manager.getMockPlatform().hasCommand("dummy")); } @Test void testUnregisterDelegatesToManager() { - AtomicBoolean called = new AtomicBoolean(false); - CommandManager fakeManager = new CommandManager(new CommandPlatform() { - @Override - public String getPlugin() { - return null; - } - - @Override - public void injectManager(CommandManager commandManager) { - } - - @Override - public java.util.logging.Logger getLogger() { - return java.util.logging.Logger.getAnonymousLogger(); - } - - @Override - public boolean hasPermission(Object sender, String permission) { - return true; - } - - @Override - public boolean isPlayer(Object sender) { - return false; - } - - @Override - public void sendMessage(Object sender, String message) { - } - - @Override - public void addCommand(Command command, String label) { - } - - @Override - public void removeCommand(String label, boolean subcommand) { - called.set(true); - } - }) { - }; - cmd.setManager(fakeManager); + manager.registerCommand(cmd); + assertTrue(manager.getMockPlatform().hasCommand("dummy")); cmd.unregister(); - assertTrue(called.get()); + assertFalse(manager.getMockPlatform().hasCommand("dummy")); } @Test @@ -311,17 +195,17 @@ void getAllLabels_nameAlwaysFirst() { assertEquals("dummy", labels.get(0)); } - private static class DummyCommand extends Command { + private static class DummyCommand extends Command { DummyCommand(String name) { - super("plugin", name); + super(null, name); } DummyCommand() { - super("plugin", "dummy"); + super(null, "dummy"); } @Override - public void execute(Object sender, Arguments arguments) { + public void execute(MockSender sender, Arguments arguments) { // no-op } } diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockCommandManager.java b/core/src/test/java/fr/traqueur/commands/test/mocks/MockCommandManager.java similarity index 71% rename from annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockCommandManager.java rename to core/src/test/java/fr/traqueur/commands/test/mocks/MockCommandManager.java index b96f822..2c0cef6 100644 --- a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockCommandManager.java +++ b/core/src/test/java/fr/traqueur/commands/test/mocks/MockCommandManager.java @@ -1,7 +1,11 @@ -package fr.traqueur.commands.annotations; +package fr.traqueur.commands.test.mocks; import fr.traqueur.commands.api.CommandManager; +/** + * Mock command manager for testing purposes. + * Simplifies test setup by providing a ready-to-use manager with mock platform. + */ public class MockCommandManager extends CommandManager { private final MockPlatform mockPlatform; @@ -18,4 +22,4 @@ public MockCommandManager(MockPlatform platform) { public MockPlatform getMockPlatform() { return mockPlatform; } -} \ No newline at end of file +} diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlatform.java b/core/src/test/java/fr/traqueur/commands/test/mocks/MockPlatform.java similarity index 93% rename from annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlatform.java rename to core/src/test/java/fr/traqueur/commands/test/mocks/MockPlatform.java index 6f6259e..3c14ffd 100644 --- a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockPlatform.java +++ b/core/src/test/java/fr/traqueur/commands/test/mocks/MockPlatform.java @@ -1,4 +1,4 @@ -package fr.traqueur.commands.annotations; +package fr.traqueur.commands.test.mocks; import fr.traqueur.commands.api.CommandManager; import fr.traqueur.commands.api.models.Command; @@ -8,6 +8,10 @@ import java.util.*; import java.util.logging.Logger; +/** + * Mock platform for testing purposes. + * Provides a minimal implementation for testing command registration and execution. + */ public class MockPlatform implements CommandPlatform { private final Object plugin = new Object(); @@ -15,7 +19,7 @@ public class MockPlatform implements CommandPlatform { private final MockSenderResolver senderResolver = new MockSenderResolver(); private final Map> registeredCommands = new HashMap<>(); private final List registeredLabels = new ArrayList<>(); - + private CommandManager commandManager; @Override @@ -81,4 +85,4 @@ public boolean hasCommand(String label) { public Command getCommand(String label) { return registeredCommands.get(label); } -} \ No newline at end of file +} diff --git a/core/src/test/java/fr/traqueur/commands/test/mocks/MockPlayer.java b/core/src/test/java/fr/traqueur/commands/test/mocks/MockPlayer.java new file mode 100644 index 0000000..8655712 --- /dev/null +++ b/core/src/test/java/fr/traqueur/commands/test/mocks/MockPlayer.java @@ -0,0 +1,9 @@ +package fr.traqueur.commands.test.mocks; + +/** + * Mock player for testing purposes. + * Represents a game-only sender. + */ +public interface MockPlayer extends MockSender { + String getName(); +} diff --git a/core/src/test/java/fr/traqueur/commands/test/mocks/MockSender.java b/core/src/test/java/fr/traqueur/commands/test/mocks/MockSender.java new file mode 100644 index 0000000..18b93d0 --- /dev/null +++ b/core/src/test/java/fr/traqueur/commands/test/mocks/MockSender.java @@ -0,0 +1,10 @@ +package fr.traqueur.commands.test.mocks; + +/** + * Mock sender for testing purposes. + * Shared across all modules for consistent testing. + */ +public interface MockSender { + void sendMessage(String message); + boolean hasPermission(String permission); +} diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSenderResolver.java b/core/src/test/java/fr/traqueur/commands/test/mocks/MockSenderResolver.java similarity index 86% rename from annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSenderResolver.java rename to core/src/test/java/fr/traqueur/commands/test/mocks/MockSenderResolver.java index a60cf8b..a564b51 100644 --- a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/MockSenderResolver.java +++ b/core/src/test/java/fr/traqueur/commands/test/mocks/MockSenderResolver.java @@ -1,7 +1,10 @@ -package fr.traqueur.commands.annotations; +package fr.traqueur.commands.test.mocks; import fr.traqueur.commands.api.resolver.SenderResolver; +/** + * Mock sender resolver for testing purposes. + */ public class MockSenderResolver implements SenderResolver { @Override @@ -21,4 +24,4 @@ public Object resolve(MockSender sender, Class type) { public boolean isGameOnly(Class type) { return MockPlayer.class.isAssignableFrom(type); } -} \ No newline at end of file +} From db51bb043b335b016bcce852fe51dd8a9cc405a4 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Wed, 31 Dec 2025 13:01:37 +0100 Subject: [PATCH 18/23] fix(spigot): spigot integration test --- .../traqueur/commands/annotations/Alias.java | 2 +- .../AnnotationCommandProcessor.java | 89 ++++++++++++++----- .../commands/annotations/Optional.java | 30 ------- .../AnnotationCommandProcessorTest.java | 10 +-- .../commands/InfiniteArgsTestCommands.java | 12 ++- .../commands/OptionalArgsTestCommands.java | 17 ++-- .../spigot/SpigotIntegrationTest.java | 23 +++++ 7 files changed, 114 insertions(+), 69 deletions(-) delete mode 100644 annotations-addon/src/main/java/fr/traqueur/commands/annotations/Optional.java diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Alias.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Alias.java index 6e9b321..18b2f5d 100644 --- a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Alias.java +++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Alias.java @@ -6,7 +6,7 @@ import java.lang.annotation.Target; /** - * Defines aliases for a {@link Command} or {@link Subcommand}. + * Defines aliases for a {@link Command}. * *

Example:

*
{@code
diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java
index 316da46..7bc2e83 100644
--- a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java
+++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java
@@ -9,6 +9,8 @@
 
 import java.lang.reflect.Method;
 import java.lang.reflect.Parameter;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
 import java.util.*;
 
 /**
@@ -72,7 +74,6 @@ private void processHandler(Object handler) {
         }
 
         // Third pass: build ALL commands first
-        // Key: fullPath, Value: command
         Map> builtCommands = new LinkedHashMap<>();
         Set rootCommands = new LinkedHashSet<>();
 
@@ -80,9 +81,6 @@ private void processHandler(Object handler) {
             String parentPath = getParentPath(info.name);
             boolean hasParentInBatch = parentPath != null && allPaths.contains(parentPath);
 
-            // Build command with appropriate name
-            // - If parent exists in batch: use short name (will be added via addSubCommand)
-            // - If no parent in batch (orphan): use full path (core will create parents)
             Command command = buildCommand(info.handler, info.method, info.name, hasParentInBatch);
             builtCommands.put(info.name, command);
         }
@@ -92,17 +90,15 @@ private void processHandler(Object handler) {
             String parentPath = getParentPath(info.name);
 
             if (parentPath != null && allPaths.contains(parentPath)) {
-                // Parent exists in our batch -> add as subcommand
                 Command parent = builtCommands.get(parentPath);
                 Command child = builtCommands.get(info.name);
                 parent.addSubCommand(child);
             } else {
-                // No parent in batch -> this is a root command (or orphan)
                 rootCommands.add(info.name);
             }
         }
 
-        // Fifth pass: register only root commands (manager handles subcommands)
+        // Fifth pass: register only root commands
         for (String rootPath : rootCommands) {
             Command rootCommand = builtCommands.get(rootPath);
             manager.registerCommand(rootCommand);
@@ -129,8 +125,6 @@ private Command buildCommand(Object handler, Method method, String fullPat
         fr.traqueur.commands.annotations.Command annotation =
                 method.getAnnotation(fr.traqueur.commands.annotations.Command.class);
 
-        // If parent exists in batch: use short name (will be added via addSubCommand)
-        // If no parent (orphan/root): use full path (core will create parent hierarchy)
         String commandName = hasParentInBatch ? getCommandName(fullPath) : fullPath;
 
         CommandBuilder builder = manager.command(commandName)
@@ -146,8 +140,15 @@ private Command buildCommand(Object handler, Method method, String fullPat
         processParameters(builder, method, fullPath);
 
         Parameter[] params = method.getParameters();
-        if (params.length > 0 && senderResolver.isGameOnly(params[0].getType())) {
-            builder.gameOnly();
+        if (params.length > 0) {
+            Class senderType = params[0].getType();
+            // Handle Optional as sender (extract inner type)
+            if (senderType == Optional.class) {
+                senderType = extractOptionalType(params[0]);
+            }
+            if (senderResolver.isGameOnly(senderType)) {
+                builder.gameOnly();
+            }
         }
 
         method.setAccessible(true);
@@ -166,14 +167,24 @@ private void processTabCompleter(Object handler, Method method) {
 
     private void processParameters(CommandBuilder builder, Method method, String commandPath) {
         Parameter[] params = method.getParameters();
+        Type[] genericTypes = method.getGenericParameterTypes();
 
         for (int i = 0; i < params.length; i++) {
             Parameter param = params[i];
+            Class paramType = param.getType();
 
-            if (i == 0 && senderResolver.canResolve(param.getType())) {
-                continue;
+            // First parameter is sender (skip it for args)
+            if (i == 0) {
+                Class senderType = paramType;
+                if (paramType == Optional.class) {
+                    senderType = extractOptionalType(param);
+                }
+                if (senderResolver.canResolve(senderType)) {
+                    continue;
+                }
             }
 
+            // Must have @Arg annotation
             Arg argAnnotation = param.getAnnotation(Arg.class);
             if (argAnnotation == null) {
                 throw new IllegalArgumentException(
@@ -183,16 +194,26 @@ private void processParameters(CommandBuilder builder, Method method, Stri
             }
 
             String argName = argAnnotation.value();
-            Class argType = param.getType();
-            boolean isOptional = param.isAnnotationPresent(Optional.class);
-            boolean isInfinite = param.isAnnotationPresent(fr.traqueur.commands.annotations.Infinite.class);
+            boolean isOptional = paramType == Optional.class;
+            boolean isInfinite = param.isAnnotationPresent(Infinite.class);
+
+            // Determine the actual argument type
+            Class argType;
+            if (isOptional) {
+                argType = extractOptionalType(param);
+            } else {
+                argType = paramType;
+            }
 
+            // If @Infinite, use Infinite.class as the type
             if (isInfinite) {
                 argType = fr.traqueur.commands.api.arguments.Infinite.class;
             }
 
+            // Get tab completer if exists
             TabCompleter completer = getTabCompleter(commandPath, argName);
 
+            // Add argument to builder
             if (isOptional) {
                 if (completer != null) {
                     builder.optionalArg(argName, argType, completer);
@@ -209,6 +230,22 @@ private void processParameters(CommandBuilder builder, Method method, Stri
         }
     }
 
+    /**
+     * Extract the inner type from Optional.
+     */
+    private Class extractOptionalType(Parameter param) {
+        Type genericType = param.getParameterizedType();
+        if (genericType instanceof ParameterizedType parameterizedType) {
+            Type[] typeArgs = parameterizedType.getActualTypeArguments();
+            if (typeArgs.length > 0 && typeArgs[0] instanceof Class innerClass) {
+                return innerClass;
+            }
+        }
+        throw new IllegalArgumentException(
+                "Cannot extract type from Optional parameter: " + param.getName()
+        );
+    }
+
     @SuppressWarnings("unchecked")
     private TabCompleter getTabCompleter(String commandPath, String argName) {
         String key = commandPath + ":" + argName;
@@ -248,18 +285,26 @@ private void invokeMethod(Object handler, Method method, S sender, Arguments arg
 
             for (int i = 0; i < params.length; i++) {
                 Parameter param = params[i];
-
-                if (i == 0 && senderResolver.canResolve(param.getType())) {
-                    invokeArgs[i] = senderResolver.resolve(sender, param.getType());
-                    continue;
+                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 (param.isAnnotationPresent(Optional.class)) {
-                        invokeArgs[i] = args.getOptional(argName).orElse(null);
+                    if (isOptional) {
+                        invokeArgs[i] = args.getOptional(argName);
                     } else {
                         invokeArgs[i] = args.get(argName);
                     }
diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Optional.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Optional.java
deleted file mode 100644
index 2a37431..0000000
--- a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Optional.java
+++ /dev/null
@@ -1,30 +0,0 @@
-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 optional.
- * 
- * 

Optional parameters will be null if not provided by the user. - * Use wrapper types (Integer, Boolean, etc.) instead of primitives - * to allow null values.

- * - *

Example:

- *
{@code
- * @Command(name = "heal")
- * public void heal(Player sender, @Arg("target") @Optional Player target) {
- *     Player toHeal = (target != null) ? target : sender;
- *     toHeal.setHealth(20);
- * }
- * }
- * - * @since 5.0.0 - * @see Arg - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.PARAMETER) -public @interface Optional { -} \ 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 index 866b90a..e548aac 100644 --- a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java @@ -200,7 +200,7 @@ void shouldRegisterCommandWithOptionalArg() { 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()); @@ -213,12 +213,12 @@ void shouldRegisterMixedArgs() { processor.register(commands); Command give = platform.getCommand("give"); - + assertEquals(1, give.getArgs().size()); - assertEquals("item", give.getArgs().get(0).name()); - + assertEquals("item", give.getArgs().getFirst().name()); + assertEquals(1, give.getOptionalArgs().size()); - assertEquals("amount", give.getOptionalArgs().get(0).name()); + assertEquals("amount", give.getOptionalArgs().getFirst().name()); } } 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 index b65b7bb..516c7aa 100644 --- 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 @@ -1,10 +1,14 @@ package fr.traqueur.commands.annotations.commands; -import fr.traqueur.commands.annotations.*; -import fr.traqueur.commands.test.mocks.*; +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 { @@ -21,8 +25,8 @@ public void broadcast(MockSender sender, @Arg("message") @Infinite String messag @Command(name = "kick", description = "Kick a player") public void kick(MockSender sender, @Arg("player") String player, - @Arg("reason") @Optional @Infinite String reason) { + @Arg("reason") @Infinite Optional reason) { executedCommands.add("kick"); - executedArgs.add(new Object[]{sender, player, reason}); + 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/OptionalArgsTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OptionalArgsTestCommands.java index 8cf7edc..0405942 100644 --- 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 @@ -1,10 +1,13 @@ package fr.traqueur.commands.annotations.commands; -import fr.traqueur.commands.annotations.*; -import fr.traqueur.commands.test.mocks.*; +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 { @@ -13,16 +16,16 @@ public class OptionalArgsTestCommands { public final List executedArgs = new ArrayList<>(); @Command(name = "heal", description = "Heal a player") - public void heal(MockPlayer sender, @Arg("target") @Optional MockPlayer target) { + public void heal(MockPlayer sender, @Arg("target") Optional target) { executedCommands.add("heal"); - executedArgs.add(new Object[]{sender, target}); + executedArgs.add(new Object[]{sender, target.orElse(null)}); } @Command(name = "give", description = "Give items") - public void give(MockPlayer sender, + public void give(MockPlayer sender, @Arg("item") String item, - @Arg("amount") @Optional Integer amount) { + @Arg("amount") Optional amount) { executedCommands.add("give"); - executedArgs.add(new Object[]{sender, item, amount}); + executedArgs.add(new Object[]{sender, item, amount.orElse(null)}); } } \ No newline at end of file diff --git a/spigot/src/test/java/fr/traqueur/commands/spigot/SpigotIntegrationTest.java b/spigot/src/test/java/fr/traqueur/commands/spigot/SpigotIntegrationTest.java index 9696b27..e588518 100644 --- a/spigot/src/test/java/fr/traqueur/commands/spigot/SpigotIntegrationTest.java +++ b/spigot/src/test/java/fr/traqueur/commands/spigot/SpigotIntegrationTest.java @@ -58,6 +58,29 @@ public void addCommand(Command command, String label) { @Override public void removeCommand(String label, boolean subcommand) { } + + @Override + public fr.traqueur.commands.api.resolver.SenderResolver getSenderResolver() { + return new fr.traqueur.commands.api.resolver.SenderResolver() { + @Override + public boolean canResolve(Class type) { + return CommandSender.class.isAssignableFrom(type) || Player.class.isAssignableFrom(type); + } + + @Override + public Object resolve(CommandSender sender, Class type) { + if (type.isInstance(sender)) { + return type.cast(sender); + } + return null; + } + + @Override + public boolean isGameOnly(Class type) { + return Player.class.isAssignableFrom(type); + } + }; + } }) { }; manager.registerConverter(Player.class, new PlayerArgument()); From 60e2312ba48f9c48e77403067c602b5db2b878cf Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Wed, 31 Dec 2025 13:06:30 +0100 Subject: [PATCH 19/23] feat: remove useless test --- .../traqueur/commands/api/updater/UpdaterTest.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java b/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java index 5c54739..0c10f60 100644 --- a/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/updater/UpdaterTest.java @@ -78,20 +78,6 @@ void checkUpdatesAsync_logsUpToDateWhenVersionsMatch() { } } - @Test - void checkUpdatesAsync_doesNothingWhenLatestIsNull() { - try (MockedStatic mocks = mockStatic(Updater.class)) { - - mocks.when(Updater::checkUpdates).thenCallRealMethod(); - mocks.when(Updater::fetchLatestVersionAsync) - .thenReturn(CompletableFuture.completedFuture(null)); - - Updater.checkUpdates(); - - assertFalse(logHandler.anyMatch(Level.WARNING, r -> true)); - } - } - /* ------------------------------------------------------------ */ /* Test log handler */ /* ------------------------------------------------------------ */ From 97fea42fdab3eb0608a1df49b0c7c8f1e29d88c6 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Wed, 31 Dec 2025 13:13:14 +0100 Subject: [PATCH 20/23] feat: remove @optional form migration file --- MIGRATION_v4_to_v5.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MIGRATION_v4_to_v5.md b/MIGRATION_v4_to_v5.md index dcca08d..363c386 100644 --- a/MIGRATION_v4_to_v5.md +++ b/MIGRATION_v4_to_v5.md @@ -99,7 +99,6 @@ processor.register(new MyCommands()); - `@CommandContainer` - Marque une classe contenant des commandes - `@Command(name, description, permission, usage)` - Définit une commande - `@Arg("name")` - Marque un paramètre d'argument -- `@Optional` - Marque un argument comme optionnel - `@Infinite` - Argument infini (prend tout le reste) - `@Alias("alias1", "alias2")` - Définit des alias - `@TabComplete(command, arg)` - Définit l'autocomplétion @@ -113,12 +112,13 @@ public void admin(CommandSender sender) { @Command(name = "admin.kick", description = "Kick a player") public void adminKick(CommandSender sender, - @Arg("player") Player target, - @Arg("reason") @Optional String reason) { - // Accessible via /admin kick [reason] + @Arg("player") Player target) { + // Accessible via /admin kick } ``` +**Note:** Les arguments optionnels doivent être ajoutés via le CommandBuilder ou la classe Command directement. Les annotations ne supportent que les arguments requis. + **Types de Sender Automatiques:** ```java // Accepte n'importe quel sender From 16103b3217eb02d74d453a5747b5f38def08466a Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Fri, 2 Jan 2026 12:06:53 +0100 Subject: [PATCH 21/23] feat: add annoted command exemple in jda module --- jda-test-bot/build.gradle | 1 + .../fr/traqueur/commands/test/TestBot.java | 8 +++++ .../commands/annoted/TestAnnotedCommands.java | 29 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TestAnnotedCommands.java diff --git a/jda-test-bot/build.gradle b/jda-test-bot/build.gradle index 5e62de5..21b4000 100644 --- a/jda-test-bot/build.gradle +++ b/jda-test-bot/build.gradle @@ -8,6 +8,7 @@ repositories { dependencies { implementation project(":jda") + implementation project(":annotations-addon") implementation("net.dv8tion:JDA:5.2.1") { exclude module: 'opus-java' } diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java index 7fb0401..7b00f87 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java @@ -1,9 +1,12 @@ package fr.traqueur.commands.test; +import fr.traqueur.commands.annotations.AnnotationCommandProcessor; import fr.traqueur.commands.jda.CommandManager; import fr.traqueur.commands.test.commands.*; +import fr.traqueur.commands.test.commands.annoted.TestAnnotedCommands; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.requests.GatewayIntent; import java.util.logging.Logger; @@ -35,8 +38,13 @@ public TestBot(String token) throws InterruptedException { CommandManager commandManager = new CommandManager<>(this, jda, LOGGER); commandManager.setDebug(true); + AnnotationCommandProcessor annotationProcessor = + new AnnotationCommandProcessor<>(commandManager); + // Register commands LOGGER.info("Registering commands..."); + annotationProcessor.register(new TestAnnotedCommands()); + commandManager.registerCommand(new PingCommand(this)); commandManager.registerCommand(new UserInfoCommand(this)); commandManager.registerCommand(new MathCommand(this)); diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TestAnnotedCommands.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TestAnnotedCommands.java new file mode 100644 index 0000000..1f82aeb --- /dev/null +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TestAnnotedCommands.java @@ -0,0 +1,29 @@ +package fr.traqueur.commands.test.commands.annoted; + +import fr.traqueur.commands.annotations.*; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@CommandContainer +public class TestAnnotedCommands { + + @Command(name = "testannoted", description = "A test annoted command", usage = "/testannoted") + @Alias(value = {"ta", "testa"}) + public void testAnnotedCommand(SlashCommandInteractionEvent event, @Arg("arg1") int argument1, @Arg("arg2") Optional argument2) { + String arg2Value = argument2.orElse("no value provided"); + event.reply("You executed the testannoted command with arg1: " + argument1 + " and arg2: " + arg2Value).setEphemeral(true).queue(); + } + + @TabComplete(command="testannoted", arg="arg2") + public List tabCompleteArg2(SlashCommandInteractionEvent event, String currentInput) { + List suggestions = Arrays.asList("option1", "option2", "option3"); + return suggestions.stream() + .filter(option -> option.startsWith(currentInput)) + .collect(Collectors.toList()); + } + +} From e45dfc08daf81608ff04a2769c8b58072694e8e9 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Fri, 2 Jan 2026 18:46:51 +0100 Subject: [PATCH 22/23] feat: add autocompletion for jda (rewrite to wrap event) --- .../traqueur/commands/api/CommandManager.java | 26 ++++ .../fr/traqueur/commands/test/TestBot.java | 14 +- .../commands/test/commands/AdminCommand.java | 18 ++- .../commands/test/commands/GreetCommand.java | 6 +- .../commands/test/commands/MathCommand.java | 14 +- .../commands/test/commands/PingCommand.java | 6 +- .../test/commands/UserInfoCommand.java | 6 +- .../annoted/HierarchicalCommands.java | 137 +++++++++++++++++ .../annoted/OptionalArgsCommands.java | 76 ++++++++++ .../annoted/SimpleAnnotatedCommands.java | 77 ++++++++++ .../commands/annoted/TabCompleteCommands.java | 120 +++++++++++++++ .../commands/annoted/TestAnnotedCommands.java | 6 +- .../fr/traqueur/commands/jda/Command.java | 45 +++--- .../traqueur/commands/jda/CommandManager.java | 5 +- .../commands/jda/JDAArgumentParser.java | 12 +- .../fr/traqueur/commands/jda/JDAExecutor.java | 143 +++++++++++++++--- .../commands/jda/JDAInteractionContext.java | 77 ++++++++++ .../fr/traqueur/commands/jda/JDAPlatform.java | 81 +++++++--- .../commands/jda/JDASenderResolver.java | 31 ++-- .../jda/requirements/GuildRequirement.java | 12 +- .../requirements/PermissionRequirement.java | 12 +- .../jda/requirements/RoleRequirement.java | 10 +- .../fr/traqueur/testplugin/TestPlugin.java | 18 +++ .../annoted/HierarchicalCommands.java | 70 +++++++++ .../annoted/OptionalArgsCommands.java | 48 ++++++ .../annoted/SimpleAnnotatedCommands.java | 43 ++++++ .../annoted/TabCompleteCommands.java | 92 +++++++++++ .../VelocityTestPlugin.java | 18 +++ .../annoted/HierarchicalCommands.java | 78 ++++++++++ .../annoted/OptionalArgsCommands.java | 54 +++++++ .../annoted/SimpleAnnotatedCommands.java | 46 ++++++ .../annoted/TabCompleteCommands.java | 81 ++++++++++ 32 files changed, 1347 insertions(+), 135 deletions(-) create mode 100644 jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/HierarchicalCommands.java create mode 100644 jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/OptionalArgsCommands.java create mode 100644 jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/SimpleAnnotatedCommands.java create mode 100644 jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TabCompleteCommands.java create mode 100644 jda/src/main/java/fr/traqueur/commands/jda/JDAInteractionContext.java create mode 100644 spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/HierarchicalCommands.java create mode 100644 spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/OptionalArgsCommands.java create mode 100644 spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/SimpleAnnotatedCommands.java create mode 100644 spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/TabCompleteCommands.java create mode 100644 velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/HierarchicalCommands.java create mode 100644 velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/OptionalArgsCommands.java create mode 100644 velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/SimpleAnnotatedCommands.java create mode 100644 velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/TabCompleteCommands.java 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 8d6fc96..a3782e2 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -245,6 +245,32 @@ 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. * diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java index 7b00f87..6e86487 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/TestBot.java @@ -2,11 +2,11 @@ import fr.traqueur.commands.annotations.AnnotationCommandProcessor; import fr.traqueur.commands.jda.CommandManager; +import fr.traqueur.commands.jda.JDAInteractionContext; import fr.traqueur.commands.test.commands.*; -import fr.traqueur.commands.test.commands.annoted.TestAnnotedCommands; +import fr.traqueur.commands.test.commands.annoted.*; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.requests.GatewayIntent; import java.util.logging.Logger; @@ -38,12 +38,16 @@ public TestBot(String token) throws InterruptedException { CommandManager commandManager = new CommandManager<>(this, jda, LOGGER); commandManager.setDebug(true); - AnnotationCommandProcessor annotationProcessor = + AnnotationCommandProcessor annotationProcessor = new AnnotationCommandProcessor<>(commandManager); - // Register commands - LOGGER.info("Registering commands..."); + // Register annotated commands + LOGGER.info("Registering annotated commands..."); annotationProcessor.register(new TestAnnotedCommands()); + annotationProcessor.register(new SimpleAnnotatedCommands()); + annotationProcessor.register(new OptionalArgsCommands()); + annotationProcessor.register(new TabCompleteCommands()); + annotationProcessor.register(new HierarchicalCommands()); commandManager.registerCommand(new PingCommand(this)); commandManager.registerCommand(new UserInfoCommand(this)); diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java index 65725cd..54c1edb 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/AdminCommand.java @@ -2,6 +2,7 @@ import fr.traqueur.commands.jda.Command; import fr.traqueur.commands.jda.JDAArguments; +import fr.traqueur.commands.jda.JDAInteractionContext; import fr.traqueur.commands.test.TestBot; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -36,7 +37,7 @@ public AdminCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { // This won't be called since we have subcommands } @@ -50,7 +51,7 @@ public UsersGroupCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { // This won't be called since we have subcommands } } @@ -67,7 +68,7 @@ public KickCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { JDAArguments jdaArgs = jda(arguments); User user = jdaArgs.get("user"); String reason = jdaArgs.getOptional("reason").orElse("No reason provided"); @@ -89,7 +90,7 @@ public BanCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { User user = arguments.get("user"); String reason = arguments.getOptional("reason").orElse("No reason provided"); @@ -108,7 +109,7 @@ public ServerGroupCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { // This won't be called since we have subcommands } } @@ -123,7 +124,8 @@ public ServerInfoCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); JDAArguments jdaArgs = jda(arguments); if (event.getGuild() == null) { @@ -159,7 +161,7 @@ public ServerSettingsCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { JDAArguments jdaArgs = jda(arguments); String option = jdaArgs.get("option"); String value = jdaArgs.get("value"); @@ -167,4 +169,4 @@ public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) jdaArgs.reply(String.format("Would set setting '%s' to '%s'", option, value)); } } -} \ No newline at end of file +} diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/GreetCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/GreetCommand.java index fb69ec4..5a7e840 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/GreetCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/GreetCommand.java @@ -3,6 +3,7 @@ import fr.traqueur.commands.api.arguments.Infinite; import fr.traqueur.commands.jda.Command; import fr.traqueur.commands.jda.JDAArguments; +import fr.traqueur.commands.jda.JDAInteractionContext; import fr.traqueur.commands.test.TestBot; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -21,7 +22,8 @@ public GreetCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); User target = arguments.get("user"); String customMessage = arguments.getOptional("message").orElse("Hello there!"); @@ -33,4 +35,4 @@ public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) arguments.reply(greeting); } -} \ No newline at end of file +} diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/MathCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/MathCommand.java index 7e0f2ad..8961903 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/MathCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/MathCommand.java @@ -2,8 +2,8 @@ import fr.traqueur.commands.jda.Command; import fr.traqueur.commands.jda.JDAArguments; +import fr.traqueur.commands.jda.JDAInteractionContext; import fr.traqueur.commands.test.TestBot; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; /** * Math command with subcommands demonstrating the command tree structure. @@ -27,7 +27,7 @@ public MathCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { // This won't be called since we have subcommands } @@ -42,7 +42,7 @@ public AddCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { double a = arguments.get("a"); double b = arguments.get("b"); double result = a + b; @@ -62,7 +62,7 @@ public SubtractCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { double a = arguments.get("a"); double b = arguments.get("b"); double result = a - b; @@ -82,7 +82,7 @@ public MultiplyCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { double a = arguments.get("a"); double b = arguments.get("b"); double result = a * b; @@ -102,7 +102,7 @@ public DivideCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { double a = arguments.get("a"); double b = arguments.get("b"); @@ -115,4 +115,4 @@ public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) jda(arguments).reply(String.format("%.2f ÷ %.2f = %.2f", a, b, result)); } } -} \ No newline at end of file +} diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/PingCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/PingCommand.java index 0db9158..a78f930 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/PingCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/PingCommand.java @@ -2,6 +2,7 @@ import fr.traqueur.commands.jda.Command; import fr.traqueur.commands.jda.JDAArguments; +import fr.traqueur.commands.jda.JDAInteractionContext; import fr.traqueur.commands.test.TestBot; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -16,7 +17,8 @@ public PingCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); long gatewayPing = event.getJDA().getGatewayPing(); event.reply("Pong! Gateway ping: " + gatewayPing + "ms").queue(response -> { @@ -27,4 +29,4 @@ public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) }); }); } -} \ No newline at end of file +} diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/UserInfoCommand.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/UserInfoCommand.java index b22c1dc..f86aceb 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/UserInfoCommand.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/UserInfoCommand.java @@ -2,6 +2,7 @@ import fr.traqueur.commands.jda.Command; import fr.traqueur.commands.jda.JDAArguments; +import fr.traqueur.commands.jda.JDAInteractionContext; import fr.traqueur.commands.test.TestBot; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.IMentionable; @@ -25,7 +26,8 @@ public UserInfoCommand(TestBot bot) { } @Override - public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) { + public void execute(JDAInteractionContext context, JDAArguments arguments) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); JDAArguments jdaArgs = jda(arguments); // Get the target user (defaults to the command executor) @@ -53,4 +55,4 @@ public void execute(SlashCommandInteractionEvent event, JDAArguments arguments) event.replyEmbeds(embed.build()).queue(); } -} \ No newline at end of file +} diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/HierarchicalCommands.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/HierarchicalCommands.java new file mode 100644 index 0000000..54ff724 --- /dev/null +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/HierarchicalCommands.java @@ -0,0 +1,137 @@ +package fr.traqueur.commands.test.commands.annoted; + +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import fr.traqueur.commands.jda.JDAInteractionContext; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.awt.Color; +import java.time.Instant; +import java.util.Optional; + +@CommandContainer +public class HierarchicalCommands { + + // Moderation commands - using dotted names for hierarchy + @Command(name = "moderation.timeout", description = "Timeout a user", permission = "MODERATE_MEMBERS") + public void moderationTimeout(JDAInteractionContext context, + @Arg("user") Member member, + @Arg("minutes") int minutes, + @Arg("reason") Optional reason) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + String timeoutReason = reason.orElse("No reason provided"); + + if (minutes < 1 || minutes > 40320) { // Discord max is 28 days + event.reply("Timeout duration must be between 1 minute and 28 days (40320 minutes)!") + .setEphemeral(true) + .queue(); + return; + } + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("⏱️ User Timed Out") + .setColor(Color.ORANGE) + .addField("User", member.getAsMention(), true) + .addField("Duration", minutes + " minutes", true) + .addField("Reason", timeoutReason, false) + .addField("Moderator", event.getUser().getAsMention(), true) + .setTimestamp(Instant.now()); + + event.replyEmbeds(embed.build()).queue(); + // Note: Actual timeout would require: member.timeoutFor(Duration.ofMinutes(minutes)).queue(); + } + + @Command(name = "moderation.warn", description = "Warn a user", permission = "MODERATE_MEMBERS") + public void moderationWarn(JDAInteractionContext context, + @Arg("user") Member member, + @Arg("reason") Optional reason) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + String warnReason = reason.orElse("No reason provided"); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("⚠️ User Warned") + .setColor(Color.YELLOW) + .addField("User", member.getAsMention(), true) + .addField("Reason", warnReason, false) + .addField("Moderator", event.getUser().getAsMention(), true) + .setTimestamp(Instant.now()); + + event.replyEmbeds(embed.build()).queue(); + } + + @Command(name = "moderation.clear", description = "Clear messages", permission = "MANAGE_MESSAGES") + public void moderationClear(JDAInteractionContext context, @Arg("amount") int amount) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + if (amount < 1 || amount > 100) { + event.reply("Amount must be between 1 and 100!").setEphemeral(true).queue(); + return; + } + + event.reply("Would clear " + amount + " messages (not implemented in test bot)") + .setEphemeral(true) + .queue(); + } + + // Configuration commands - multi-level hierarchy + @Command(name = "botconfig.prefix", description = "Change bot prefix") + public void configPrefix(JDAInteractionContext context, @Arg("new_prefix") String newPrefix) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + event.reply("✅ Prefix changed to: `" + newPrefix + "` (not actually saved in test bot)") + .setEphemeral(true) + .queue(); + } + + @Command(name = "botconfig.language", description = "Change bot language") + public void configLanguage(JDAInteractionContext context, @Arg("lang") String language) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + event.reply("✅ Language changed to: " + language + " (not actually saved in test bot)") + .setEphemeral(true) + .queue(); + } + + @Command(name = "botconfig.reset", description = "Reset all settings") + public void configReset(JDAInteractionContext context) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + event.reply("✅ All settings have been reset to defaults! (not actually reset in test bot)") + .setEphemeral(true) + .queue(); + } + + // Deep hierarchy example + @Command(name = "system.server.restart", description = "Restart the server", permission = "ADMINISTRATOR") + public void systemServerRestart(JDAInteractionContext context) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + event.reply("🔄 Server restart initiated! (not actually restarting in test bot)") + .setEphemeral(true) + .queue(); + } + + @Command(name = "system.server.backup", description = "Create a server backup", permission = "ADMINISTRATOR") + public void systemServerBackup(JDAInteractionContext context) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + event.reply("💾 Creating server backup... (not actually creating backup in test bot)") + .setEphemeral(true) + .queue(); + } + + @Command(name = "system.logs.view", description = "View server logs", permission = "ADMINISTRATOR") + public void systemLogsView(JDAInteractionContext context, + @Arg("lines") Optional lines) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + int numLines = lines.orElse(10); + event.reply("📋 Viewing last " + numLines + " log lines... (not implemented in test bot)") + .setEphemeral(true) + .queue(); + } + + @Command(name = "system.logs.clear", description = "Clear server logs", permission = "ADMINISTRATOR") + public void systemLogsClear(JDAInteractionContext context) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + event.reply("🗑️ Server logs cleared! (not actually cleared in test bot)") + .setEphemeral(true) + .queue(); + } +} diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/OptionalArgsCommands.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/OptionalArgsCommands.java new file mode 100644 index 0000000..6f6bf06 --- /dev/null +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/OptionalArgsCommands.java @@ -0,0 +1,76 @@ +package fr.traqueur.commands.test.commands.annoted; + +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import fr.traqueur.commands.jda.JDAInteractionContext; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.emoji.Emoji; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.awt.Color; +import java.time.Instant; +import java.util.Optional; + +@CommandContainer +public class OptionalArgsCommands { + + @Command(name = "announce", description = "Make an announcement") + public void announce(JDAInteractionContext context, + @Arg("message") String message, + @Arg("title") Optional title, + @Arg("color") Optional colorHex) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + Color color; + try { + color = colorHex.map(hex -> Color.decode(hex.startsWith("#") ? hex : "#" + hex)) + .orElse(Color.BLUE); + } catch (NumberFormatException e) { + color = Color.BLUE; + } + + EmbedBuilder embed = new EmbedBuilder() + .setTitle(title.orElse("Announcement")) + .setDescription(message) + .setColor(color) + .setTimestamp(Instant.now()) + .setFooter("Announced by " + event.getUser().getName(), event.getUser().getEffectiveAvatarUrl()); + + event.reply("Announcement sent!").setEphemeral(true).queue(); + event.getChannel().sendMessageEmbeds(embed.build()).queue(); + } + + @Command(name = "poll", description = "Create a simple poll") + public void poll(JDAInteractionContext context, + @Arg("question") String question, + @Arg("duration") Optional durationMinutes) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + int duration = durationMinutes.orElse(5); + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("📊 Poll") + .setDescription(question) + .setColor(Color.ORANGE) + .setFooter("Poll will close in " + duration + " minutes") + .setTimestamp(Instant.now()); + + event.replyEmbeds(embed.build()).queue(response -> { + response.retrieveOriginal().queue(message -> { + message.addReaction(Emoji.fromUnicode("👍")).queue(); + message.addReaction(Emoji.fromUnicode("👎")).queue(); + }); + }); + } + + @Command(name = "remind", description = "Set a reminder") + public void remind(JDAInteractionContext context, + @Arg("message") String message, + @Arg("minutes") Optional minutes) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + int time = minutes.orElse(5); + event.reply("⏰ I'll remind you in " + time + " minutes about: " + message) + .setEphemeral(true) + .queue(); + // Note: Actual reminder implementation would require a scheduler + } +} diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/SimpleAnnotatedCommands.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/SimpleAnnotatedCommands.java new file mode 100644 index 0000000..a8b009a --- /dev/null +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/SimpleAnnotatedCommands.java @@ -0,0 +1,77 @@ +package fr.traqueur.commands.test.commands.annoted; + +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import fr.traqueur.commands.jda.JDAInteractionContext; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.awt.Color; +import java.time.Instant; + +@CommandContainer +public class SimpleAnnotatedCommands { + + @Command(name = "echo", description = "Echo back a message") + public void echo(JDAInteractionContext context, @Arg("message") String message) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + event.reply("You said: " + message).setEphemeral(true).queue(); + } + + @Command(name = "serverinfo", description = "Get information about this server") + public void serverInfo(JDAInteractionContext context) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + if (event.getGuild() == null) { + event.reply("This command can only be used in a server!").setEphemeral(true).queue(); + return; + } + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("Server Information") + .setColor(Color.BLUE) + .addField("Server Name", event.getGuild().getName(), true) + .addField("Server ID", event.getGuild().getId(), true) + .addField("Owner", event.getGuild().getOwner() != null ? event.getGuild().getOwner().getAsMention() : "Unknown", true) + .addField("Member Count", String.valueOf(event.getGuild().getMemberCount()), true) + .addField("Boost Level", String.valueOf(event.getGuild().getBoostTier()), true) + .addField("Created", event.getGuild().getTimeCreated().toString(), false) + .setThumbnail(event.getGuild().getIconUrl()) + .setTimestamp(Instant.now()); + + event.replyEmbeds(embed.build()).queue(); + } + + @Command(name = "avatar", description = "Get a user's avatar") + public void avatar(JDAInteractionContext context, @Arg("user") net.dv8tion.jda.api.entities.User user) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + String avatarUrl = user.getEffectiveAvatarUrl() + "?size=512"; + + EmbedBuilder embed = new EmbedBuilder() + .setTitle(user.getName() + "'s Avatar") + .setImage(avatarUrl) + .setColor(Color.CYAN) + .setTimestamp(Instant.now()); + + event.replyEmbeds(embed.build()).queue(); + } + + @Command(name = "roll", description = "Roll a dice") + public void roll(JDAInteractionContext context, @Arg("sides") int sides) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + if (sides < 2 || sides > 1000) { + event.reply("Please choose a dice with 2-1000 sides!").setEphemeral(true).queue(); + return; + } + + int result = (int) (Math.random() * sides) + 1; + event.reply("🎲 You rolled a **" + result + "** (d" + sides + ")").queue(); + } + + @Command(name = "coinflip", description = "Flip a coin") + public void coinFlip(JDAInteractionContext context) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + String result = Math.random() < 0.5 ? "Heads" : "Tails"; + event.reply("🪙 The coin landed on: **" + result + "**").queue(); + } +} diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TabCompleteCommands.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TabCompleteCommands.java new file mode 100644 index 0000000..ec8ada4 --- /dev/null +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TabCompleteCommands.java @@ -0,0 +1,120 @@ +package fr.traqueur.commands.test.commands.annoted; + +import fr.traqueur.commands.annotations.*; +import fr.traqueur.commands.jda.JDAInteractionContext; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; + +import java.awt.Color; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@CommandContainer +public class TabCompleteCommands { + + @Command(name = "color", description = "Choose a color and see it displayed") + @Alias(value = {"colour"}) + public void color(JDAInteractionContext context, @Arg("color") String colorName) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + Color color; + String colorDisplay; + + switch (colorName.toLowerCase()) { + case "red": + color = Color.RED; + colorDisplay = "Red 🔴"; + break; + case "blue": + color = Color.BLUE; + colorDisplay = "Blue 🔵"; + break; + case "green": + color = Color.GREEN; + colorDisplay = "Green 🟢"; + break; + case "yellow": + color = Color.YELLOW; + colorDisplay = "Yellow 🟡"; + break; + case "purple": + color = new Color(128, 0, 128); + colorDisplay = "Purple 🟣"; + break; + case "orange": + color = Color.ORANGE; + colorDisplay = "Orange 🟠"; + break; + default: + event.reply("Invalid color! Choose from: red, blue, green, yellow, purple, orange") + .setEphemeral(true) + .queue(); + return; + } + + EmbedBuilder embed = new EmbedBuilder() + .setTitle("Color Display") + .setDescription("You selected: " + colorDisplay) + .setColor(color); + + event.replyEmbeds(embed.build()).queue(); + } + + @TabComplete(command = "color", arg = "color") + public List completeColor(JDAInteractionContext context, String current) { + List colors = Arrays.asList("red", "blue", "green", "yellow", "purple", "orange"); + return colors.stream() + .filter(c -> c.startsWith(current.toLowerCase())) + .collect(Collectors.toList()); + } + + @Command(name = "language", description = "Set your preferred language") + public void language(JDAInteractionContext context, @Arg("lang") String language) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + String message; + switch (language.toLowerCase()) { + case "english": + message = "Language set to English! 🇬🇧"; + break; + case "french": + message = "Langue définie sur Français! 🇫🇷"; + break; + case "spanish": + message = "¡Idioma establecido en Español! 🇪🇸"; + break; + case "german": + message = "Sprache auf Deutsch eingestellt! 🇩🇪"; + break; + case "japanese": + message = "言語を日本語に設定しました!🇯🇵"; + break; + default: + event.reply("Unsupported language!").setEphemeral(true).queue(); + return; + } + event.reply(message).queue(); + } + + @TabComplete(command = "language", arg = "lang") + public List completeLanguage(JDAInteractionContext context, String current) { + return Arrays.asList("english", "french", "spanish", "german", "japanese").stream() + .filter(lang -> lang.startsWith(current.toLowerCase())) + .collect(Collectors.toList()); + } + + @Command(name = "category", description = "Browse categories") + public void category(JDAInteractionContext context, @Arg("name") String categoryName) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + EmbedBuilder embed = new EmbedBuilder() + .setTitle("Category: " + categoryName) + .setDescription("Viewing category: " + categoryName) + .setColor(Color.CYAN); + + event.replyEmbeds(embed.build()).queue(); + } + + @TabComplete(command = "category", arg = "name") + public List completeCategory() { + return Arrays.asList("gaming", "music", "art", "programming", "sports", "movies"); + } +} diff --git a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TestAnnotedCommands.java b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TestAnnotedCommands.java index 1f82aeb..b69a303 100644 --- a/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TestAnnotedCommands.java +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TestAnnotedCommands.java @@ -1,6 +1,7 @@ package fr.traqueur.commands.test.commands.annoted; import fr.traqueur.commands.annotations.*; +import fr.traqueur.commands.jda.JDAInteractionContext; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.util.Arrays; @@ -13,13 +14,14 @@ public class TestAnnotedCommands { @Command(name = "testannoted", description = "A test annoted command", usage = "/testannoted") @Alias(value = {"ta", "testa"}) - public void testAnnotedCommand(SlashCommandInteractionEvent event, @Arg("arg1") int argument1, @Arg("arg2") Optional argument2) { + public void testAnnotedCommand(JDAInteractionContext context, @Arg("arg1") int argument1, @Arg("arg2") Optional argument2) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); String arg2Value = argument2.orElse("no value provided"); event.reply("You executed the testannoted command with arg1: " + argument1 + " and arg2: " + arg2Value).setEphemeral(true).queue(); } @TabComplete(command="testannoted", arg="arg2") - public List tabCompleteArg2(SlashCommandInteractionEvent event, String currentInput) { + public List tabCompleteArg2(JDAInteractionContext context, String currentInput) { List suggestions = Arrays.asList("option1", "option2", "option3"); return suggestions.stream() .filter(option -> option.startsWith(currentInput)) diff --git a/jda/src/main/java/fr/traqueur/commands/jda/Command.java b/jda/src/main/java/fr/traqueur/commands/jda/Command.java index b5c58ee..afd29c4 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/Command.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/Command.java @@ -5,7 +5,6 @@ import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; /** * Abstract base class for JDA slash commands. @@ -13,7 +12,7 @@ * * @param The type of the bot instance. */ -public abstract class Command extends fr.traqueur.commands.api.models.Command { +public abstract class Command extends fr.traqueur.commands.api.models.Command { /** * Constructor for a JDA command. @@ -29,13 +28,13 @@ public Command(T bot, String name) { * Abstract method to execute the command. * Must be implemented by subclasses to define command behavior. * - * @param event The slash command interaction event. + * @param context The JDA interaction context. * @param arguments The JDA-specific arguments for the command. */ - public abstract void execute(SlashCommandInteractionEvent event, JDAArguments arguments); + public abstract void execute(JDAInteractionContext context, JDAArguments arguments); - public void execute(SlashCommandInteractionEvent event, Arguments arguments) { - this.execute(event, jda(arguments)); + public void execute(JDAInteractionContext context, Arguments arguments) { + this.execute(context, jda(arguments)); } /** @@ -50,42 +49,42 @@ protected JDAArguments jda(Arguments arguments) { } /** - * Helper method to get the event's user. + * Helper method to get the context's user. * - * @param event The slash command event. + * @param context The JDA interaction context. * @return The user who triggered the command. */ - protected User getUser(SlashCommandInteractionEvent event) { - return event.getUser(); + protected User getUser(JDAInteractionContext context) { + return context.getUser(); } /** - * Helper method to get the event's member (null if not in a guild). + * Helper method to get the context's member (null if not in a guild). * - * @param event The slash command event. + * @param context The JDA interaction context. * @return The member who triggered the command, or null if not in a guild. */ - protected Member getMember(SlashCommandInteractionEvent event) { - return event.getMember(); + protected Member getMember(JDAInteractionContext context) { + return context.getMember(); } /** - * Helper method to get the event's guild (null if not in a guild). + * Helper method to get the context's guild (null if not in a guild). * - * @param event The slash command event. + * @param context The JDA interaction context. * @return The guild where the command was triggered, or null if not in a guild. */ - protected Guild getGuild(SlashCommandInteractionEvent event) { - return event.getGuild(); + protected Guild getGuild(JDAInteractionContext context) { + return context.getGuild(); } /** - * Helper method to get the event's channel. + * Helper method to get the context's channel. * - * @param event The slash command event. + * @param context The JDA interaction context. * @return The channel where the command was triggered. */ - protected MessageChannelUnion getChannel(SlashCommandInteractionEvent event) { - return event.getChannel(); + protected MessageChannelUnion getChannel(JDAInteractionContext context) { + return context.getChannel(); } -} \ No newline at end of file +} diff --git a/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java b/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java index c01773b..2ac4a3c 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/CommandManager.java @@ -1,7 +1,6 @@ package fr.traqueur.commands.jda; import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.util.logging.Logger; @@ -11,7 +10,7 @@ * * @param The type of the bot instance. */ -public class CommandManager extends fr.traqueur.commands.api.CommandManager { +public class CommandManager extends fr.traqueur.commands.api.CommandManager { /** * The JDA platform instance. @@ -74,4 +73,4 @@ public JDA getJDA() { public JDAPlatform getJDAPlatform() { return jdaPlatform; } -} \ No newline at end of file +} diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java index 04175ce..07144c1 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java @@ -21,14 +21,14 @@ * Discord handles type resolution natively, so no string conversion needed. */ public record JDAArgumentParser( - Logger logger) implements ArgumentParser { + Logger logger) implements ArgumentParser { @Override - public ParseResult parse(Command command, SlashCommandInteractionEvent event) { + public ParseResult parse(Command command, SlashCommandInteractionEvent event) { JDAArguments arguments = new JDAArguments(logger, event); List options = event.getOptions(); - List> allArgs = new ArrayList<>(); + List> allArgs = new ArrayList<>(); allArgs.addAll(command.getArgs()); allArgs.addAll(command.getOptionalArgs()); @@ -56,7 +56,7 @@ public ParseResult parse(Command command, Slash // Parse each option for (int i = 0; i < options.size(); i++) { OptionMapping option = options.get(i); - Argument arg = allArgs.get(i); + Argument arg = allArgs.get(i); try { populateArgument(arguments, option, arg); @@ -74,7 +74,7 @@ public ParseResult parse(Command command, Slash } private void populateArgument(JDAArguments arguments, OptionMapping option, - Argument arg) { + Argument arg) { String name = option.getName(); switch (option.getType()) { @@ -129,4 +129,4 @@ private void populateArgument(JDAArguments arguments, OptionMapping option, default -> { /* Unknown type, skip */ } } } -} \ No newline at end of file +} diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java index 56a5417..2549745 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAExecutor.java @@ -1,25 +1,30 @@ package fr.traqueur.commands.jda; import fr.traqueur.commands.api.CommandManager; +import fr.traqueur.commands.api.arguments.Argument; +import fr.traqueur.commands.api.arguments.TabCompleter; import fr.traqueur.commands.api.models.Command; import fr.traqueur.commands.api.models.collections.CommandTree; import fr.traqueur.commands.api.parsing.ParseResult; import fr.traqueur.commands.api.requirements.Requirement; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.commands.Command.Choice; import org.jetbrains.annotations.NotNull; +import java.util.List; import java.util.Optional; /** - * JDA executor that handles slash command events. + * JDA executor that handles slash command and autocomplete events. */ public class JDAExecutor extends ListenerAdapter { - private final CommandManager commandManager; + private final CommandManager commandManager; private final JDAArgumentParser parser; - public JDAExecutor(CommandManager commandManager) { + public JDAExecutor(CommandManager commandManager) { this.commandManager = commandManager; this.parser = new JDAArgumentParser<>(commandManager.getLogger()); } @@ -32,9 +37,12 @@ public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent even commandManager.getLogger().info("Received slash command: " + label); } + // Wrap the event + JDAInteractionContext context = JDAInteractionContext.wrap(event); + // Find command String[] labelParts = label.split("\\."); - Optional> found = + Optional> found = commandManager.getCommands().findNode(labelParts); if (found.isEmpty()) { @@ -42,14 +50,14 @@ public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent even return; } - Command command = found.get().node().getCommand().orElse(null); + Command command = found.get().node().getCommand().orElse(null); if (command == null) { event.reply("Command implementation not found!").setEphemeral(true).queue(); return; } // Validate - if (!validateCommand(event, command)) { + if (!validateCommand(context, event, command)) { return; } @@ -64,7 +72,7 @@ public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent even } try { - command.execute(event, result.arguments()); + command.execute(context, result.arguments()); } catch (Exception e) { commandManager.getLogger().error("Error executing command " + label + ": " + e.getMessage()); if (!event.isAcknowledged()) { @@ -73,8 +81,9 @@ public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent even } } - private boolean validateCommand(SlashCommandInteractionEvent event, - Command command) { + private boolean validateCommand(JDAInteractionContext context, + SlashCommandInteractionEvent event, + Command command) { // Enabled check if (!command.isEnabled()) { event.reply(commandManager.getMessageHandler().getCommandDisabledMessage()) @@ -91,15 +100,15 @@ private boolean validateCommand(SlashCommandInteractionEvent event, // Permission check String perm = command.getPermission(); - if (!perm.isEmpty() && !commandManager.getPlatform().hasPermission(event, perm)) { + if (!perm.isEmpty() && !commandManager.getPlatform().hasPermission(context, perm)) { event.reply(commandManager.getMessageHandler().getNoPermissionMessage()) .setEphemeral(true).queue(); return false; } // Requirements check - for (Requirement req : command.getRequirements()) { - if (!req.check(event)) { + for (Requirement req : command.getRequirements()) { + if (!req.check(context)) { String msg = req.errorMessage().isEmpty() ? commandManager.getMessageHandler().getRequirementMessage() .replace("%requirement%", req.getClass().getSimpleName()) @@ -112,13 +121,113 @@ private boolean validateCommand(SlashCommandInteractionEvent event, return true; } + @Override + public void onCommandAutoCompleteInteraction(@NotNull CommandAutoCompleteInteractionEvent event) { + String label = buildLabel(event); + String focusedOptionName = event.getFocusedOption().getName(); + + if (commandManager.isDebug()) { + commandManager.getLogger().info("Received autocomplete for: " + label + " arg: " + focusedOptionName); + } + + // Wrap the event + JDAInteractionContext context = JDAInteractionContext.wrap(event); + + // Find command + String[] labelParts = label.split("\\."); + Optional> found = + commandManager.getCommands().findNode(labelParts); + + if (found.isEmpty()) { + event.replyChoices(List.of()).queue(); + return; + } + + Command command = found.get().node().getCommand().orElse(null); + if (command == null) { + event.replyChoices(List.of()).queue(); + return; + } + + // Find the argument being completed + Argument targetArg = null; + for (Argument arg : command.getArgs()) { + if (arg.name().equals(focusedOptionName)) { + targetArg = arg; + break; + } + } + if (targetArg == null) { + for (Argument arg : command.getOptionalArgs()) { + if (arg.name().equals(focusedOptionName)) { + targetArg = arg; + break; + } + } + } + + if (targetArg == null) { + event.replyChoices(List.of()).queue(); + return; + } + + // Get the TabCompleter + TabCompleter completer = getTabCompleter(targetArg); + + if (completer == null) { + event.replyChoices(List.of()).queue(); + return; + } + + // Invoke the completer + try { + String currentInput = event.getFocusedOption().getValue(); + List suggestions = completer.onCompletion(context, + List.of(currentInput)); + + // Convert to Discord choices (max 25) + List choices = suggestions.stream() + .limit(25) + .map(s -> new Choice(s, s)) + .toList(); + + event.replyChoices(choices).queue(); + + } catch (Exception e) { + commandManager.getLogger().error("Error during autocomplete: " + e.getMessage()); + event.replyChoices(List.of()).queue(); + } + } + + /** + * Get the TabCompleter for an argument (custom or general). + */ + private TabCompleter getTabCompleter(Argument arg) { + // Check for custom TabCompleter on the argument + if (arg.tabCompleter() != null) { + return arg.tabCompleter(); + } + + // Check for general TabCompleter for this type + return commandManager.getTabCompleterForType(arg.type().key()); + } + private String buildLabel(SlashCommandInteractionEvent event) { - StringBuilder label = new StringBuilder(event.getName()); - if (event.getSubcommandGroup() != null) { - label.append(".").append(event.getSubcommandGroup()); + return buildLabel(event.getName(), event.getSubcommandGroup(), event.getSubcommandName()); + } + + private String buildLabel(CommandAutoCompleteInteractionEvent event) { + return buildLabel(event.getName(), event.getSubcommandGroup(), event.getSubcommandName()); + } + + @NotNull + private String buildLabel(String name, String subcommandGroup, String subcommandName) { + StringBuilder label = new StringBuilder(name); + if (subcommandGroup != null) { + label.append(".").append(subcommandGroup); } - if (event.getSubcommandName() != null) { - label.append(".").append(event.getSubcommandName()); + if (subcommandName != null) { + label.append(".").append(subcommandName); } return label.toString().toLowerCase(); } diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAInteractionContext.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAInteractionContext.java new file mode 100644 index 0000000..e57e100 --- /dev/null +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAInteractionContext.java @@ -0,0 +1,77 @@ +package fr.traqueur.commands.jda; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.Interaction; + +/** + * Wrapper interface that provides common access to both SlashCommandInteractionEvent + * and CommandAutoCompleteInteractionEvent for use in TabCompleters. + */ +public interface JDAInteractionContext { + + User getUser(); + Member getMember(); + Guild getGuild(); + MessageChannelUnion getChannel(); + boolean isFromGuild(); + + /** + * Get the underlying interaction. + */ + Interaction getEvent(); + + /** + * Wrap a SlashCommandInteractionEvent. + */ + static JDAInteractionContext wrap(SlashCommandInteractionEvent event) { + return new JDAInteractionContext() { + @Override + public User getUser() { return event.getUser(); } + + @Override + public Member getMember() { return event.getMember(); } + + @Override + public Guild getGuild() { return event.getGuild(); } + + @Override + public MessageChannelUnion getChannel() { return event.getChannel(); } + + @Override + public boolean isFromGuild() { return event.isFromGuild(); } + + @Override + public Interaction getEvent() { return event; } + }; + } + + /** + * Wrap a CommandAutoCompleteInteractionEvent. + */ + static JDAInteractionContext wrap(CommandAutoCompleteInteractionEvent event) { + return new JDAInteractionContext() { + @Override + public User getUser() { return event.getUser(); } + + @Override + public Member getMember() { return event.getMember(); } + + @Override + public Guild getGuild() { return event.getGuild(); } + + @Override + public MessageChannelUnion getChannel() { return event.getChannel(); } + + @Override + public boolean isFromGuild() { return event.isFromGuild(); } + + @Override + public Interaction getEvent() { return event; } + }; + } +} \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java index a2ab1ce..7d97aeb 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java @@ -22,7 +22,7 @@ * * @param The type of the bot instance. */ -public class JDAPlatform implements CommandPlatform { +public class JDAPlatform implements CommandPlatform { /** * The bot instance associated with this platform. @@ -45,7 +45,7 @@ public class JDAPlatform implements CommandPlatform commandManager; + private CommandManager commandManager; /** * Constructor for JDAPlatform. @@ -67,7 +67,7 @@ public T getPlugin() { } @Override - public void injectManager(CommandManager commandManager) { + public void injectManager(CommandManager commandManager) { this.commandManager = commandManager; this.jda.addEventListener(new JDAExecutor<>(commandManager)); } @@ -78,7 +78,7 @@ public Logger getLogger() { } @Override - public boolean hasPermission(SlashCommandInteractionEvent sender, String permission) { + public boolean hasPermission(JDAInteractionContext sender, String permission) { if (sender.getMember() == null) { return false; } @@ -92,22 +92,24 @@ public boolean hasPermission(SlashCommandInteractionEvent sender, String permiss } @Override - public boolean isPlayer(SlashCommandInteractionEvent sender) { + public boolean isPlayer(JDAInteractionContext sender) { // In Discord context, we consider guild-only commands return sender.isFromGuild(); } @Override - public void sendMessage(SlashCommandInteractionEvent sender, String message) { - if (!sender.isAcknowledged()) { - sender.reply(message).queue(); - } else { - sender.getHook().sendMessage(message).queue(); + public void sendMessage(JDAInteractionContext sender, String message) { + if (sender.getEvent() instanceof SlashCommandInteractionEvent event) { + if (!event.isAcknowledged()) { + event.reply(message).queue(); + } else { + event.getHook().sendMessage(message).queue(); + } } } @Override - public void addCommand(Command command, String label) { + public void addCommand(Command command, String label) { String[] parts = label.split("\\."); String rootName = parts[0].toLowerCase(); @@ -190,7 +192,7 @@ public void removeCommand(String label, boolean subcommand) { } @Override - public SenderResolver getSenderResolver() { + public SenderResolver getSenderResolver() { return new JDASenderResolver(); } @@ -200,16 +202,16 @@ public SenderResolver getSenderResolver() { * @param slashCommand The slash command data. * @param command The command instance. */ - private void addArgumentsToCommand(SlashCommandData slashCommand, Command command) { - List> args = command.getArgs(); - List> optionalArgs = command.getOptionalArgs(); + private void addArgumentsToCommand(SlashCommandData slashCommand, Command command) { + List> args = command.getArgs(); + List> optionalArgs = command.getOptionalArgs(); - for (Argument arg : args) { + for (Argument arg : args) { OptionData option = createOptionData(arg, true); slashCommand.addOptions(option); } - for (Argument arg : optionalArgs) { + for (Argument arg : optionalArgs) { OptionData option = createOptionData(arg, false); slashCommand.addOptions(option); } @@ -221,16 +223,16 @@ private void addArgumentsToCommand(SlashCommandData slashCommand, Command command) { - List> args = command.getArgs(); - List> optionalArgs = command.getOptionalArgs(); + private void addArgumentsToSubcommand(SubcommandData subcommand, Command command) { + List> args = command.getArgs(); + List> optionalArgs = command.getOptionalArgs(); - for (Argument arg : args) { + for (Argument arg : args) { OptionData option = createOptionData(arg, true); subcommand.addOptions(option); } - for (Argument arg : optionalArgs) { + for (Argument arg : optionalArgs) { OptionData option = createOptionData(arg, false); subcommand.addOptions(option); } @@ -243,12 +245,41 @@ private void addArgumentsToSubcommand(SubcommandData subcommand, Command arg, boolean required) { + private OptionData createOptionData(Argument arg, boolean required) { String name = arg.name(); OptionType optionType = mapToOptionType(arg.type().key()); - return new OptionData(optionType, name, "Argument: " + name, required); + OptionData optionData = new OptionData(optionType, name, "Argument: " + name, required); + + // Enable autocomplete if: + // 1. The argument has a custom TabCompleter, OR + // 2. A general TabCompleter exists for this type + if (hasTabCompleter(arg)) { + optionData.setAutoComplete(true); + } + + return optionData; + } + + /** + * Check if an argument has a TabCompleter (either custom or general). + * + * @param arg The argument to check. + * @return true if a TabCompleter exists for this argument. + */ + private boolean hasTabCompleter(Argument arg) { + // Check for custom TabCompleter on the argument itself + if (arg.tabCompleter() != null) { + return true; + } + + // Check for general TabCompleter registered for this type + if (commandManager != null) { + return commandManager.hasTabCompleterForType(arg.type().key()); + } + + return false; } /** @@ -328,7 +359,7 @@ public JDA getJDA() { * * @return The command manager. */ - public CommandManager getCommandManager() { + public CommandManager getCommandManager() { return commandManager; } } \ No newline at end of file diff --git a/jda/src/main/java/fr/traqueur/commands/jda/JDASenderResolver.java b/jda/src/main/java/fr/traqueur/commands/jda/JDASenderResolver.java index 971588a..927894a 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDASenderResolver.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDASenderResolver.java @@ -4,29 +4,28 @@ import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; /** * Sender resolver for the JDA (Discord) platform. - * + * *

Resolves method parameter types to appropriate objects:

*
    - *
  • {@link SlashCommandInteractionEvent} → the raw event
  • - *
  • {@link User} → event.getUser()
  • - *
  • {@link Member} → event.getMember() (requires guild, gameOnly = true)
  • - *
  • {@link MessageChannelUnion} → event.getChannel()
  • + *
  • {@link JDAInteractionContext} → the interaction context
  • + *
  • {@link User} → context.getUser()
  • + *
  • {@link Member} → context.getMember() (requires guild, gameOnly = true)
  • + *
  • {@link MessageChannelUnion} → context.getChannel()
  • *
- * + * * @since 5.0.0 */ -public class JDASenderResolver implements SenderResolver { +public class JDASenderResolver implements SenderResolver { /** * {@inheritDoc} */ @Override public boolean canResolve(Class type) { - return SlashCommandInteractionEvent.class.isAssignableFrom(type) + return JDAInteractionContext.class.isAssignableFrom(type) || User.class.isAssignableFrom(type) || Member.class.isAssignableFrom(type) || MessageChannelUnion.class.isAssignableFrom(type); @@ -36,18 +35,18 @@ public boolean canResolve(Class type) { * {@inheritDoc} */ @Override - public Object resolve(SlashCommandInteractionEvent event, Class type) { - if (SlashCommandInteractionEvent.class.isAssignableFrom(type)) { - return event; + public Object resolve(JDAInteractionContext context, Class type) { + if (JDAInteractionContext.class.isAssignableFrom(type)) { + return context; } if (User.class.isAssignableFrom(type)) { - return event.getUser(); + return context.getUser(); } if (Member.class.isAssignableFrom(type)) { - return event.getMember(); // null if not in guild + return context.getMember(); // null if not in guild } if (MessageChannelUnion.class.isAssignableFrom(type)) { - return event.getChannel(); + return context.getChannel(); } return null; } @@ -60,4 +59,4 @@ public boolean isGameOnly(Class type) { // Member requires guild context (like Player requires game context) return Member.class.isAssignableFrom(type); } -} \ No newline at end of file +} diff --git a/jda/src/main/java/fr/traqueur/commands/jda/requirements/GuildRequirement.java b/jda/src/main/java/fr/traqueur/commands/jda/requirements/GuildRequirement.java index b962f9f..5e2ab56 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/requirements/GuildRequirement.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/requirements/GuildRequirement.java @@ -1,7 +1,7 @@ package fr.traqueur.commands.jda.requirements; import fr.traqueur.commands.api.requirements.Requirement; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import fr.traqueur.commands.jda.JDAInteractionContext; import java.util.Arrays; import java.util.Collection; @@ -9,7 +9,7 @@ /** * Requirement that checks if the command is executed in a specific guild. */ -public class GuildRequirement implements Requirement { +public class GuildRequirement implements Requirement { private final Collection guildIds; private final String errorMessage; @@ -36,15 +36,15 @@ public GuildRequirement(String errorMessage, Long... guildIds) { } @Override - public boolean check(SlashCommandInteractionEvent event) { - if (event.getGuild() == null) { + public boolean check(JDAInteractionContext context) { + if (context.getGuild() == null) { return false; } - return guildIds.contains(event.getGuild().getIdLong()); + return guildIds.contains(context.getGuild().getIdLong()); } @Override public String errorMessage() { return errorMessage; } -} \ No newline at end of file +} diff --git a/jda/src/main/java/fr/traqueur/commands/jda/requirements/PermissionRequirement.java b/jda/src/main/java/fr/traqueur/commands/jda/requirements/PermissionRequirement.java index 78a3eab..f9d3410 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/requirements/PermissionRequirement.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/requirements/PermissionRequirement.java @@ -1,8 +1,8 @@ package fr.traqueur.commands.jda.requirements; import fr.traqueur.commands.api.requirements.Requirement; +import fr.traqueur.commands.jda.JDAInteractionContext; import net.dv8tion.jda.api.Permission; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.util.Arrays; import java.util.Collection; @@ -10,7 +10,7 @@ /** * Requirement that checks if the user has specific Discord permissions. */ -public class PermissionRequirement implements Requirement { +public class PermissionRequirement implements Requirement { private final Collection permissions; private final String errorMessage; @@ -37,11 +37,11 @@ public PermissionRequirement(String errorMessage, Permission... permissions) { } @Override - public boolean check(SlashCommandInteractionEvent event) { - if (event.getMember() == null) { + public boolean check(JDAInteractionContext context) { + if (context.getMember() == null) { return false; } - return event.getMember().hasPermission(permissions); + return context.getMember().hasPermission(permissions); } @Override @@ -55,4 +55,4 @@ private String formatPermissions() { .reduce((a, b) -> a + ", " + b) .orElse(""); } -} \ No newline at end of file +} diff --git a/jda/src/main/java/fr/traqueur/commands/jda/requirements/RoleRequirement.java b/jda/src/main/java/fr/traqueur/commands/jda/requirements/RoleRequirement.java index 06d200e..d66bd51 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/requirements/RoleRequirement.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/requirements/RoleRequirement.java @@ -1,8 +1,8 @@ package fr.traqueur.commands.jda.requirements; import fr.traqueur.commands.api.requirements.Requirement; +import fr.traqueur.commands.jda.JDAInteractionContext; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.util.Arrays; import java.util.Collection; @@ -10,7 +10,7 @@ /** * Requirement that checks if the user has specific Discord roles. */ -public class RoleRequirement implements Requirement { +public class RoleRequirement implements Requirement { private final Collection roleIds; private final String errorMessage; @@ -41,8 +41,8 @@ public RoleRequirement(boolean requireAll, String errorMessage, Long... roleIds) } @Override - public boolean check(SlashCommandInteractionEvent event) { - Member member = event.getMember(); + public boolean check(JDAInteractionContext context) { + Member member = context.getMember(); if (member == null) { return false; } @@ -63,4 +63,4 @@ public boolean check(SlashCommandInteractionEvent event) { public String errorMessage() { return errorMessage; } -} \ No newline at end of file +} diff --git a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/TestPlugin.java b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/TestPlugin.java index 3695bb5..91d95cd 100644 --- a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/TestPlugin.java +++ b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/TestPlugin.java @@ -1,6 +1,9 @@ package fr.traqueur.testplugin; +import fr.traqueur.commands.annotations.AnnotationCommandProcessor; import fr.traqueur.commands.spigot.CommandManager; +import fr.traqueur.testplugin.annoted.*; +import org.bukkit.command.CommandSender; import org.bukkit.plugin.java.JavaPlugin; public final class TestPlugin extends JavaPlugin { @@ -9,7 +12,22 @@ public final class TestPlugin extends JavaPlugin { public void onEnable() { CommandManager commandManager = new CommandManager<>(this); commandManager.setDebug(true); + + // Create annotation processor + AnnotationCommandProcessor annotationProcessor = + new AnnotationCommandProcessor<>(commandManager); + + // Register annotated commands + getLogger().info("Registering annotated commands..."); + annotationProcessor.register(new SimpleAnnotatedCommands()); + annotationProcessor.register(new OptionalArgsCommands()); + annotationProcessor.register(new TabCompleteCommands()); + annotationProcessor.register(new HierarchicalCommands()); + + // Register traditional commands commandManager.registerCommand(new TestCommand(this)); + + getLogger().info("All commands registered successfully!"); } @Override diff --git a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/HierarchicalCommands.java b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/HierarchicalCommands.java new file mode 100644 index 0000000..3f8c7e4 --- /dev/null +++ b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/HierarchicalCommands.java @@ -0,0 +1,70 @@ +package fr.traqueur.testplugin.annoted; + +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Optional; + +@CommandContainer +public class HierarchicalCommands { + + @Command(name = "admin", description = "Admin management commands", permission = "testplugin.admin") + public void admin(Player sender) { + sender.sendMessage("§6=== Admin Commands ==="); + sender.sendMessage("§e/admin kick [reason] - Kick a player"); + sender.sendMessage("§e/admin ban [reason] - Ban a player"); + sender.sendMessage("§e/admin mute - Mute a player"); + } + + @Command(name = "admin.kick", description = "Kick a player", permission = "testplugin.admin.kick") + public void adminKick(Player sender, + @Arg("player") Player target, + @Arg("reason") Optional reason) { + String kickReason = reason.orElse("Kicked by an administrator"); + target.kickPlayer("§c" + kickReason); + sender.sendMessage("§a" + target.getName() + " has been kicked!"); + Bukkit.broadcastMessage("§e" + target.getName() + " was kicked by " + sender.getName()); + } + + @Command(name = "admin.ban", description = "Ban a player", permission = "testplugin.admin.ban") + public void adminBan(Player sender, + @Arg("player") Player target, + @Arg("reason") Optional reason) { + String banReason = reason.orElse("Banned by an administrator"); + Bukkit.getBanList(org.bukkit.BanList.Type.NAME).addBan(target.getName(), banReason, null, sender.getName()); + target.kickPlayer("§cYou have been banned!\n§7Reason: " + banReason); + sender.sendMessage("§a" + target.getName() + " has been banned!"); + Bukkit.broadcastMessage("§e" + target.getName() + " was banned by " + sender.getName()); + } + + @Command(name = "admin.mute", description = "Mute a player", permission = "testplugin.admin.mute") + public void adminMute(Player sender, @Arg("player") Player target) { + sender.sendMessage("§a" + target.getName() + " has been muted! (Feature not fully implemented)"); + } + + @Command(name = "config", description = "Configuration commands", permission = "testplugin.config") + public void config(Player sender) { + sender.sendMessage("§6=== Config Commands ==="); + sender.sendMessage("§e/config reload - Reload configuration"); + sender.sendMessage("§e/config reload all - Reload all configs"); + sender.sendMessage("§e/config reload messages - Reload messages"); + } + + @Command(name = "config.reload", description = "Reload configuration", permission = "testplugin.config.reload") + public void configReload(Player sender) { + sender.sendMessage("§aConfiguration reloaded!"); + } + + @Command(name = "config.reload.all", description = "Reload all configurations", permission = "testplugin.config.reload") + public void configReloadAll(Player sender) { + sender.sendMessage("§aAll configurations reloaded!"); + } + + @Command(name = "config.reload.messages", description = "Reload message configuration", permission = "testplugin.config.reload") + public void configReloadMessages(Player sender) { + sender.sendMessage("§aMessage configuration reloaded!"); + } +} \ No newline at end of file diff --git a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/OptionalArgsCommands.java b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/OptionalArgsCommands.java new file mode 100644 index 0000000..ec9a5a3 --- /dev/null +++ b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/OptionalArgsCommands.java @@ -0,0 +1,48 @@ +package fr.traqueur.testplugin.annoted; + +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.Optional; + +@CommandContainer +public class OptionalArgsCommands { + + @Command(name = "give", description = "Give items to a player", permission = "testplugin.give") + public void give(Player sender, + @Arg("item") Material material, + @Arg("amount") Optional amount, + @Arg("target") Optional target) { + Player recipient = target.orElse(sender); + int itemAmount = amount.orElse(1); + + if (itemAmount < 1 || itemAmount > 64) { + sender.sendMessage("§cAmount must be between 1 and 64!"); + return; + } + + ItemStack item = new ItemStack(material, itemAmount); + recipient.getInventory().addItem(item); + sender.sendMessage("§aGave " + itemAmount + " " + material.name() + " to " + recipient.getName()); + } + + @Command(name = "tp", description = "Teleport to a player or location", permission = "testplugin.tp") + public void teleport(Player sender, @Arg("target") Player target) { + sender.teleport(target.getLocation()); + sender.sendMessage("§aTeleported to " + target.getName()); + } + + @Command(name = "broadcast", description = "Broadcast a message", permission = "testplugin.broadcast") + public void broadcast(Player sender, + @Arg("message") String message, + @Arg("prefix") Optional prefix) { + String finalMessage = prefix.orElse("§6[BROADCAST]§r") + " " + message; + Bukkit.broadcastMessage(finalMessage); + sender.sendMessage("§aMessage broadcasted!"); + } +} \ No newline at end of file diff --git a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/SimpleAnnotatedCommands.java b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/SimpleAnnotatedCommands.java new file mode 100644 index 0000000..5ad2bba --- /dev/null +++ b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/SimpleAnnotatedCommands.java @@ -0,0 +1,43 @@ +package fr.traqueur.testplugin.annoted; + +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import org.bukkit.entity.Player; + +@CommandContainer +public class SimpleAnnotatedCommands { + + @Command(name = "heal", description = "Heal yourself or another player", permission = "testplugin.heal") + public void heal(Player sender, @Arg("target") Player target) { + target.setHealth(20.0); + target.setFoodLevel(20); + sender.sendMessage("§a" + target.getName() + " has been healed!"); + } + + @Command(name = "feed", description = "Feed yourself", permission = "testplugin.feed") + public void feed(Player sender) { + sender.setFoodLevel(20); + sender.setSaturation(20.0f); + sender.sendMessage("§aYou have been fed!"); + } + + @Command(name = "fly", description = "Toggle fly mode", permission = "testplugin.fly") + public void fly(Player sender) { + boolean canFly = !sender.getAllowFlight(); + sender.setAllowFlight(canFly); + sender.setFlying(canFly); + sender.sendMessage(canFly ? "§aFly mode enabled!" : "§cFly mode disabled!"); + } + + @Command(name = "speed", description = "Set your movement speed", permission = "testplugin.speed") + public void speed(Player sender, @Arg("speed") int speed) { + if (speed < 1 || speed > 10) { + sender.sendMessage("§cSpeed must be between 1 and 10!"); + return; + } + float walkSpeed = speed / 10.0f; + sender.setWalkSpeed(walkSpeed); + sender.sendMessage("§aWalk speed set to " + speed + "!"); + } +} \ No newline at end of file diff --git a/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/TabCompleteCommands.java b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/TabCompleteCommands.java new file mode 100644 index 0000000..0aaff6d --- /dev/null +++ b/spigot-test-plugin/src/main/java/fr/traqueur/testplugin/annoted/TabCompleteCommands.java @@ -0,0 +1,92 @@ +package fr.traqueur.testplugin.annoted; + +import fr.traqueur.commands.annotations.*; +import org.bukkit.GameMode; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@CommandContainer +public class TabCompleteCommands { + + @Command(name = "gamemode", description = "Change your gamemode", permission = "testplugin.gamemode") + @Alias(value = {"gm"}) + public void gamemode(Player sender, @Arg("mode") String mode) { + try { + GameMode gameMode = GameMode.valueOf(mode.toUpperCase()); + sender.setGameMode(gameMode); + sender.sendMessage("§aGamemode changed to " + gameMode.name()); + } catch (IllegalArgumentException e) { + sender.sendMessage("§cInvalid gamemode! Use: survival, creative, adventure, or spectator"); + } + } + + @TabComplete(command = "gamemode", arg = "mode") + public List completeGamemode(Player sender, String current) { + return Arrays.stream(GameMode.values()) + .map(gm -> gm.name().toLowerCase()) + .filter(name -> name.startsWith(current.toLowerCase())) + .collect(Collectors.toList()); + } + + @Command(name = "weather", description = "Change the weather", permission = "testplugin.weather") + public void weather(Player sender, @Arg("type") String weatherType) { + switch (weatherType.toLowerCase()) { + case "clear": + sender.getWorld().setStorm(false); + sender.getWorld().setThundering(false); + sender.sendMessage("§aWeather set to clear!"); + break; + case "rain": + sender.getWorld().setStorm(true); + sender.getWorld().setThundering(false); + sender.sendMessage("§aWeather set to rain!"); + break; + case "thunder": + sender.getWorld().setStorm(true); + sender.getWorld().setThundering(true); + sender.sendMessage("§aWeather set to thunder!"); + break; + default: + sender.sendMessage("§cInvalid weather type! Use: clear, rain, or thunder"); + } + } + + @TabComplete(command = "weather", arg = "type") + public List completeWeather(Player sender, String current) { + return Arrays.asList("clear", "rain", "thunder").stream() + .filter(type -> type.startsWith(current.toLowerCase())) + .collect(Collectors.toList()); + } + + @Command(name = "time", description = "Set the time", permission = "testplugin.time") + public void time(Player sender, @Arg("preset") String preset) { + long time; + switch (preset.toLowerCase()) { + case "day": + time = 1000; + break; + case "noon": + time = 6000; + break; + case "night": + time = 13000; + break; + case "midnight": + time = 18000; + break; + default: + sender.sendMessage("§cInvalid time preset! Use: day, noon, night, or midnight"); + return; + } + sender.getWorld().setTime(time); + sender.sendMessage("§aTime set to " + preset + "!"); + } + + @TabComplete(command = "time", arg = "preset") + public List completeTime() { + return Arrays.asList("day", "noon", "night", "midnight"); + } +} \ No newline at end of file diff --git a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/VelocityTestPlugin.java b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/VelocityTestPlugin.java index ec21270..1b633c4 100644 --- a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/VelocityTestPlugin.java +++ b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/VelocityTestPlugin.java @@ -1,11 +1,14 @@ package fr.traqueur.velocityTestPlugin; import com.google.inject.Inject; +import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.plugin.Plugin; import com.velocitypowered.api.proxy.ProxyServer; +import fr.traqueur.commands.annotations.AnnotationCommandProcessor; import fr.traqueur.commands.velocity.CommandManager; +import fr.traqueur.velocityTestPlugin.annoted.*; import org.slf4j.Logger; @Plugin( @@ -25,6 +28,21 @@ public class VelocityTestPlugin { public void onProxyInitialization(ProxyInitializeEvent event) { CommandManager commandManager = new CommandManager<>(this, server, java.util.logging.Logger.getLogger(ProxyServer.class.getName())); commandManager.setDebug(true); + + // Create annotation processor + AnnotationCommandProcessor annotationProcessor = + new AnnotationCommandProcessor<>(commandManager); + + // Register annotated commands + logger.info("Registering annotated commands..."); + annotationProcessor.register(new SimpleAnnotatedCommands()); + annotationProcessor.register(new OptionalArgsCommands()); + annotationProcessor.register(new TabCompleteCommands()); + annotationProcessor.register(new HierarchicalCommands()); + + // Register traditional commands commandManager.registerCommand(new TestCommand(this)); + + logger.info("All commands registered successfully!"); } } diff --git a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/HierarchicalCommands.java b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/HierarchicalCommands.java new file mode 100644 index 0000000..74b7930 --- /dev/null +++ b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/HierarchicalCommands.java @@ -0,0 +1,78 @@ +package fr.traqueur.velocityTestPlugin.annoted; + +import com.velocitypowered.api.command.CommandSource; +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Optional; + +@CommandContainer +public class HierarchicalCommands { + + @Command(name = "vproxy", description = "Proxy management commands", permission = "testplugin.proxy") + public void proxy(CommandSource sender) { + sender.sendMessage(Component.text("=== Proxy Commands ===", NamedTextColor.GOLD)); + sender.sendMessage(Component.text("/vproxy info - Show proxy information", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/vproxy servers - List all servers", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/vproxy players - List all players", NamedTextColor.YELLOW)); + } + + @Command(name = "vproxy.info", description = "Show proxy information", permission = "testplugin.proxy.info") + public void proxyInfo(CommandSource sender) { + sender.sendMessage(Component.text("=== Proxy Info ===", NamedTextColor.GOLD)); + sender.sendMessage(Component.text("Velocity Test Plugin", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("(Detailed info requires proxy instance)", NamedTextColor.GRAY)); + } + + @Command(name = "vproxy.servers", description = "List all servers", permission = "testplugin.proxy.servers") + public void proxyServers(CommandSource sender) { + sender.sendMessage(Component.text("=== Registered Servers ===", NamedTextColor.GOLD)); + sender.sendMessage(Component.text("(Server list requires proxy instance)", NamedTextColor.GRAY)); + } + + @Command(name = "vproxy.players", description = "List all players", permission = "testplugin.proxy.players") + public void proxyPlayers(CommandSource sender) { + sender.sendMessage(Component.text("=== Online Players ===", NamedTextColor.GOLD)); + sender.sendMessage(Component.text("(Player list requires proxy instance)", NamedTextColor.GRAY)); + } + + @Command(name = "vadmin", description = "Admin commands", permission = "testplugin.admin") + public void admin(CommandSource sender) { + sender.sendMessage(Component.text("=== Admin Commands ===", NamedTextColor.GOLD)); + sender.sendMessage(Component.text("/vadmin maintenance - Toggle maintenance mode", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/vadmin whitelist - Add player to whitelist", NamedTextColor.YELLOW)); + } + + @Command(name = "vadmin.maintenance", description = "Toggle maintenance mode", permission = "testplugin.admin.maintenance") + public void adminMaintenance(CommandSource sender, @Arg("mode") String mode) { + boolean enabled = mode.equalsIgnoreCase("on"); + sender.sendMessage(Component.text("Maintenance mode " + (enabled ? "enabled" : "disabled"), + enabled ? NamedTextColor.RED : NamedTextColor.GREEN)); + } + + @Command(name = "vadmin.whitelist", description = "Add player to whitelist", permission = "testplugin.admin.whitelist") + public void adminWhitelist(CommandSource sender, @Arg("player") String playerName) { + sender.sendMessage(Component.text(playerName + " added to whitelist!", NamedTextColor.GREEN)); + sender.sendMessage(Component.text("(Whitelist feature not fully implemented)", NamedTextColor.GRAY)); + } + + @Command(name = "vconfig", description = "Configuration commands", permission = "testplugin.config") + public void config(CommandSource sender) { + sender.sendMessage(Component.text("=== Config Commands ===", NamedTextColor.GOLD)); + sender.sendMessage(Component.text("/vconfig reload - Reload configuration", NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("/vconfig reload.all - Reload all configs", NamedTextColor.YELLOW)); + } + + @Command(name = "vconfig.reload", description = "Reload configuration", permission = "testplugin.config.reload") + public void configReload(CommandSource sender) { + sender.sendMessage(Component.text("Configuration reloaded!", NamedTextColor.GREEN)); + } + + @Command(name = "vconfig.reload.all", description = "Reload all configurations", permission = "testplugin.config.reload") + public void configReloadAll(CommandSource sender) { + sender.sendMessage(Component.text("All configurations reloaded!", NamedTextColor.GREEN)); + } +} \ No newline at end of file diff --git a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/OptionalArgsCommands.java b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/OptionalArgsCommands.java new file mode 100644 index 0000000..9617e17 --- /dev/null +++ b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/OptionalArgsCommands.java @@ -0,0 +1,54 @@ +package fr.traqueur.velocityTestPlugin.annoted; + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Optional; + +@CommandContainer +public class OptionalArgsCommands { + + @Command(name = "vkick", description = "Kick a player from the proxy", permission = "testplugin.kick") + public void kick(CommandSource sender, + @Arg("player") Player target, + @Arg("reason") Optional reason) { + String kickReason = reason.orElse("Kicked from the proxy"); + target.disconnect(Component.text(kickReason, NamedTextColor.RED)); + sender.sendMessage(Component.text(target.getUsername() + " has been kicked!", NamedTextColor.GREEN)); + } + + @Command(name = "vannounce", description = "Send an announcement", permission = "testplugin.announce") + public void announce(CommandSource sender, + @Arg("message") String message, + @Arg("color") Optional color) { + NamedTextColor textColor; + try { + textColor = color.map(c -> NamedTextColor.NAMES.value(c.toLowerCase())) + .orElse(NamedTextColor.YELLOW); + } catch (Exception e) { + textColor = NamedTextColor.YELLOW; + } + + Component announcement = Component.text("[ANNOUNCEMENT] ", NamedTextColor.GOLD) + .append(Component.text(message, textColor)); + + // This would broadcast to all players on the proxy + sender.sendMessage(Component.text("Announcement sent!", NamedTextColor.GREEN)); + sender.sendMessage(Component.text("(Broadcast requires proxy instance)", NamedTextColor.GRAY)); + } + + @Command(name = "vfind", description = "Find which server a player is on", permission = "testplugin.find") + public void find(CommandSource sender, @Arg("player") Player target) { + target.getCurrentServer().ifPresentOrElse( + server -> sender.sendMessage(Component.text(target.getUsername() + " is on: " + + server.getServerInfo().getName(), NamedTextColor.GREEN)), + () -> sender.sendMessage(Component.text(target.getUsername() + " is not connected to any server", + NamedTextColor.YELLOW)) + ); + } +} \ No newline at end of file diff --git a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/SimpleAnnotatedCommands.java b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/SimpleAnnotatedCommands.java new file mode 100644 index 0000000..9556613 --- /dev/null +++ b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/SimpleAnnotatedCommands.java @@ -0,0 +1,46 @@ +package fr.traqueur.velocityTestPlugin.annoted; + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +@CommandContainer +public class SimpleAnnotatedCommands { + + @Command(name = "vping", description = "Check proxy latency", permission = "testplugin.ping") + public void ping(Player player) { + long ping = player.getPing(); + player.sendMessage(Component.text("Your ping: " + ping + "ms", NamedTextColor.GREEN)); + } + + @Command(name = "vlist", description = "List online players", permission = "testplugin.list") + public void list(CommandSource sender) { + sender.sendMessage(Component.text("Online players:", NamedTextColor.GOLD)); + // This would list players from the proxy + sender.sendMessage(Component.text("Feature requires proxy instance", NamedTextColor.YELLOW)); + } + + @Command(name = "vinfo", description = "Get player info", permission = "testplugin.info") + public void info(CommandSource sender, @Arg("player") Player target) { + sender.sendMessage(Component.text("=== Player Info ===", NamedTextColor.GOLD)); + sender.sendMessage(Component.text("Name: " + target.getUsername(), NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("UUID: " + target.getUniqueId(), NamedTextColor.YELLOW)); + sender.sendMessage(Component.text("Ping: " + target.getPing() + "ms", NamedTextColor.YELLOW)); + target.getCurrentServer().ifPresent(server -> + sender.sendMessage(Component.text("Server: " + server.getServerInfo().getName(), NamedTextColor.YELLOW)) + ); + } + + @Command(name = "vmessage", description = "Send a message to a player", permission = "testplugin.message") + public void message(Player sender, @Arg("target") Player target, @Arg("message") String message) { + target.sendMessage(Component.text("[" + sender.getUsername() + " -> You] ", NamedTextColor.GRAY) + .append(Component.text(message, NamedTextColor.WHITE))); + sender.sendMessage(Component.text("[You -> " + target.getUsername() + "] ", NamedTextColor.GRAY) + .append(Component.text(message, NamedTextColor.WHITE))); + } +} \ No newline at end of file diff --git a/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/TabCompleteCommands.java b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/TabCompleteCommands.java new file mode 100644 index 0000000..bdd6aba --- /dev/null +++ b/velocity-test-plugin/src/main/java/fr/traqueur/velocityTestPlugin/annoted/TabCompleteCommands.java @@ -0,0 +1,81 @@ +package fr.traqueur.velocityTestPlugin.annoted; + +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.Player; +import fr.traqueur.commands.annotations.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@CommandContainer +public class TabCompleteCommands { + + @Command(name = "vserver", description = "Connect to a server", permission = "testplugin.server") + @Alias(value = {"vconnect", "vgo"}) + public void server(Player player, @Arg("server") String serverName) { + // This would connect the player to the specified server + player.sendMessage(Component.text("Connecting to " + serverName + "...", NamedTextColor.GREEN)); + player.sendMessage(Component.text("(Server connection requires proxy instance)", NamedTextColor.GRAY)); + } + + @TabComplete(command = "vserver", arg = "server") + public List completeServer(CommandSource sender, String current) { + // In a real scenario, this would get servers from the proxy + List servers = Arrays.asList("lobby", "survival", "creative", "minigames", "skyblock"); + return servers.stream() + .filter(server -> server.toLowerCase().startsWith(current.toLowerCase())) + .collect(Collectors.toList()); + } + + @Command(name = "vsend", description = "Send a player to a server", permission = "testplugin.send") + public void send(CommandSource sender, + @Arg("player") Player target, + @Arg("server") String serverName) { + sender.sendMessage(Component.text("Sending " + target.getUsername() + " to " + serverName, + NamedTextColor.GREEN)); + sender.sendMessage(Component.text("(Server send requires proxy instance)", NamedTextColor.GRAY)); + } + + @TabComplete(command = "vsend", arg = "server") + public List completeSendServer(CommandSource sender, String current) { + List servers = Arrays.asList("lobby", "survival", "creative", "minigames", "skyblock"); + return servers.stream() + .filter(server -> server.toLowerCase().startsWith(current.toLowerCase())) + .collect(Collectors.toList()); + } + + @Command(name = "valert", description = "Send an alert with a type", permission = "testplugin.alert") + public void alert(CommandSource sender, + @Arg("type") String alertType, + @Arg("message") String message) { + NamedTextColor color; + switch (alertType.toLowerCase()) { + case "info": + color = NamedTextColor.AQUA; + break; + case "warning": + color = NamedTextColor.YELLOW; + break; + case "error": + color = NamedTextColor.RED; + break; + case "success": + color = NamedTextColor.GREEN; + break; + default: + sender.sendMessage(Component.text("Invalid alert type!", NamedTextColor.RED)); + return; + } + + sender.sendMessage(Component.text("[" + alertType.toUpperCase() + "] ", color) + .append(Component.text(message, NamedTextColor.WHITE))); + } + + @TabComplete(command = "valert", arg = "type") + public List completeAlertType() { + return Arrays.asList("info", "warning", "error", "success"); + } +} \ No newline at end of file From 1e54dddfd7ce0dfff1a404af74dd548f5693c237 Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Fri, 2 Jan 2026 18:58:03 +0100 Subject: [PATCH 23/23] feat: fix test plugin annotations --- spigot-test-plugin/build.gradle | 2 +- velocity-test-plugin/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spigot-test-plugin/build.gradle b/spigot-test-plugin/build.gradle index 489fd90..4e222fe 100644 --- a/spigot-test-plugin/build.gradle +++ b/spigot-test-plugin/build.gradle @@ -28,7 +28,7 @@ tasks { dependencies { compileOnly("org.spigotmc:spigot-api:1.21.3-R0.1-SNAPSHOT") implementation project(":spigot") - implementation project(":core") + implementation project(":annotations-addon") } def targetJavaVersion = 21 diff --git a/velocity-test-plugin/build.gradle b/velocity-test-plugin/build.gradle index 0eecfd3..f66ac0d 100644 --- a/velocity-test-plugin/build.gradle +++ b/velocity-test-plugin/build.gradle @@ -22,8 +22,8 @@ repositories { dependencies { compileOnly("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT") annotationProcessor("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT") - implementation(project(":core")) implementation(project(":velocity")) + implementation project(":annotations-addon") } tasks {