From 072fea0b86275ef126d37948be255766a01028f7 Mon Sep 17 00:00:00 2001 From: Traqueur <54551467+Traqueur-dev@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:02:40 +0100 Subject: [PATCH 1/2] Feat/v5 (#47) * feat(arguments) : modernize argument system * feat(jda): remove duplication code Use ArgumentParser as interface * feat: optimization and aliases management and enable runtime command * feat: continue to use Pattern dot matching precompile * feat: add little cache system for player in arguments * feat: add fluent command builder possibility from manager * feat: force usage with manager for command builder * feat: add tests * feat: cleanup code * feat: add ci/cd for publishing * feat: remove jitpack * feat: update readme * fix: rename master branch in ci * fix(core): improve updater * feat: remove useless job * feat: add annotation addon * feat: rework tests * fix(spigot): spigot integration test * feat: remove useless test * feat: remove @optional form migration file * feat: add annoted command exemple in jda module * feat: add autocompletion for jda (rewrite to wrap event) * feat: fix test plugin annotations --- .github/workflows/build.yml | 30 ++ .github/workflows/test-all.yml | 46 -- MIGRATION_v4_to_v5.md | 413 ++++++++++++++++++ README.md | 21 +- annotations-addon/build.gradle | 4 + .../traqueur/commands/annotations/Alias.java | 39 ++ .../AnnotationCommandProcessor.java | 323 ++++++++++++++ .../fr/traqueur/commands/annotations/Arg.java | 40 ++ .../commands/annotations/Command.java | 61 +++ .../annotations/CommandContainer.java | 38 ++ .../commands/annotations/Infinite.java | 39 ++ .../commands/annotations/TabComplete.java | 58 +++ .../AnnotationCommandProcessorTest.java | 401 +++++++++++++++++ .../commands/AliasTestCommands.java | 31 ++ .../commands/HierarchicalTestCommands.java | 39 ++ .../commands/InfiniteArgsTestCommands.java | 32 ++ .../commands/InvalidContainerMissingArg.java | 13 + .../InvalidContainerNoAnnotation.java | 12 + .../commands/OptionalArgsTestCommands.java | 31 ++ .../commands/OrphanTestCommands.java | 30 ++ .../commands/SimpleTestCommands.java | 34 ++ .../commands/TabCompleteTestCommands.java | 56 +++ build.gradle | 159 ++++++- core/build.gradle | 19 - .../commands/CommandLookupBenchmark.java | 19 +- .../traqueur/commands/api/CommandManager.java | 368 +++++++--------- .../commands/api/arguments/Argument.java | 62 +-- .../api/arguments/ArgumentConverter.java | 16 + .../commands/api/arguments/ArgumentType.java | 41 ++ .../commands/api/arguments/ArgumentValue.java | 31 +- .../commands/api/arguments/Arguments.java | 320 ++------------ .../commands/api/arguments/Infinite.java | 4 + .../commands/api/arguments/TabCompleter.java | 6 +- .../ArgsWithInfiniteArgumentException.java | 1 + .../ArgumentIncorrectException.java | 3 +- .../exceptions/ArgumentNotExistException.java | 2 +- .../CommandRegistrationException.java | 2 +- .../TypeArgumentNotExistException.java | 2 +- .../UpdaterInitializationException.java | 2 +- .../traqueur/commands/api/logging/Logger.java | 2 + .../commands/api/logging/MessageHandler.java | 13 +- .../traqueur/commands/api/models/Command.java | 314 ++++++------- .../commands/api/models/CommandBuilder.java | 173 ++++++++ .../commands/api/models/CommandInvoker.java | 133 +++--- .../commands/api/models/CommandPlatform.java | 22 +- .../api/models/collections/CommandTree.java | 223 +++++----- .../commands/api/parsing/ArgumentParser.java | 22 + .../commands/api/parsing/ParseError.java | 35 ++ .../commands/api/parsing/ParseResult.java | 34 ++ .../api/requirements/Requirement.java | 5 +- .../commands/api/resolver/SenderResolver.java | 55 +++ .../commands/api/updater/Updater.java | 220 +++++++--- .../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 | 11 +- .../impl/parsing/DefaultArgumentParser.java | 135 ++++++ .../commands/api/CommandManagerTest.java | 149 ++++--- .../commands/api/arguments/ArgumentsTest.java | 169 ++++--- .../api/models/CommandBuilderTest.java | 365 ++++++++++++++++ .../api/models/CommandInvokerTest.java | 158 +++++-- .../commands/api/models/CommandTest.java | 181 ++++---- .../models/collections/CommandTreeTest.java | 80 ++-- .../commands/api/updater/UpdaterTest.java | 66 +-- .../impl/arguments/EnumArgumentTest.java | 3 +- .../logging/InternalMessageHandlerTest.java | 29 +- .../parsing/DefaultArgumentParserTest.java | 230 ++++++++++ .../test/mocks/MockCommandManager.java | 25 ++ .../commands/test/mocks/MockPlatform.java | 88 ++++ .../commands/test/mocks/MockPlayer.java | 9 + .../commands/test/mocks/MockSender.java | 10 + .../test/mocks/MockSenderResolver.java | 27 ++ gradle.properties | 2 +- jda-test-bot/build.gradle | 1 + .../fr/traqueur/commands/test/TestBot.java | 50 ++- .../commands/test/commands/AdminCommand.java | 53 +-- .../commands/test/commands/GreetCommand.java | 18 +- .../commands/test/commands/MathCommand.java | 32 +- .../commands/test/commands/PingCommand.java | 9 +- .../test/commands/UserInfoCommand.java | 14 +- .../annoted/HierarchicalCommands.java | 137 ++++++ .../annoted/OptionalArgsCommands.java | 76 ++++ .../annoted/SimpleAnnotatedCommands.java | 77 ++++ .../commands/annoted/TabCompleteCommands.java | 120 +++++ .../commands/annoted/TestAnnotedCommands.java | 31 ++ jda/build.gradle | 15 - .../fr/traqueur/commands/jda/Command.java | 45 +- .../traqueur/commands/jda/CommandManager.java | 30 +- .../commands/jda/JDAArgumentParser.java | 132 ++++++ .../traqueur/commands/jda/JDAArguments.java | 184 -------- .../fr/traqueur/commands/jda/JDAExecutor.java | 342 ++++++--------- .../commands/jda/JDAInteractionContext.java | 77 ++++ .../fr/traqueur/commands/jda/JDAPlatform.java | 115 +++-- .../commands/jda/JDASenderResolver.java | 62 +++ .../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 --- .../jda/requirements/GuildRequirement.java | 12 +- .../requirements/PermissionRequirement.java | 12 +- .../jda/requirements/RoleRequirement.java | 11 +- jitpack.yml | 6 - settings.gradle | 3 +- spigot-test-plugin/build.gradle | 2 +- .../traqueur/testplugin/Sub2TestCommand.java | 4 +- .../traqueur/testplugin/SubTestCommand.java | 14 +- .../fr/traqueur/testplugin/TestCommand.java | 4 +- .../fr/traqueur/testplugin/TestPlugin.java | 18 + .../annoted/HierarchicalCommands.java | 70 +++ .../annoted/OptionalArgsCommands.java | 48 ++ .../annoted/SimpleAnnotatedCommands.java | 43 ++ .../annoted/TabCompleteCommands.java | 92 ++++ spigot/build.gradle | 26 +- .../fr/traqueur/commands/spigot/Command.java | 2 +- .../commands/spigot/CommandManager.java | 1 + .../commands/spigot/SpigotExecutor.java | 25 +- .../commands/spigot/SpigotPlatform.java | 13 +- .../commands/spigot/SpigotSenderResolver.java | 54 +++ .../arguments/OfflinePlayerArgument.java | 41 +- .../spigot/arguments/PlayerArgument.java | 35 +- .../spigot/requirements/WorldRequirement.java | 26 +- .../spigot/requirements/ZoneRequirement.java | 78 ++-- .../spigot/SpigotIntegrationTest.java | 69 ++- velocity-test-plugin/build.gradle | 8 +- .../velocityTestPlugin/Sub2TestCommand.java | 4 +- .../velocityTestPlugin/SubTestCommand.java | 14 +- .../velocityTestPlugin/TestCommand.java | 4 +- .../VelocityTestPlugin.java | 32 +- .../annoted/HierarchicalCommands.java | 78 ++++ .../annoted/OptionalArgsCommands.java | 54 +++ .../annoted/SimpleAnnotatedCommands.java | 46 ++ .../annoted/TabCompleteCommands.java | 81 ++++ velocity/build.gradle | 22 - .../traqueur/commands/velocity/Command.java | 1 + .../commands/velocity/CommandManager.java | 5 +- .../commands/velocity/VelocityPlatform.java | 11 +- .../velocity/VelocitySenderResolver.java | 54 +++ 141 files changed, 6418 insertions(+), 2529 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/test-all.yml 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/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/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/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/models/CommandBuilder.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/api/resolver/SenderResolver.java create mode 100644 core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java 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 create mode 100644 core/src/test/java/fr/traqueur/commands/test/mocks/MockCommandManager.java create mode 100644 core/src/test/java/fr/traqueur/commands/test/mocks/MockPlatform.java 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 create mode 100644 core/src/test/java/fr/traqueur/commands/test/mocks/MockSenderResolver.java 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-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TestAnnotedCommands.java create mode 100644 jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java create mode 100644 jda/src/main/java/fr/traqueur/commands/jda/JDAInteractionContext.java create mode 100644 jda/src/main/java/fr/traqueur/commands/jda/JDASenderResolver.java 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 delete mode 100644 jitpack.yml 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 spigot/src/main/java/fr/traqueur/commands/spigot/SpigotSenderResolver.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 create mode 100644 velocity/src/main/java/fr/traqueur/commands/velocity/VelocitySenderResolver.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c0c1cd2 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: Build Action + +on: + push: + branches: [ master, develop ] + pull_request: + types: [ opened, synchronize, reopened ] + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + 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/.github/workflows/test-all.yml b/.github/workflows/test-all.yml deleted file mode 100644 index 45851f5..0000000 --- a/.github/workflows/test-all.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: CI - -on: - push: - branches: [ master ] - pull_request: - branches: [ master, develop ] - workflow_dispatch: - -jobs: - build: - 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 - - - name: Upload test reports - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: | - **/build/test-results/**/*.xml - **/build/reports/tests/** diff --git a/MIGRATION_v4_to_v5.md b/MIGRATION_v4_to_v5.md new file mode 100644 index 0000000..363c386 --- /dev/null +++ b/MIGRATION_v4_to_v5.md @@ -0,0 +1,413 @@ +# 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. 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 +- `@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) { + // 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 +@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. + +**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 + } +} +``` + +--- + +### 4. 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 + +--- + +### 5. 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` + +--- + +### 6. 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) + +✅ **Annotations Addon** - Créez des commandes par annotations (@Command, @Arg, @TabComplete) +✅ **ArgumentParser** - System de parsing extensible +✅ **CommandBuilder** - Alternative fluent à l'héritage +✅ **Cache PlayerArgument** - Autocomplétion optimisée +✅ **ParseResult/ParseError** - Gestion d'erreurs typée +✅ **SenderResolver** - Résolution automatique de types de sender +✅ **Updater async** - Vérification non-bloquante +✅ **Tests** - Coverage améliorée + Mocks partagés (core/test/mocks) + +## 🔄 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 + +--- + +## 🎯 3 Façons de Créer des Commandes en v5.0.0 + +### 1. Héritage Classique (v4 compatible) +```java +public class HelloCommand extends Command { + 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/README.md b/README.md index b8a5b41..b7d579c 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. --- @@ -47,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- } ``` @@ -60,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] @@ -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/annotations-addon/build.gradle b/annotations-addon/build.gradle new file mode 100644 index 0000000..af944bc --- /dev/null +++ b/annotations-addon/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':core') + testImplementation project(':core').sourceSets.test.output +} \ 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..18b2f5d --- /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}. + * + *

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..7bc2e83 --- /dev/null +++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java @@ -0,0 +1,323 @@ +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.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +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 + Map> builtCommands = new LinkedHashMap<>(); + Set rootCommands = new LinkedHashSet<>(); + + for (CommandMethodInfo info : commandMethods) { + String parentPath = getParentPath(info.name); + boolean hasParentInBatch = parentPath != null && allPaths.contains(parentPath); + + 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)) { + Command parent = builtCommands.get(parentPath); + Command child = builtCommands.get(info.name); + parent.addSubCommand(child); + } else { + rootCommands.add(info.name); + } + } + + // Fifth pass: register only root commands + 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); + + 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) { + 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); + 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(); + Type[] genericTypes = method.getGenericParameterTypes(); + + for (int i = 0; i < params.length; i++) { + Parameter param = params[i]; + Class paramType = param.getType(); + + // 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( + "Parameter '" + param.getName() + "' in method '" + method.getName() + + "' must be annotated with @Arg or be the sender type" + ); + } + + String argName = argAnnotation.value(); + 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); + } else { + builder.optionalArg(argName, argType); + } + } else { + if (completer != null) { + builder.arg(argName, argType, completer); + } else { + builder.arg(argName, argType); + } + } + } + } + + /** + * 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; + 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]; + Class paramType = param.getType(); + boolean isOptional = paramType == Optional.class; + + // First param: sender + if (i == 0) { + Class senderType = isOptional ? extractOptionalType(param) : paramType; + if (senderResolver.canResolve(senderType)) { + Object resolved = senderResolver.resolve(sender, senderType); + invokeArgs[i] = isOptional ? Optional.ofNullable(resolved) : resolved; + continue; + } + } + + // Other params: @Arg + Arg argAnnotation = param.getAnnotation(Arg.class); + if (argAnnotation != null) { + String argName = argAnnotation.value(); + + if (isOptional) { + invokeArgs[i] = args.getOptional(argName); + } else { + invokeArgs[i] = args.get(argName); + } + } + } + + method.invoke(handler, invokeArgs); + + } catch (Exception e) { + throw new RuntimeException("Failed to invoke command method: " + method.getName(), e); + } + } + + private record CommandMethodInfo(Object handler, Method method, String name) {} + private record TabCompleterMethod(Object handler, Method method) {} +} \ No newline at end of file diff --git a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Arg.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Arg.java new file mode 100644 index 0000000..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/TabComplete.java b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/TabComplete.java new file mode 100644 index 0000000..7848a55 --- /dev/null +++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/TabComplete.java @@ -0,0 +1,58 @@ +package fr.traqueur.commands.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines a tab completer for a specific argument of a command. + * + *

The annotated method must return {@code List} and can have + * the sender as first parameter, followed by the current input string.

+ * + *

Example:

+ *
{@code
+ * @Command(name = "warp")
+ * public void warp(Player sender, @Arg("name") String warpName) {
+ *     // teleport to warp
+ * }
+ * 
+ * @TabComplete(command = "warp", arg = "name")
+ * public List completeWarpName(Player sender, String current) {
+ *     return getWarps().stream()
+ *         .filter(w -> w.startsWith(current.toLowerCase()))
+ *         .toList();
+ * }
+ * }
+ * + *

For subcommands, use dot notation in the command parameter:

+ *
{@code
+ * @TabComplete(command = "warp.set", arg = "name")
+ * public List completeWarpSetName(Player sender, String current) {
+ *     // ...
+ * }
+ * }
+ * + * @since 5.0.0 + * @see Command + * @see Arg + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface TabComplete { + + /** + * The command name (or path for subcommands using dot notation). + * + * @return the command path + */ + String command(); + + /** + * The argument name to provide completions for. + * + * @return the argument name + */ + String arg(); +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java new file mode 100644 index 0000000..e548aac --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/AnnotationCommandProcessorTest.java @@ -0,0 +1,401 @@ +package fr.traqueur.commands.annotations; + +import fr.traqueur.commands.annotations.commands.*; +import fr.traqueur.commands.api.arguments.Argument; +import fr.traqueur.commands.api.models.Command; +import fr.traqueur.commands.test.mocks.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("AnnotationCommandProcessor") +class AnnotationCommandProcessorTest { + + private MockPlatform platform; + private AnnotationCommandProcessor processor; + + @BeforeEach + void setUp() { + platform = new MockPlatform(); + MockCommandManager manager = new MockCommandManager(platform); + processor = new AnnotationCommandProcessor<>(manager); + } + + @Nested + @DisplayName("Basic Registration") + class BasicRegistration { + + @Test + @DisplayName("should register simple command") + void shouldRegisterSimpleCommand() { + SimpleTestCommands commands = new SimpleTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("test")); + Command cmd = platform.getCommand("test"); + assertNotNull(cmd); + assertEquals("A test command", cmd.getDescription()); + assertEquals("test.use", cmd.getPermission()); + } + + @Test + @DisplayName("should register command with string argument") + void shouldRegisterCommandWithStringArg() { + SimpleTestCommands commands = new SimpleTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("greet")); + Command cmd = platform.getCommand("greet"); + + List> args = cmd.getArgs(); + assertEquals(1, args.size()); + assertEquals("name", args.get(0).name()); + } + + @Test + @DisplayName("should register command with multiple arguments") + void shouldRegisterCommandWithMultipleArgs() { + SimpleTestCommands commands = new SimpleTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("add")); + Command cmd = platform.getCommand("add"); + + List> args = cmd.getArgs(); + assertEquals(2, args.size()); + assertEquals("a", args.get(0).name()); + assertEquals("b", args.get(1).name()); + } + + @Test + @DisplayName("should register multiple handlers") + void shouldRegisterMultipleHandlers() { + SimpleTestCommands simple = new SimpleTestCommands(); + AliasTestCommands alias = new AliasTestCommands(); + + processor.register(simple, alias); + + assertTrue(platform.hasCommand("test")); + assertTrue(platform.hasCommand("greet")); + assertTrue(platform.hasCommand("gamemode")); + assertTrue(platform.hasCommand("teleport")); + } + } + + @Nested + @DisplayName("Hierarchical Commands") + class HierarchicalCommands { + + @Test + @DisplayName("should register parent command") + void shouldRegisterParentCommand() { + HierarchicalTestCommands commands = new HierarchicalTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("admin")); + } + + @Test + @DisplayName("should register first level subcommands") + void shouldRegisterFirstLevelSubcommands() { + HierarchicalTestCommands commands = new HierarchicalTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("admin.reload")); + assertTrue(platform.hasCommand("admin.info")); + } + + @Test + @DisplayName("should register second level subcommands") + void shouldRegisterSecondLevelSubcommands() { + HierarchicalTestCommands commands = new HierarchicalTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("admin.reload.config")); + assertTrue(platform.hasCommand("admin.reload.plugins")); + } + + @Test + @DisplayName("should add subcommands to parent") + void shouldAddSubcommandsToParent() { + HierarchicalTestCommands commands = new HierarchicalTestCommands(); + processor.register(commands); + + Command admin = platform.getCommand("admin"); + List> subcommands = admin.getSubcommands(); + assertEquals(2, subcommands.size()); + + Command reload = subcommands.stream() + .filter(c -> c.getName().equals("reload")) + .findFirst() + .orElse(null); + assertNotNull(reload); + assertEquals(2, reload.getSubcommands().size()); + } + + @Test + @DisplayName("should preserve permission on subcommands") + void shouldPreservePermissionOnSubcommands() { + HierarchicalTestCommands commands = new HierarchicalTestCommands(); + processor.register(commands); + + Command admin = platform.getCommand("admin"); + Command reload = admin.getSubcommands().stream() + .filter(c -> c.getName().equals("reload")) + .findFirst() + .orElse(null); + + assertNotNull(reload); + assertEquals("admin.reload", reload.getPermission()); + } + } + + @Nested + @DisplayName("Orphan Commands") + class OrphanCommands { + + @Test + @DisplayName("should register orphan subcommand directly") + void shouldRegisterOrphanSubcommandDirectly() { + OrphanTestCommands commands = new OrphanTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("warp.set")); + assertTrue(platform.hasCommand("warp.delete")); + } + + @Test + @DisplayName("should register deep orphan command") + void shouldRegisterDeepOrphanCommand() { + OrphanTestCommands commands = new OrphanTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("config.database.reset")); + } + + @Test + @DisplayName("orphan commands should be gameOnly when using MockPlayer") + void orphanCommandsShouldBeGameOnly() { + OrphanTestCommands commands = new OrphanTestCommands(); + processor.register(commands); + + Command warpSet = platform.getCommand("warp.set"); + assertTrue(warpSet.inGameOnly()); + } + } + + @Nested + @DisplayName("Optional Arguments") + class OptionalArguments { + + @Test + @DisplayName("should register command with optional argument") + void shouldRegisterCommandWithOptionalArg() { + OptionalArgsTestCommands commands = new OptionalArgsTestCommands(); + processor.register(commands); + + Command heal = platform.getCommand("heal"); + + assertEquals(0, heal.getArgs().size()); + assertEquals(1, heal.getOptionalArgs().size()); + assertEquals("target", heal.getOptionalArgs().get(0).name()); + } + + @Test + @DisplayName("should register command with mixed required and optional args") + void shouldRegisterMixedArgs() { + OptionalArgsTestCommands commands = new OptionalArgsTestCommands(); + processor.register(commands); + + Command give = platform.getCommand("give"); + + assertEquals(1, give.getArgs().size()); + assertEquals("item", give.getArgs().getFirst().name()); + + assertEquals(1, give.getOptionalArgs().size()); + assertEquals("amount", give.getOptionalArgs().getFirst().name()); + } + } + + @Nested + @DisplayName("Infinite Arguments") + class InfiniteArguments { + + @Test + @DisplayName("should register command with infinite argument") + void shouldRegisterCommandWithInfiniteArg() { + InfiniteArgsTestCommands commands = new InfiniteArgsTestCommands(); + processor.register(commands); + + Command broadcast = platform.getCommand("broadcast"); + + assertEquals(1, broadcast.getArgs().size()); + Argument arg = broadcast.getArgs().get(0); + assertEquals("message", arg.name()); + assertTrue(arg.type().isInfinite()); + } + + @Test + @DisplayName("should register command with optional infinite argument") + void shouldRegisterOptionalInfiniteArg() { + InfiniteArgsTestCommands commands = new InfiniteArgsTestCommands(); + processor.register(commands); + + Command kick = platform.getCommand("kick"); + + assertEquals(1, kick.getArgs().size()); + assertEquals(1, kick.getOptionalArgs().size()); + + Argument reason = kick.getOptionalArgs().get(0); + assertEquals("reason", reason.name()); + assertTrue(reason.type().isInfinite()); + } + } + + @Nested + @DisplayName("Aliases") + class Aliases { + + @Test + @DisplayName("should register command with single alias") + void shouldRegisterSingleAlias() { + AliasTestCommands commands = new AliasTestCommands(); + processor.register(commands); + + Command gamemode = platform.getCommand("gamemode"); + + List aliases = gamemode.getAliases(); + assertEquals(1, aliases.size()); + assertTrue(aliases.contains("gm")); + } + + @Test + @DisplayName("should register command with multiple aliases") + void shouldRegisterMultipleAliases() { + AliasTestCommands commands = new AliasTestCommands(); + processor.register(commands); + + Command teleport = platform.getCommand("teleport"); + + List aliases = teleport.getAliases(); + assertEquals(3, aliases.size()); + assertTrue(aliases.contains("tp")); + assertTrue(aliases.contains("tpto")); + assertTrue(aliases.contains("goto")); + } + + @Test + @DisplayName("all labels should be registered") + void allLabelsShouldBeRegistered() { + AliasTestCommands commands = new AliasTestCommands(); + processor.register(commands); + + assertTrue(platform.hasCommand("spawn")); + assertTrue(platform.hasCommand("hub")); + assertTrue(platform.hasCommand("lobby")); + assertTrue(platform.hasCommand("s")); + } + } + + @Nested + @DisplayName("Tab Completion") + class TabCompletion { + + @Test + @DisplayName("should register tab completer for argument") + void shouldRegisterTabCompleter() { + TabCompleteTestCommands commands = new TabCompleteTestCommands(); + processor.register(commands); + + Command world = platform.getCommand("world"); + + List> args = world.getArgs(); + assertEquals(1, args.size()); + assertNotNull(args.get(0).tabCompleter()); + } + } + + @Nested + @DisplayName("Game Only Detection") + class GameOnlyDetection { + + @Test + @DisplayName("should detect gameOnly when first param is MockPlayer") + void shouldDetectGameOnlyWithMockPlayer() { + AliasTestCommands commands = new AliasTestCommands(); + processor.register(commands); + + Command gamemode = platform.getCommand("gamemode"); + assertTrue(gamemode.inGameOnly()); + } + + @Test + @DisplayName("should not be gameOnly when first param is MockSender") + void shouldNotBeGameOnlyWithMockSender() { + SimpleTestCommands commands = new SimpleTestCommands(); + processor.register(commands); + + Command test = platform.getCommand("test"); + assertFalse(test.inGameOnly()); + } + } + + @Nested + @DisplayName("Error Handling") + class ErrorHandling { + + @Test + @DisplayName("should throw when class is not annotated with @CommandContainer") + void shouldThrowWhenMissingCommandContainer() { + InvalidContainerNoAnnotation invalid = new InvalidContainerNoAnnotation(); + + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> processor.register(invalid) + ); + + assertTrue(ex.getMessage().contains("@CommandContainer")); + } + + @Test + @DisplayName("should throw when parameter is missing @Arg annotation") + void shouldThrowWhenMissingArgAnnotation() { + InvalidContainerMissingArg invalid = new InvalidContainerMissingArg(); + + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> processor.register(invalid) + ); + + assertTrue(ex.getMessage().contains("@Arg")); + } + } + + @Nested + @DisplayName("Registration Order") + class RegistrationOrder { + + @Test + @DisplayName("commands should be sorted by depth (parents first)") + void commandsShouldBeSortedByDepth() { + HierarchicalTestCommands commands = new HierarchicalTestCommands(); + processor.register(commands); + + List labels = platform.getRegisteredLabels(); + + int adminIndex = labels.indexOf("admin"); + int adminReloadIndex = labels.indexOf("admin.reload"); + int adminReloadConfigIndex = labels.indexOf("admin.reload.config"); + + assertTrue(adminIndex < adminReloadIndex, + "admin should be registered before admin.reload"); + assertTrue(adminReloadIndex < adminReloadConfigIndex, + "admin.reload should be registered before admin.reload.config"); + } + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/AliasTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/AliasTestCommands.java new file mode 100644 index 0000000..890dd0d --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/AliasTestCommands.java @@ -0,0 +1,31 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.*; +import fr.traqueur.commands.test.mocks.*; + +import java.util.ArrayList; +import java.util.List; + +@CommandContainer +public class AliasTestCommands { + + public final List executedCommands = new ArrayList<>(); + + @Command(name = "gamemode", description = "Change game mode") + @Alias({"gm"}) + public void gamemode(MockPlayer sender, @Arg("mode") String mode) { + executedCommands.add("gamemode:" + mode); + } + + @Command(name = "teleport", description = "Teleport to player") + @Alias({"tp", "tpto", "goto"}) + public void teleport(MockPlayer sender, @Arg("target") String target) { + executedCommands.add("teleport:" + target); + } + + @Command(name = "spawn") + @Alias({"hub", "lobby", "s"}) + public void spawn(MockPlayer sender) { + executedCommands.add("spawn"); + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/HierarchicalTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/HierarchicalTestCommands.java new file mode 100644 index 0000000..86f9630 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/HierarchicalTestCommands.java @@ -0,0 +1,39 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import fr.traqueur.commands.test.mocks.*; + +import java.util.ArrayList; +import java.util.List; + +@CommandContainer +public class HierarchicalTestCommands { + + public final List executedCommands = new ArrayList<>(); + + @Command(name = "admin", description = "Admin commands") + public void admin(MockSender sender) { + executedCommands.add("admin"); + } + + @Command(name = "admin.reload", description = "Reload configuration", permission = "admin.reload") + public void adminReload(MockSender sender) { + executedCommands.add("admin.reload"); + } + + @Command(name = "admin.info", description = "Show server info") + public void adminInfo(MockSender sender) { + executedCommands.add("admin.info"); + } + + @Command(name = "admin.reload.config", description = "Reload config file") + public void adminReloadConfig(MockSender sender) { + executedCommands.add("admin.reload.config"); + } + + @Command(name = "admin.reload.plugins", description = "Reload plugins") + public void adminReloadPlugins(MockSender sender) { + executedCommands.add("admin.reload.plugins"); + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InfiniteArgsTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InfiniteArgsTestCommands.java new file mode 100644 index 0000000..516c7aa --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InfiniteArgsTestCommands.java @@ -0,0 +1,32 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import fr.traqueur.commands.annotations.Infinite; +import fr.traqueur.commands.test.mocks.MockSender; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@CommandContainer +public class InfiniteArgsTestCommands { + + public final List executedCommands = new ArrayList<>(); + public final List executedArgs = new ArrayList<>(); + + @Command(name = "broadcast", description = "Broadcast a message") + public void broadcast(MockSender sender, @Arg("message") @Infinite String message) { + executedCommands.add("broadcast"); + executedArgs.add(new Object[]{sender, message}); + } + + @Command(name = "kick", description = "Kick a player") + public void kick(MockSender sender, + @Arg("player") String player, + @Arg("reason") @Infinite Optional reason) { + executedCommands.add("kick"); + executedArgs.add(new Object[]{sender, player, reason.orElse(null)}); + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerMissingArg.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerMissingArg.java new file mode 100644 index 0000000..de44b04 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerMissingArg.java @@ -0,0 +1,13 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.*; +import fr.traqueur.commands.test.mocks.*; + +@CommandContainer +public class InvalidContainerMissingArg { + + @Command(name = "test") + public void test(MockSender sender, String missingArgAnnotation) { + // Should fail - second parameter has no @Arg + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerNoAnnotation.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerNoAnnotation.java new file mode 100644 index 0000000..305981d --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/InvalidContainerNoAnnotation.java @@ -0,0 +1,12 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.test.mocks.MockSender; + +// Missing @CommandContainer - should throw error +public class InvalidContainerNoAnnotation { + + @Command(name = "test") + public void test(MockSender sender) { + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OptionalArgsTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OptionalArgsTestCommands.java new file mode 100644 index 0000000..0405942 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OptionalArgsTestCommands.java @@ -0,0 +1,31 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import fr.traqueur.commands.test.mocks.MockPlayer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@CommandContainer +public class OptionalArgsTestCommands { + + public final List executedCommands = new ArrayList<>(); + public final List executedArgs = new ArrayList<>(); + + @Command(name = "heal", description = "Heal a player") + public void heal(MockPlayer sender, @Arg("target") Optional target) { + executedCommands.add("heal"); + executedArgs.add(new Object[]{sender, target.orElse(null)}); + } + + @Command(name = "give", description = "Give items") + public void give(MockPlayer sender, + @Arg("item") String item, + @Arg("amount") Optional amount) { + executedCommands.add("give"); + executedArgs.add(new Object[]{sender, item, amount.orElse(null)}); + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OrphanTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OrphanTestCommands.java new file mode 100644 index 0000000..43f632d --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/OrphanTestCommands.java @@ -0,0 +1,30 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.*; +import fr.traqueur.commands.test.mocks.*; + +import java.util.ArrayList; +import java.util.List; + +@CommandContainer +public class OrphanTestCommands { + + public final List executedCommands = new ArrayList<>(); + + // No "warp" parent defined - core will handle it + @Command(name = "warp.set", description = "Set a warp", permission = "warp.set") + public void warpSet(MockPlayer sender, @Arg("name") String name) { + executedCommands.add("warp.set:" + name); + } + + @Command(name = "warp.delete", description = "Delete a warp") + public void warpDelete(MockPlayer sender, @Arg("name") String name) { + executedCommands.add("warp.delete:" + name); + } + + // Deep orphan - no "config" or "config.database" defined + @Command(name = "config.database.reset", description = "Reset database") + public void configDatabaseReset(MockSender sender) { + executedCommands.add("config.database.reset"); + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/SimpleTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/SimpleTestCommands.java new file mode 100644 index 0000000..2786169 --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/SimpleTestCommands.java @@ -0,0 +1,34 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.Arg; +import fr.traqueur.commands.annotations.Command; +import fr.traqueur.commands.annotations.CommandContainer; +import fr.traqueur.commands.test.mocks.*; + +import java.util.ArrayList; +import java.util.List; + +@CommandContainer +public class SimpleTestCommands { + + public final List executedCommands = new ArrayList<>(); + public final List executedArgs = new ArrayList<>(); + + @Command(name = "test", description = "A test command", permission = "test.use") + public void testCommand(MockSender sender) { + executedCommands.add("test"); + executedArgs.add(new Object[]{sender}); + } + + @Command(name = "greet", description = "Greet someone") + public void greetCommand(MockSender sender, @Arg("name") String name) { + executedCommands.add("greet"); + executedArgs.add(new Object[]{sender, name}); + } + + @Command(name = "add", description = "Add two numbers") + public void addCommand(MockSender sender, @Arg("a") Integer a, @Arg("b") Integer b) { + executedCommands.add("add"); + executedArgs.add(new Object[]{sender, a, b}); + } +} \ No newline at end of file diff --git a/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/TabCompleteTestCommands.java b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/TabCompleteTestCommands.java new file mode 100644 index 0000000..a68c14b --- /dev/null +++ b/annotations-addon/src/test/java/fr/traqueur/commands/annotations/commands/TabCompleteTestCommands.java @@ -0,0 +1,56 @@ +package fr.traqueur.commands.annotations.commands; + +import fr.traqueur.commands.annotations.*; +import fr.traqueur.commands.test.mocks.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@CommandContainer +public class TabCompleteTestCommands { + + public final List executedCommands = new ArrayList<>(); + public final List tabCompleteInvocations = new ArrayList<>(); + + private final List availableWorlds = Arrays.asList("world", "world_nether", "world_the_end"); + private final List availableWarps = Arrays.asList("spawn", "shop", "arena", "hub"); + + @Command(name = "world", description = "Change world") + public void world(MockPlayer sender, @Arg("world") String world) { + executedCommands.add("world:" + world); + } + + @TabComplete(command = "world", arg = "world") + public List completeWorld(MockPlayer sender, String current) { + tabCompleteInvocations.add("world:" + current); + return availableWorlds.stream() + .filter(w -> w.toLowerCase().startsWith(current.toLowerCase())) + .toList(); + } + + @Command(name = "warp", description = "Warp to location") + public void warp(MockPlayer sender, @Arg("name") String name) { + executedCommands.add("warp:" + name); + } + + @TabComplete(command = "warp", arg = "name") + public List completeWarp(MockPlayer sender, String current) { + tabCompleteInvocations.add("warp:" + current); + return availableWarps.stream() + .filter(w -> w.toLowerCase().startsWith(current.toLowerCase())) + .toList(); + } + + // Tab completer without parameters + @Command(name = "gamemode", description = "Change gamemode") + public void gamemode(MockPlayer sender, @Arg("mode") String mode) { + executedCommands.add("gamemode:" + mode); + } + + @TabComplete(command = "gamemode", arg = "mode") + public List completeGamemode() { + tabCompleteInvocations.add("gamemode:no-args"); + return Arrays.asList("survival", "creative", "adventure", "spectator"); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index eb10988..d2e5a4b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,8 @@ +plugins { + id 'java-library' + id 'maven-publish' +} + allprojects { group = 'fr.traqueur.commands' version = property('version') @@ -6,8 +11,11 @@ allprojects { plugin 'java-library' } - tasks.withType(JavaCompile).configureEach { - options.compilerArgs += ['-nowarn'] + ext.classifier = System.getProperty('archive.classifier') + ext.sha = System.getProperty('github.sha') + + if (rootProject.ext.has('sha') && rootProject.ext.sha) { + version = rootProject.ext.sha } repositories { @@ -19,35 +27,140 @@ allprojects { } subprojects { - if (!project.name.contains('test-plugin')) { + if (project.name.contains('test-')) { + return; + } + + apply plugin: 'maven-publish' + + java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + withSourcesJar() + withJavadocJar() + } + + tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.compilerArgs.addAll([ + '-Xlint:all', + '-Xlint:-processing', + '-Xlint:-serial' + ]) + } + + tasks.withType(Javadoc).configureEach { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') + } + + dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' + testImplementation 'org.mockito:mockito-core:5.3.1' + } + test { + useJUnitPlatform() + jvmArgs += ['-XX:+EnableDynamicAgentLoading'] + reports { + html.required.set(true) + junitXml.required.set(true) + } - dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' - testImplementation 'org.mockito:mockito-core:5.3.1' + testLogging { + showStandardStreams = true + events("passed", "skipped", "failed", "standardOut", "standardError") + exceptionFormat = "full" } - test { - useJUnitPlatform() - jvmArgs += ['-XX:+EnableDynamicAgentLoading'] - reports { - html.required.set(true) - junitXml.required.set(true) + } + + publishing { + repositories { + maven { + def repository = System.getProperty('repository.name', 'snapshots') + def repoType = repository.toLowerCase() + + name = "groupez${repository.capitalize()}" + url = uri("https://repo.groupez.dev/${repoType}") + + credentials { + username = findProperty("${name}Username") ?: System.getenv('MAVEN_USERNAME') + password = findProperty("${name}Password") ?: System.getenv('MAVEN_PASSWORD') + } + + authentication { + create("basic", BasicAuthentication) + } } + } + + publications { + create('maven', MavenPublication) { + from components.java + + groupId = rootProject.group.toString() + + if (project.name == 'core') { + artifactId = 'core' + } else if (project.name == 'annotations-addon') { + artifactId = 'annotations-addon' + } else { + def platform = project.name.replaceFirst(/^platform-/, '') + artifactId = "platform-${platform}" + } - testLogging { - showStandardStreams = true - events("passed", "skipped", "failed", "standardOut", "standardError") - exceptionFormat "full" + version = rootProject.version.toString() + + pom { + name = artifactId + description = 'CommandsAPI - A flexible command framework for Java applications' + url = 'https://github.com/Traqueur-dev/CommandsAPI' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/licenses/MIT' + } + } + + developers { + developer { + id = 'traqueur' + name = 'Traqueur' + } + } + + scm { + connection = 'scm:git:git://github.com/Traqueur-dev/CommandsAPI.git' + developerConnection = 'scm:git:ssh://github.com/Traqueur-dev/CommandsAPI.git' + url = 'https://github.com/Traqueur-dev/CommandsAPI' + } + } } } } + } -tasks.register("testAll") { - group = "verification" - description = "Execute tous les tests des sous-projets qui ont une tâche 'test'" +tasks.register('publishAll') { + description = 'Publishes all subprojects to Maven repository' + group = 'publishing' - // Déclare la dépendance vers chaque tâche 'test' de chaque sous-projet - dependsOn subprojects - .findAll { it.tasks.findByName('test') != null } - .collect { it.tasks.named('test') } + subprojects.each { sub -> + sub.plugins.withId('maven-publish') { + dependsOn sub.tasks.named('publish') + } + } } + + +tasks.register('testAll') { + description = 'Runs all tests across all subprojects' + group = 'verification' + + subprojects.each { sub -> + sub.plugins.withId('java') { + dependsOn sub.tasks.named('test') + } + } +} + diff --git a/core/build.gradle b/core/build.gradle index 9f48b74..3cfd6a9 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,15 +1,7 @@ plugins { - id 'maven-publish' id("me.champeau.jmh") version "0.7.3" } -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - withSourcesJar() - withJavadocJar() -} - dependencies { jmh 'org.openjdk.jmh:jmh-core:1.37' jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' @@ -35,15 +27,4 @@ sourceSets { srcDir generatedResourcesDir } } -} - -publishing { - publications { - maven(MavenPublication) { - from components.java - groupId = project.group - artifactId = 'core' - version = project.version - } - } } \ No newline at end of file diff --git a/core/src/jmh/java/fr/traqueur/commands/CommandLookupBenchmark.java b/core/src/jmh/java/fr/traqueur/commands/CommandLookupBenchmark.java index eaed6a6..23efe55 100644 --- a/core/src/jmh/java/fr/traqueur/commands/CommandLookupBenchmark.java +++ b/core/src/jmh/java/fr/traqueur/commands/CommandLookupBenchmark.java @@ -4,7 +4,9 @@ import fr.traqueur.commands.api.models.collections.CommandTree; import org.openjdk.jmh.annotations.*; -import java.util.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; @@ -16,10 +18,10 @@ @OutputTimeUnit(TimeUnit.MILLISECONDS) public class CommandLookupBenchmark { - @Param({ "1000", "10000", "50000" }) + @Param({"1000", "10000", "50000"}) public int N; - @Param({ "1", "2", "3" }) + @Param({"1", "2", "3"}) public int maxDepth; private Map flatMap; @@ -29,7 +31,7 @@ public class CommandLookupBenchmark { @Setup(Level.Trial) public void setup() { flatMap = new HashMap<>(N); - tree = new CommandTree<>(); + tree = new CommandTree<>(); rawLabels = new String[N]; ThreadLocalRandom rnd = ThreadLocalRandom.current(); @@ -74,7 +76,12 @@ public CommandTree.MatchResult treeLookup() { } public static class DummyCommand extends Command { - public DummyCommand(String name) { super(null, name); } - @Override public void execute(Object s, fr.traqueur.commands.api.arguments.Arguments a) {} + public DummyCommand(String name) { + super(null, name); + } + + @Override + public void execute(Object s, fr.traqueur.commands.api.arguments.Arguments a) { + } } } diff --git a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java index acc91df..a3782e2 100644 --- a/core/src/main/java/fr/traqueur/commands/api/CommandManager.java +++ b/core/src/main/java/fr/traqueur/commands/api/CommandManager.java @@ -5,14 +5,17 @@ import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.api.arguments.TabCompleter; import fr.traqueur.commands.api.exceptions.ArgumentIncorrectException; -import fr.traqueur.commands.api.exceptions.CommandRegistrationException; import fr.traqueur.commands.api.exceptions.TypeArgumentNotExistException; import fr.traqueur.commands.api.logging.Logger; import fr.traqueur.commands.api.logging.MessageHandler; import fr.traqueur.commands.api.models.Command; +import fr.traqueur.commands.api.models.CommandBuilder; import fr.traqueur.commands.api.models.CommandInvoker; import fr.traqueur.commands.api.models.CommandPlatform; import fr.traqueur.commands.api.models.collections.CommandTree; +import fr.traqueur.commands.api.parsing.ArgumentParser; +import fr.traqueur.commands.api.parsing.ParseError; +import fr.traqueur.commands.api.parsing.ParseResult; import fr.traqueur.commands.api.updater.Updater; import fr.traqueur.commands.impl.arguments.BooleanArgument; import fr.traqueur.commands.impl.arguments.DoubleArgument; @@ -20,42 +23,33 @@ import fr.traqueur.commands.impl.arguments.LongArgument; import fr.traqueur.commands.impl.logging.InternalLogger; import fr.traqueur.commands.impl.logging.InternalMessageHandler; +import fr.traqueur.commands.impl.parsing.DefaultArgumentParser; import java.util.*; -import java.util.stream.Collectors; /** * This class is the command manager. * It allows you to register commands and subcommands. * It also allows you to register argument converters and tab completer. + * * @param The type of the platform that will use this command manager. * @param The type of the sender that will use this command manager. */ public abstract class CommandManager { - /** - * The parser used to separate the type of the argument from its name. - *

For example: "player:Player" will be parsed as "player" and "Player".

- */ - public static final String TYPE_PARSER = ":"; - /** - * The parser used to separate the infinite argument from its name. - *

For example: "args:infinite" will be parsed as "args" and "infinite".

- */ - private static final String INFINITE = "infinite"; - - private final CommandPlatform platform; + private final ArgumentParser parser; + private final CommandPlatform platform; /** * The commands registered in the command manager. */ - private final CommandTree commands; + private final CommandTree commands; /** * The argument converters registered in the command manager. */ - private final Map, ArgumentConverter>> typeConverters; + private final Map> typeConverters; /** * The tab completer registered in the command manager. @@ -63,7 +57,7 @@ public abstract class CommandManager { private final Map>> completers; - private final CommandInvoker invoker; + private final CommandInvoker invoker; /** * The message handler of the command manager. @@ -83,9 +77,10 @@ public abstract class CommandManager { /** * Create a new command manager. + * * @param platform The platform of the command manager. */ - public CommandManager(CommandPlatform platform) { + public CommandManager(CommandPlatform platform) { Updater.checkUpdates(); this.platform = platform; this.platform.injectManager(this); @@ -96,20 +91,22 @@ public CommandManager(CommandPlatform platform) { this.typeConverters = new HashMap<>(); this.completers = new HashMap<>(); this.invoker = new CommandInvoker<>(this); + this.parser = new DefaultArgumentParser<>(this.typeConverters, this.logger); this.registerInternalConverters(); } - /** - * Set the custom logger of the command manager. - * @param logger The logger to set. + * Get the message handler of the command manager. + * + * @return The message handler of the command manager. */ - public void setLogger(Logger logger) { - this.logger = logger; + public MessageHandler getMessageHandler() { + return messageHandler; } /** * Set the message handler of the command manager. + * * @param messageHandler The message handler to set. */ public void setMessageHandler(MessageHandler messageHandler) { @@ -117,46 +114,38 @@ public void setMessageHandler(MessageHandler messageHandler) { } /** - * Get the message handler of the command manager. - * @return The message handler of the command manager. + * Get the debug mode of the command manager. + * + * @return If the debug mode is enabled. */ - public MessageHandler getMessageHandler() { - return messageHandler; + public boolean isDebug() { + return this.debug; } /** * Set the debug mode of the command manager. + * * @param debug If the debug mode is enabled. */ public void setDebug(boolean debug) { this.debug = debug; } - /** - * Get the debug mode of the command manager. - * @return If the debug mode is enabled. - */ - public boolean isDebug() { - return this.debug; - } - /** * Register a command in the command manager. + * * @param command The command to register. */ - public void registerCommand(Command command) { - try { - for (String alias : command.getAliases()) { - this.addCommand(command, alias); - this.registerSubCommands(alias, command.getSubcommands()); - } - } catch(TypeArgumentNotExistException e) { - throw new CommandRegistrationException("Failed to register command: " + command.getClass().getSimpleName(), e); + public void registerCommand(Command command) { + for (String label : command.getAllLabels()) { + this.addCommand(command, label); + this.registerSubCommands(label, command.getSubcommands()); } } /** * Unregister a command in the command manager. + * * @param label The label of the command to unregister. */ public void unregisterCommand(String label) { @@ -165,15 +154,16 @@ public void unregisterCommand(String label) { /** * Unregister a command in the command manager. - * @param label The label of the command to unregister. + * + * @param label The label of the command to unregister. * @param subcommands If the subcommands must be unregistered. */ public void unregisterCommand(String label, boolean subcommands) { String[] rawArgs = label.split("\\."); - Optional> commandOptional = this.commands.findNode(rawArgs) - .flatMap(result -> result.node.getCommand()); + Optional> commandOptional = this.commands.findNode(rawArgs) + .flatMap(result -> result.node().getCommand()); - if (!commandOptional.isPresent()) { + if (commandOptional.isEmpty()) { throw new IllegalArgumentException("Command with label '" + label + "' does not exist."); } this.unregisterCommand(commandOptional.get(), subcommands); @@ -181,127 +171,145 @@ public void unregisterCommand(String label, boolean subcommands) { /** * Unregister a command in the command manager. + * * @param command The command to unregister. */ - public void unregisterCommand(Command command) { + public void unregisterCommand(Command command) { this.unregisterCommand(command, true); } /** * Unregister a command in the command manager. - * @param command The command to unregister. + * + * @param command The command to unregister. * @param subcommands If the subcommands must be unregistered. */ - public void unregisterCommand(Command command, boolean subcommands) { - List aliases = new ArrayList<>(command.getAliases()); - aliases.add(command.getName()); - for (String alias : aliases) { - this.removeCommand(alias, subcommands); - if(subcommands) { - this.unregisterSubCommands(alias, command.getSubcommands()); + public void unregisterCommand(Command command, boolean subcommands) { + List labels = new ArrayList<>(command.getAllLabels()); + for (String label : labels) { + this.removeCommand(label, subcommands); + if (subcommands) { + this.unregisterSubCommands(label, command.getSubcommands()); } } } /** * Register an argument converter in the command manager. + * * @param typeClass The class of the type. * @param converter The converter of the argument. - * @param The type of the argument. + * @param The type of the argument. */ public void registerConverter(Class typeClass, ArgumentConverter converter) { - this.typeConverters.put(typeClass.getSimpleName().toLowerCase(), new AbstractMap.SimpleEntry<>(typeClass, converter)); - } - - /** - * Register an argument converter in the command manager. - * @param typeClass The class of the type. - * @param type The type of the argument. - * @param converter The converter of the argument. - * @param The type of the argument. - */ - @Deprecated - public void registerConverter(Class typeClass, String type, ArgumentConverter converter) { - this.typeConverters.put(type, new AbstractMap.SimpleEntry<>(typeClass, converter)); + this.typeConverters.put(typeClass.getSimpleName().toLowerCase(), new ArgumentConverter.Wrapper<>(typeClass, converter)); } /** * Parse the arguments of the command. + * * @param command The command to parse. - * @param args The arguments to parse. + * @param args The arguments to parse. * @return The arguments parsed. * @throws TypeArgumentNotExistException If the type of the argument does not exist. - * @throws ArgumentIncorrectException If the argument is incorrect. - */ - public Arguments parse(Command command, String[] args) throws TypeArgumentNotExistException, ArgumentIncorrectException { - Arguments arguments = new Arguments(this.logger); - List> templates = command.getArgs(); - for (int i = 0; i < templates.size(); i++) { - String input = args[i]; - if (applyParsing(args, arguments, templates, i, input)) break; - } - - List> optArgs = command.getOptinalArgs(); - if (optArgs.isEmpty()) { - return arguments; - } - - for (int i = 0; i < optArgs.size(); i++) { - if (args.length > templates.size() + i) { - String input = args[templates.size() + i]; - if (applyParsing(args, arguments, optArgs, i, input)) break; + * @throws ArgumentIncorrectException If the argument is incorrect. + */ + public Arguments parse(Command command, String[] args) throws TypeArgumentNotExistException, ArgumentIncorrectException { + ParseResult result = parser.parse(command, args); + if (!result.isSuccess()) { + ParseError error = result.error(); + switch (error.type()) { + case TYPE_NOT_FOUND -> throw new TypeArgumentNotExistException(); + case CONVERSION_FAILED -> throw new ArgumentIncorrectException(error.input()); + default -> throw new ArgumentIncorrectException(error.message()); } } - - return arguments; + return result.arguments(); } /** * Get the commands of the command manager. + * * @return The commands of the command manager. */ public CommandTree getCommands() { return commands; } - /** * Get the completers of the command manager + * * @return The completers of command manager */ public Map>> getCompleters() { return this.completers; } + /** + * Check if a TabCompleter exists for the given type. + * + * @param type The type to check. + * @return true if a TabCompleter is registered for this type. + */ + public boolean hasTabCompleterForType(String type) { + ArgumentConverter.Wrapper wrapper = this.typeConverters.get(type.toLowerCase()); + return wrapper != null && wrapper.converter() instanceof TabCompleter; + } + + /** + * Get the TabCompleter for the given type. + * + * @param type The type to get the TabCompleter for. + * @return The TabCompleter for this type, or null if none exists. + */ + @SuppressWarnings("unchecked") + public TabCompleter getTabCompleterForType(String type) { + ArgumentConverter.Wrapper wrapper = this.typeConverters.get(type.toLowerCase()); + if (wrapper != null && wrapper.converter() instanceof TabCompleter) { + return (TabCompleter) wrapper.converter(); + } + return null; + } + /** * Get the platform of the command manager. + * * @return The platform of the command manager. */ - public CommandPlatform getPlatform() { + public CommandPlatform getPlatform() { return platform; } /** * Get the logger of the command manager. + * * @return The logger of the command manager. */ public Logger getLogger() { return this.logger; } + /** + * Set the custom logger of the command manager. + * + * @param logger The logger to set. + */ + public void setLogger(Logger logger) { + this.logger = logger; + } + /** * Register a list of subcommands in the command manager. + * * @param parentLabel The parent label of the commands. * @param subcommands The list of subcommands to register. - * @throws TypeArgumentNotExistException If the type of the argument does not exist. */ - private void registerSubCommands(String parentLabel, List> subcommands) throws TypeArgumentNotExistException { - if(subcommands == null || subcommands.isEmpty()) { + private void registerSubCommands(String parentLabel, List> subcommands) { + if (subcommands == null || subcommands.isEmpty()) { return; } - for (Command subcommand : subcommands) { - // getAliases() already returns [name, ...aliases], so no need to add the name again - List aliasesSub = new ArrayList<>(subcommand.getAliases()); + for (Command subcommand : subcommands) { + List aliasesSub = new ArrayList<>(subcommand.getAllLabels()); for (String aliasSub : aliasesSub) { this.addCommand(subcommand, parentLabel + "." + aliasSub); this.registerSubCommands(parentLabel + "." + aliasSub, subcommand.getSubcommands()); @@ -311,26 +319,27 @@ private void registerSubCommands(String parentLabel, List> subcomma /** * Unregister the subcommands of a command. - * @param parentLabel The parent label of the subcommands. + * + * @param parentLabel The parent label of the subcommands. * @param subcommandsList The list of subcommands to unregister. */ - private void unregisterSubCommands(String parentLabel, List> subcommandsList) { - if(subcommandsList == null || subcommandsList.isEmpty()) { + private void unregisterSubCommands(String parentLabel, List> subcommandsList) { + if (subcommandsList == null || subcommandsList.isEmpty()) { return; } - for (Command subcommand : subcommandsList) { - // getAliases() already returns [name, ...aliases], so no need to add the name again - List aliasesSub = new ArrayList<>(subcommand.getAliases()); - for (String aliasSub : aliasesSub) { - this.removeCommand(parentLabel + "." + aliasSub, true); - this.unregisterSubCommands(parentLabel + "." + aliasSub, subcommand.getSubcommands()); + for (Command subcommand : subcommandsList) { + List labelsSub = subcommand.getAllLabels(); + for (String labelSub : labelsSub) { + this.removeCommand(parentLabel + "." + labelSub, true); + this.unregisterSubCommands(parentLabel + "." + labelSub, subcommand.getSubcommands()); } } } /** * Unregister a command in the command manager. - * @param label The label of the command. + * + * @param label The label of the command. * @param subcommand If the subcommand must be unregistered. */ private void removeCommand(String label, boolean subcommand) { @@ -339,25 +348,32 @@ private void removeCommand(String label, boolean subcommand) { this.completers.remove(label); } + /** + * Create a new command builder bound to this manager. + * This allows for a fluent API without specifying generic types. + * + * @param name the command name + * @return a new command builder + */ + public CommandBuilder command(String name) { + return new CommandBuilder<>(this, name); + } + /** * Register a command in the command manager. + * * @param command The command to register. - * @param label The label of the command. - * @throws TypeArgumentNotExistException If the type of the argument does not exist. + * @param label The label of the command. */ - private void addCommand(Command command, String label) throws TypeArgumentNotExistException { - if(this.isDebug()) { + private void addCommand(Command command, String label) { + if (this.isDebug()) { this.logger.info("Register command " + label); } List> args = command.getArgs(); - List> optArgs = command.getOptinalArgs(); + List> optArgs = command.getOptionalArgs(); String[] labelParts = label.split("\\."); int labelSize = labelParts.length; - if(!this.checkTypeForArgs(args) || !this.checkTypeForArgs(optArgs)) { - throw new TypeArgumentNotExistException(); - } - command.setManager(this); this.platform.addCommand(command, label); commands.addCommand(label, command); @@ -369,6 +385,7 @@ private void addCommand(Command command, String label) throws TypeArgumentN /** * Register the completions of the command. + * * @param labelParts The parts of the label. */ private void addCompletionsForLabel(String[] labelParts) { @@ -385,23 +402,22 @@ private void addCompletionsForLabel(String[] labelParts) { /** * Register the completions of the arguments. - * @param label The label of the command. + * + * @param label The label of the command. * @param commandSize The size of the command. - * @param args The arguments to register. + * @param args The arguments to register. */ + @SuppressWarnings({"unchecked", "rawtypes"}) private void addCompletionForArgs(String label, int commandSize, List> args) { for (int i = 0; i < args.size(); i++) { Argument arg = args.get(i); - String[] parts = arg.arg().split(TYPE_PARSER); - String type = parts[1].trim(); - ArgumentConverter converter = this.typeConverters.get(type).getValue(); - TabCompleter argConverter = arg.tabConverter(); + String type = arg.type().key(); + ArgumentConverter.Wrapper entry = this.typeConverters.get(type); + TabCompleter argConverter = arg.tabCompleter(); if (argConverter != null) { - this.addCompletion(label,commandSize + i, argConverter); - } else if (converter instanceof TabCompleter) { - @SuppressWarnings("unchecked") - TabCompleter tabCompleter = (TabCompleter) converter; - this.addCompletion(label,commandSize + i, tabCompleter); + this.addCompletion(label, commandSize + i, argConverter); + } else if (entry != null && entry.converter() instanceof TabCompleter completer) { + this.addCompletion(label, commandSize + i, (TabCompleter) completer); } else { this.addCompletion(label, commandSize + i, (s, argsInner) -> new ArrayList<>()); } @@ -410,9 +426,10 @@ private void addCompletionForArgs(String label, int commandSize, List converter) { Map> mapInner = this.completers.getOrDefault(label, new HashMap<>()); @@ -421,9 +438,9 @@ private void addCompletion(String label, int commandSize, TabCompleter conver TabCompleter existing = mapInner.get(commandSize); if (existing != null) { - combined = (s,args) -> { - List completions = new ArrayList<>(existing.onCompletion(s,args)); - completions.addAll(converter.onCompletion(s,args)); + combined = (s, args) -> { + List completions = new ArrayList<>(existing.onCompletion(s, args)); + completions.addAll(converter.onCompletion(s, args)); return completions; }; } else { @@ -434,83 +451,9 @@ private void addCompletion(String label, int commandSize, TabCompleter conver this.completers.put(label, mapInner); } - /** - * Check if the type of the argument exists. - * @param args The arguments to check. - */ - private boolean checkTypeForArgs(List> args) throws TypeArgumentNotExistException { - for(String arg: args.stream().map(Argument::arg).collect(Collectors.toList())) { - String[] parts = arg.split(TYPE_PARSER); - - if (parts.length != 2) { - throw new TypeArgumentNotExistException(); - } - String type = parts[1].trim(); - if(!this.typeExist(type)) { - return false; - } - } - return true; - } - - /** - * Check if the type of the argument exists. - * @param type The type of the argument. - * @return If the type of the argument exists. - */ - private boolean typeExist(String type) { - return this.typeConverters.containsKey(type); - } - - /** - * Apply the parsing of the arguments. - * @param args The arguments to parse. - * @param arguments The arguments parsed. - * @param templates The templates of the arguments. - * @param argIndex The index of the argument. - * @param input The input of the argument. - * @return If the parsing is applied. - * @throws TypeArgumentNotExistException If the type of the argument does not exist. - * @throws ArgumentIncorrectException If the argument is incorrect. - */ - private boolean applyParsing(String[] args, Arguments arguments, List> templates, int argIndex, - String input) throws TypeArgumentNotExistException, ArgumentIncorrectException { - String template = templates.get(argIndex).arg(); - String[] parts = template.split(TYPE_PARSER); - - if (parts.length != 2) { - throw new TypeArgumentNotExistException(); - } - - String key = parts[0].trim(); - String type = parts[1].trim(); - - if (type.equals(INFINITE)) { - StringBuilder builder = new StringBuilder(); - for (int i = argIndex; i < args.length; i++) { - builder.append(args[i]); - if (i < args.length - 1) { - builder.append(" "); - } - } - arguments.add(key, String.class, builder.toString()); - return true; - } - - if (typeConverters.containsKey(type)) { - Class typeClass = typeConverters.get(type).getKey(); - ArgumentConverter converter = typeConverters.get(type).getValue(); - Object obj = converter.apply(input); - if (obj == null) { - throw new ArgumentIncorrectException(input); - } - arguments.add(key, typeClass, obj); - } - return false; - } - /** * Get the command invoker of the command manager. + * * @return The command invoker of the command manager. */ public CommandInvoker getInvoker() { @@ -521,11 +464,10 @@ public CommandInvoker getInvoker() { * Register the internal converters of the command manager. */ private void registerInternalConverters() { - this.registerConverter(String.class, (s) -> s); + this.registerConverter(String.class, (s) -> s); this.registerConverter(Boolean.class, new BooleanArgument<>()); this.registerConverter(Integer.class, new IntegerArgument()); this.registerConverter(Double.class, new DoubleArgument()); - this.registerConverter(Long.class, new LongArgument()); - this.registerConverter(String.class, INFINITE, s -> s); + this.registerConverter(Long.class, new LongArgument()); } } diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java b/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java index 46b9f00..a287103 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/Argument.java @@ -1,55 +1,59 @@ package fr.traqueur.commands.api.arguments; +import java.util.Objects; + /** * The class Argument. *

This class is used to represent an argument of a command.

- * @param The type of the sender that will use this argument. + * + * @param The type of the sender that will use this argument. + * @param name The argument name. + *

+ * This is the name of the argument that will be used in the command. + *

+ * @param tabCompleter The tab completer for this argument. + *

+ * This is used to provide tab completion for the argument. + *

*/ -public class Argument { - - /** - * The argument name. - *

- * This is the name of the argument that will be used in the command. - *

- */ - private final String arg; - - /** - * The tab completer for this argument. - *

- * This is used to provide tab completion for the argument. - *

- */ - private final TabCompleter tabCompleter; +public record Argument(String name, ArgumentType type, TabCompleter tabCompleter) { /** * Constructor for Argument. * - * @param arg The argument name. + * @param name The argument name. + * @param type The argument type. * @param tabCompleter The tab completer for this argument. */ - public Argument(String arg, TabCompleter tabCompleter) { - this.arg = arg; + public Argument(String name, ArgumentType type, TabCompleter tabCompleter) { + this.name = Objects.requireNonNull(name, "Argument name cannot be null"); + this.type = Objects.requireNonNull(type, "Argument type cannot be null"); this.tabCompleter = tabCompleter; } + /** - * Get the argument name. + * Create an argument without tab completer. * - * @return The argument name. + * @param name the argument name + * @param type the argument type */ - public String arg() { - return this.arg; + public Argument(String name, ArgumentType type) { + this(name, type, null); } + public String canonicalName() { + return this.name + ":" + this.type.key(); + } + + /** - * Get the tab completer for this argument. + * Check if this argument is infinite. * - * @return The tab completer. + * @return true if infinite type */ - public TabCompleter tabConverter() { - return this.tabCompleter; + public boolean isInfinite() { + return type.isInfinite(); } } diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java index e81e726..51d9990 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentConverter.java @@ -5,6 +5,7 @@ /** * The class ArgumentConverter. *

This class is used to convert a string to an object.

+ * * @param The type of the object. */ @FunctionalInterface @@ -12,9 +13,24 @@ public interface ArgumentConverter extends Function { /** * Apply the conversion. + * * @param s The string to convert. * @return The object. */ @Override T apply(String s); + + record Wrapper(Class clazz, ArgumentConverter converter) { + + public boolean convertAndApply(String input, String name, Arguments arguments) { + T result = converter.apply(input); + if (result == null) { + return false; + } + arguments.add(name, clazz, result); + return true; + } + + } + } diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java new file mode 100644 index 0000000..55c958d --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentType.java @@ -0,0 +1,41 @@ +package fr.traqueur.commands.api.arguments; + +public sealed interface ArgumentType permits ArgumentType.Simple, ArgumentType.Infinite { + + static ArgumentType of(Class clazz) { + if (clazz.isAssignableFrom(fr.traqueur.commands.api.arguments.Infinite.class)) { + return Infinite.INSTANCE; + } + return new Simple(clazz); + } + + String key(); + + /** + * Check if this is the infinite type. + * + * @return true if infinite + */ + default boolean isInfinite() { + return this instanceof Infinite; + } + + record Simple(Class clazz) implements ArgumentType { + + @Override + public String key() { + return clazz.getSimpleName().toLowerCase(); + } + + } + + record Infinite() implements ArgumentType { + public static final Infinite INSTANCE = new Infinite(); + + @Override + public String key() { + return "infinite"; + } + } + +} diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentValue.java b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentValue.java index f74a424..7ee15a8 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentValue.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/ArgumentValue.java @@ -3,35 +3,19 @@ /** * Represents a value of an argument with its type. * This class is used to store the type and value of an argument. + * + * @param type The type of the argument. + * @param value The value of the argument. */ -public class ArgumentValue { - - /** - * The type of the argument. - */ - private final Class type; - /** - * The value of the argument. - */ - private final Object value; - - /** - * Constructor to create an ArgumentValue with a specified type and value. - * - * @param type The class type of the argument. - * @param value The value of the argument. - */ - public ArgumentValue(Class type, Object value) { - this.type = type; - this.value = value; - } +public record ArgumentValue(Class type, Object value) { /** * Get the type of the argument. * * @return The type of the argument. */ - public Class getType() { + @Override + public Class type() { return this.type; } @@ -40,7 +24,8 @@ public Class getType() { * * @return The value of the argument. */ - public Object getValue() { + @Override + public Object value() { return this.value; } diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java b/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java index 07f98d0..53200f3 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/Arguments.java @@ -4,9 +4,8 @@ import fr.traqueur.commands.api.exceptions.NoGoodTypeArgumentException; import fr.traqueur.commands.api.logging.Logger; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.function.BiConsumer; /** * This class is used to store arguments. @@ -25,6 +24,7 @@ public class Arguments { /** * Constructor of the class. + * * @param logger The logger of the class. */ public Arguments(Logger logger) { @@ -32,323 +32,61 @@ public Arguments(Logger logger) { this.logger = logger; } - /** - * Get an argument from the map. - * - * @param argument The key of the argument. - * @param The type of the argument. - * @return The argument. - */ - public T get(String argument) { - try { - Optional value = this.getOptional(argument); - if (!value.isPresent()) { - throw new ArgumentNotExistException(); - } - return value.get(); - } catch (ArgumentNotExistException e) { - logger.error("The argument " + argument + " does not exist."); - logger.error(e.getMessage()); - } - return null; - } - - /** - * Get an argument from the map as an integer. - * - * @param argument The key of the argument. - * @param defaultValue The default value to return if the argument is not present. - * @return The integer or the default value if not present. - */ - public int getAsInt(String argument, int defaultValue) { - Optional value = this.getAsInt(argument); - return value.orElse(defaultValue); - } - - /** - * Get an argument from the map as a double. - * - * @param argument The key of the argument. - * @param defaultValue The default value to return if the argument is not present. - * @return The double or the default value if not present. - */ - public double getAsDouble(String argument, double defaultValue) { - Optional value = this.getAsDouble(argument); - return value.orElse(defaultValue); - } - - /** - * Get an argument from the map as a boolean. - * - * @param argument The key of the argument. - * @param defaultValue The default value to return if the argument is not present. - * @return The boolean or the default value if not present. - */ - public boolean getAsBoolean(String argument, boolean defaultValue) { - Optional value = this.getAsBoolean(argument); - return value.orElse(defaultValue); - } - - /** - * Get an argument from the map as a string. - * - * @param argument The key of the argument. - * @param defaultValue The default value to return if the argument is not present. - * @return The string or the default value if not present. - */ - public String getAsString(String argument, String defaultValue) { - Optional value = this.getAsString(argument); - return value.orElse(defaultValue); - } - - /** - * Get an argument from the map as a long. - * - * @param argument The key of the argument. - * @param defaultValue The default value to return if the argument is not present. - * @return The long or the default value if not present. - */ - public long getAsLong(String argument, long defaultValue) { - Optional value = this.getAsLong(argument); - return value.orElse(defaultValue); - } - - /** - * Get an argument from the map as a float. - * - * @param argument The key of the argument. - * @param defaultValue The default value to return if the argument is not present. - * @return The float or the default value if not present. - */ - public float getAsFloat(String argument, float defaultValue) { - Optional value = this.getAsFloat(argument); - return value.orElse(defaultValue); + public Map toMap() { + Map result = new HashMap<>(); + arguments.forEach((k, v) -> result.put(k, v.value())); + return result; } - /** - * Get an argument from the map as a short. - * - * @param argument The key of the argument. - * @param defaultValue The default value to return if the argument is not present. - * @return The short or the default value if not present. - */ - public short getAsShort(String argument, short defaultValue) { - Optional value = this.getAsShort(argument); - return value.orElse(defaultValue); + public int size() { + return arguments.size(); } - /** - * Get an argument from the map as a byte. - * - * @param argument The key of the argument. - * @param defaultValue The default value to return if the argument is not present. - * @return The byte or the default value if not present. - */ - public byte getAsByte(String argument, byte defaultValue) { - Optional value = this.getAsByte(argument); - return value.orElse(defaultValue); + public boolean isEmpty() { + return arguments.isEmpty(); } - /** - * Get an argument from the map as a character. - * - * @param argument The key of the argument. - * @param defaultValue The default value to return if the argument is not present. - * @return The character or the default value if not present. - */ - public char getAsChar(String argument, char defaultValue) { - Optional value = this.getAsChar(argument); - return value.orElse(defaultValue); + public Set getKeys() { + return Collections.unmodifiableSet(arguments.keySet()); } - /** - * Get an argument from the map as an integer. - * - * @param argument The key of the argument. - * @return The integer or empty if not present. - */ - public Optional getAsInt(String argument) { - try { - return this.getAs(argument, String.class).map(Integer::parseInt); - } catch (NumberFormatException e) { - return Optional.empty(); - } + public void forEach(BiConsumer action) { + arguments.forEach((k, v) -> action.accept(k, v.value())); } /** - * Get an argument from the map as a double. - * - * @param argument The key of the argument. - * @return The double or empty if not present. - */ - public Optional getAsDouble(String argument) { - try { - return this.getAs(argument, String.class).map(Double::parseDouble); - } catch (NumberFormatException e) { - return Optional.empty(); - } - } - - /** - * Get an argument from the map as a boolean. - * - * @param argument The key of the argument. - * @return The boolean or empty if not present. - */ - public Optional getAsBoolean(String argument) { - return this.getAs(argument, String.class).map(Boolean::parseBoolean); - } - - /** - * Get an argument from the map as a string. - * - * @param argument The key of the argument. - * @return The string or empty if not present. - */ - public Optional getAsString(String argument) { - return this.getAs(argument, String.class); - } - - /** - * Get an argument from the map as a long. - * - * @param argument The key of the argument. - * @return The long or empty if not present. - */ - public Optional getAsLong(String argument) { - try { - return this.getAs(argument, String.class).map(Long::parseLong); - } catch (NumberFormatException e) { - return Optional.empty(); - } - } - - /** - * Get an argument from the map as a float. - * - * @param argument The key of the argument. - * @return The float or empty if not present. - */ - public Optional getAsFloat(String argument) { - try { - return this.getAs(argument, String.class).map(Float::parseFloat); - } catch (NumberFormatException e) { - return Optional.empty(); - } - } - - /** - * Get an argument from the map as a short. - * - * @param argument The key of the argument. - * @return The short or empty if not present. - */ - public Optional getAsShort(String argument) { - try { - return this.getAs(argument, String.class).map(Short::parseShort); - } catch (NumberFormatException e) { - return Optional.empty(); - } - } - - /** - * Get an argument from the map as a byte. - * - * @param argument The key of the argument. - * @return The byte or empty if not present. - */ - public Optional getAsByte(String argument) { - try { - return this.getAs(argument, String.class).map(Byte::parseByte); - } catch (NumberFormatException e) { - return Optional.empty(); - } - } - - /** - * Get an argument from the map as a character. - * - * @param argument The key of the argument. - * @return The character or empty if not present. - */ - public Optional getAsChar(String argument) { - return this.getAs(argument, String.class).map(s -> s.charAt(0)); - } - - /** - * Get an argument from the map as a specific type. - * - * @param argument The key of the argument. - * @param typeRef The type of the argument. - * @param defaultValue The default value to return if the argument is not present. - * @param The type of the argument. - * @return The argument or the default value if not present. - */ - public T getAs(String argument, Class typeRef, T defaultValue) { - Optional value = this.getAs(argument, typeRef); - return value.orElse(defaultValue); - } - - /** - * Get an argument from the map as optional. + * Get an argument from the map. * * @param argument The key of the argument. - * @param typeRef The type of the argument. - * @param The type of the argument. + * @param The type of the argument. * @return The argument. */ - public Optional getAs(String argument, Class typeRef) { - if(typeRef.isPrimitive()) { - throw new IllegalArgumentException("The type " + typeRef.getName() + " is a primitive type. You must use the primitive methode"); - } - if(this.arguments.isEmpty()) { - return Optional.empty(); - } - - ArgumentValue argumentValue = this.arguments.getOrDefault(argument, null); - - if(argumentValue == null) { - return Optional.empty(); - } - - Class type = argumentValue.getType(); - Object value = argumentValue.getValue(); - - try { - if(!typeRef.isAssignableFrom(type)) { - throw new NoGoodTypeArgumentException(); - } - if (!typeRef.isInstance(value)) { - throw new NoGoodTypeArgumentException(); - } - } catch (NoGoodTypeArgumentException e) { - logger.error("The argument " + argument + " is not the good type."); - return Optional.empty(); - } - - return Optional.of(typeRef.cast(value)); + public T get(String argument) { + return this.getOptional(argument).orElseThrow(ArgumentNotExistException::new); } + /** * Get an argument from the map as optional. * * @param argument The key of the argument. - * @param The type of the argument. + * @param The type of the argument. * @return The argument. */ + @SuppressWarnings("unchecked") public Optional getOptional(String argument) { - if(this.arguments.isEmpty()) { + if (this.isEmpty()) { return Optional.empty(); } ArgumentValue argumentValue = this.arguments.getOrDefault(argument, null); - if(argumentValue == null) { + if (argumentValue == null) { return Optional.empty(); } - Class type = argumentValue.getType(); - Object value = argumentValue.getValue(); + Class type = argumentValue.type(); + Object value = argumentValue.value(); Class goodType = (Class) type; try { @@ -366,11 +104,11 @@ public Optional getOptional(String argument) { /** * Add an argument to the map. * - * @param key The key of the argument. - * @param type The type of the argument. + * @param key The key of the argument. + * @param type The type of the argument. * @param object The object of the argument. */ - public void add(String key, Class type, Object object) { + public void add(String key, Class type, T object) { ArgumentValue argumentValue = new ArgumentValue(type, object); this.arguments.put(key, argumentValue); } diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/Infinite.java b/core/src/main/java/fr/traqueur/commands/api/arguments/Infinite.java new file mode 100644 index 0000000..c0e3843 --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/Infinite.java @@ -0,0 +1,4 @@ +package fr.traqueur.commands.api.arguments; + +public interface Infinite { +} diff --git a/core/src/main/java/fr/traqueur/commands/api/arguments/TabCompleter.java b/core/src/main/java/fr/traqueur/commands/api/arguments/TabCompleter.java index 231007f..a9170d7 100644 --- a/core/src/main/java/fr/traqueur/commands/api/arguments/TabCompleter.java +++ b/core/src/main/java/fr/traqueur/commands/api/arguments/TabCompleter.java @@ -5,8 +5,9 @@ /** * The class TabConverter. *

- * This class is used to represent a tabulation command converter. + * This class is used to represent a tabulation command converter. *

+ * * @param The type of the sender that will use this tab completer. */ @FunctionalInterface @@ -15,8 +16,9 @@ public interface TabCompleter { /** * This method is called when the tabulation is used. * It is used to get the completion of the command. + * * @param sender The sender that will use this tab completer. - * @param args The arguments of the command. + * @param args The arguments of the command. * @return The completion of the command. */ List onCompletion(S sender, List args); diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgsWithInfiniteArgumentException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgsWithInfiniteArgumentException.java index 16d5b1f..6413e57 100644 --- a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgsWithInfiniteArgumentException.java +++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgsWithInfiniteArgumentException.java @@ -7,6 +7,7 @@ public class ArgsWithInfiniteArgumentException extends Exception { /** * Create a new instance of the exception with the default message. + * * @param optional if the argument is optional */ public ArgsWithInfiniteArgumentException(boolean optional) { diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentIncorrectException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentIncorrectException.java index 917a748..6d78018 100644 --- a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentIncorrectException.java +++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentIncorrectException.java @@ -3,7 +3,7 @@ /** * Exception thrown when an argument is incorrect. */ -public class ArgumentIncorrectException extends Exception { +public class ArgumentIncorrectException extends RuntimeException { /** * The input that caused the exception. @@ -22,6 +22,7 @@ public ArgumentIncorrectException(String input) { /** * Get the input that caused the exception. + * * @return The input. */ public String getInput() { diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentNotExistException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentNotExistException.java index 35f105a..98ac597 100644 --- a/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentNotExistException.java +++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/ArgumentNotExistException.java @@ -3,7 +3,7 @@ /** * This exception is thrown when the argument does not exist. */ -public class ArgumentNotExistException extends Exception { +public class ArgumentNotExistException extends RuntimeException { /** * Create a new instance of the exception with the default message. diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java index d0ad43b..f1814e3 100644 --- a/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java +++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/CommandRegistrationException.java @@ -19,7 +19,7 @@ public CommandRegistrationException(String message) { * Constructs a new exception with the specified detail message and cause. * * @param message the detail message - * @param cause the cause of the exception + * @param cause the cause of the exception */ public CommandRegistrationException(String message, Throwable cause) { super(message, cause); diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/TypeArgumentNotExistException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/TypeArgumentNotExistException.java index d27e073..a28df77 100644 --- a/core/src/main/java/fr/traqueur/commands/api/exceptions/TypeArgumentNotExistException.java +++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/TypeArgumentNotExistException.java @@ -3,7 +3,7 @@ /** * Exception thrown when an type of an argument is not found. */ -public class TypeArgumentNotExistException extends Exception { +public class TypeArgumentNotExistException extends RuntimeException { /** * Constructs a new exception with the default message. diff --git a/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java b/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java index 6fc453a..58d91ec 100644 --- a/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java +++ b/core/src/main/java/fr/traqueur/commands/api/exceptions/UpdaterInitializationException.java @@ -19,7 +19,7 @@ public UpdaterInitializationException(String message) { * Constructs a new exception with the specified detail message and cause. * * @param message the detail message - * @param cause the cause of the exception + * @param cause the cause of the exception */ public UpdaterInitializationException(String message, Throwable cause) { super(message, cause); diff --git a/core/src/main/java/fr/traqueur/commands/api/logging/Logger.java b/core/src/main/java/fr/traqueur/commands/api/logging/Logger.java index 96e38ee..a44b013 100644 --- a/core/src/main/java/fr/traqueur/commands/api/logging/Logger.java +++ b/core/src/main/java/fr/traqueur/commands/api/logging/Logger.java @@ -7,12 +7,14 @@ public interface Logger { /** * Logs an error message. + * * @param message The message to log. */ void error(String message); /** * Logs an information message. + * * @param message The message to log. */ void info(String message); diff --git a/core/src/main/java/fr/traqueur/commands/api/logging/MessageHandler.java b/core/src/main/java/fr/traqueur/commands/api/logging/MessageHandler.java index b457e66..27a8896 100644 --- a/core/src/main/java/fr/traqueur/commands/api/logging/MessageHandler.java +++ b/core/src/main/java/fr/traqueur/commands/api/logging/MessageHandler.java @@ -3,32 +3,43 @@ /** * The class MessageHandler. *

- * This class is used to represent a message handler. + * This class is used to represent a message handler. *

*/ public interface MessageHandler { /** * This method is used to get the no permission message. + * * @return The no permission message. */ String getNoPermissionMessage(); /** * This method is used to get the only in game message. + * * @return The only in game message. */ String getOnlyInGameMessage(); /** * This method is used to get the arg not recognized message. + * * @return The arg not recognized message. */ String getArgNotRecognized(); /** * This method is used to get the requirement message. + * * @return The requirement message. */ String getRequirementMessage(); + + /** + * This method is used to get the command disabled message. + * + * @return The command disabled message. + */ + String getCommandDisabledMessage(); } diff --git a/core/src/main/java/fr/traqueur/commands/api/models/Command.java b/core/src/main/java/fr/traqueur/commands/api/models/Command.java index 22eeb00..24e5943 100644 --- a/core/src/main/java/fr/traqueur/commands/api/models/Command.java +++ b/core/src/main/java/fr/traqueur/commands/api/models/Command.java @@ -1,64 +1,59 @@ package fr.traqueur.commands.api.models; -import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.api.CommandManager; import fr.traqueur.commands.api.arguments.Argument; +import fr.traqueur.commands.api.arguments.ArgumentType; +import fr.traqueur.commands.api.arguments.Arguments; import fr.traqueur.commands.api.arguments.TabCompleter; -import fr.traqueur.commands.api.exceptions.ArgsWithInfiniteArgumentException; import fr.traqueur.commands.api.requirements.Requirement; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * This class is the base class for all commands. * It contains all the necessary methods to create a command. * It is abstract and must be inherited to be used. + * * @param The plugin that owns the command. * @param The type of the sender who use the command. */ public abstract class Command { - private CommandManager manager; - + private static final Pattern DOT_PATTERN = Pattern.compile("\\."); /** * The plugin that owns the command. */ private final T plugin; - /** * The name of the command. */ private final String name; - /** * The aliases of the command. */ private final List aliases; - /** * The subcommands of the command. */ private final List> subcommands; - /** * The arguments of the command. */ private final List> args; - /** * The optional arguments of the command. */ private final List> optionalArgs; - /** * The requirements of the command. */ private final List> requirements; - + private CommandManager manager; /** * The description of the command. */ @@ -84,6 +79,8 @@ public abstract class Command { */ private boolean infiniteArgs; + private boolean enable; + /** * If the command is subcommand */ @@ -91,8 +88,9 @@ public abstract class Command { /** * The constructor of the command. + * * @param plugin The plugin that owns the command. - * @param name The name of the command. + * @param name The name of the command. */ public Command(T plugin, String name) { this.plugin = plugin; @@ -107,10 +105,12 @@ public Command(T plugin, String name) { this.optionalArgs = new ArrayList<>(); this.requirements = new ArrayList<>(); this.subcommand = false; + this.enable = true; } /** * This method is called to set the manager of the command. + * * @param manager The manager of the command. */ public void setManager(CommandManager manager) { @@ -119,7 +119,8 @@ public void setManager(CommandManager manager) { /** * This method is called when the command is executed. - * @param sender The sender of the command. + * + * @param sender The sender of the command. * @param arguments The arguments of the command. */ public abstract void execute(S sender, Arguments arguments); @@ -133,10 +134,11 @@ public void unregister() { /** * This method is called to unregister the command. + * * @param subcommands If the subcommands must be unregistered. */ public void unregister(boolean subcommands) { - if(this.manager == null) { + if (this.manager == null) { throw new IllegalArgumentException("The command is not registered."); } this.manager.unregisterCommand(this, subcommands); @@ -144,6 +146,7 @@ public void unregister(boolean subcommands) { /** * This method is called to get the name of the command. + * * @return The name of the command. */ public final String getName() { @@ -152,44 +155,79 @@ public final String getName() { /** * This method is called to get the description of the command. + * * @return The description of the command. */ public final String getDescription() { return description; } + /** + * This method is called to set the description of the command + * + * @param description The description of the command. + */ + public final void setDescription(String description) { + this.description = description; + } + /** * This method is called to get the permission of the command. + * * @return The permission of the command. */ public final String getPermission() { return permission; } + /** + * This method is called to set the permission of the command. + * + * @param permission The permission of the command. + */ + public final void setPermission(String permission) { + this.permission = permission; + } + /** * This method is called to get the usage of the command. + * * @return The usage of the command. */ public final String getUsage() { return usage; } + /** + * This method is called to set the usage of the command. + * + * @param usage The usage of the command. + */ + public final void setUsage(String usage) { + this.usage = usage; + } + /** * This method is called to get the aliases of the command. + * * @return The aliases of the command. */ public final List getAliases() { - List aliases = new ArrayList<>(); - aliases.add(name); + return Collections.unmodifiableList(aliases); + } + + public final List getAllLabels() { + List labels = new ArrayList<>(); + labels.add(name); if (!this.aliases.isEmpty()) { - aliases.addAll(this.aliases); + labels.addAll(this.aliases); } - return aliases; + return labels; } - /** * This method is called to get the subcommands of the command. + * * @return The subcommands of the command. */ public final List> getSubcommands() { @@ -198,6 +236,7 @@ public final List> getSubcommands() { /** * This method is called to get the arguments of the command. + * * @return The arguments of the command. */ public final List> getArgs() { @@ -206,14 +245,16 @@ public final List> 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; } /** * 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() { @@ -222,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() { @@ -230,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) { @@ -278,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) { @@ -286,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 @@ -297,189 +320,117 @@ 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) { - 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.addArg(argName, type); } } /** * 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 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 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); } /** * 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 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); } /** * This method is called to add arguments to the command. + * * @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); } /** * 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 addOptionalArgs(String arg, Class type) { - this.addOptionalArgs(arg, type,null); + public final void addOptionalArg(String arg, Class type) { + this.addOptionalArg(arg, type, null); } /** * 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 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); } } /** * 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)); } /** * Check if the command is subcommand + * * @return if the command is subcommand */ public final boolean isSubCommand() { @@ -488,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() { @@ -496,23 +448,23 @@ 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. + * @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("\\."); + String[] parts = DOT_PATTERN.split(label); usage.append(String.join(" ", parts)); 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(" <"); @@ -523,22 +475,22 @@ public String generateDefaultUsage(CommandPlatform platform, S sender, Stri usage.append(subs).append(">"); } - if (!this.getArgs().isEmpty() || !this.getOptinalArgs().isEmpty()) { + if (!this.getArgs().isEmpty() || !this.getOptionalArgs().isEmpty()) { usage.append(!directSubs.isEmpty() ? "|" : " "); // arguments obligatoires : String req = this.getArgs().stream() - .map(arg -> "<" + arg.arg() + ">") + .map(arg -> "<" + arg.canonicalName() + ">") .collect(Collectors.joining(" ")); 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() - .map(arg -> "[" + arg.arg() + "]") + String opt = this.getOptionalArgs().stream() + .map(arg -> "[" + arg.canonicalName() + "]") .collect(Collectors.joining(" ")); usage.append(opt); } @@ -547,6 +499,24 @@ public String generateDefaultUsage(CommandPlatform platform, S sender, Stri return usage.toString(); } + /** + * 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/CommandBuilder.java b/core/src/main/java/fr/traqueur/commands/api/models/CommandBuilder.java new file mode 100644 index 0000000..6eba4fb --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/models/CommandBuilder.java @@ -0,0 +1,173 @@ +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); + } + + 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 + */ + public Command register() { + 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 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..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 @@ -5,12 +5,12 @@ 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; import java.util.*; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -18,31 +18,25 @@ * CommandInvoker is responsible for invoking and suggesting commands. * It performs lookup, permission and requirement checks, usage display, parsing, and execution. * - * @param plugin type - * @param sender type + * @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) { Optional> contextOpt = findCommandContext(base, rawArgs); - if (!contextOpt.isPresent()) { + if (contextOpt.isEmpty()) { return false; } @@ -57,47 +51,66 @@ 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 */ 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)); } /** - * 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 */ @@ -111,7 +124,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 */ @@ -126,7 +140,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 */ @@ -143,6 +158,7 @@ private boolean checkRequirements(S source, Command command) { /** * Build error message for failed requirement. + * * @param req the failed requirement * @return the error message */ @@ -155,7 +171,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 */ @@ -164,7 +181,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); @@ -176,7 +193,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 */ @@ -185,13 +203,14 @@ 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(); } /** * 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 */ @@ -209,6 +228,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 */ @@ -219,8 +239,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) { @@ -229,27 +250,13 @@ 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. + * * @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) { @@ -257,8 +264,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 +295,17 @@ 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(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())); + } + private CommandTree.CommandNode traverseNode(CommandTree.CommandNode node, String[] args) { int index = 0; while (index < args.length - 1) { @@ -308,12 +326,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/CommandPlatform.java b/core/src/main/java/fr/traqueur/commands/api/models/CommandPlatform.java index 2232542..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,10 +1,12 @@ package fr.traqueur.commands.api.models; import fr.traqueur.commands.api.CommandManager; +import fr.traqueur.commands.api.resolver.SenderResolver; 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 +38,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 +55,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,15 +64,25 @@ 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); + + /** + * 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/models/collections/CommandTree.java b/core/src/main/java/fr/traqueur/commands/api/models/collections/CommandTree.java index 64d0964..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 @@ -3,114 +3,60 @@ 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 { + /** - * 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 + * Pre-compiled pattern for splitting labels. */ - 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 static final Pattern DOT_PATTERN = Pattern.compile("\\."); /** - * 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 + * Valid label pattern: starts with letter, followed by letters, digits, underscores, or dots. + * Each segment must start with a letter. */ - 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; - - /** 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; - } + private static final Pattern VALID_LABEL_SEGMENT = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]*$"); - /** Get the label of this node segment. - * @return the label like "hello" - */ - public String getLabel() { - return label; - } + /** + * Maximum length for a single label segment. + */ + private static final int MAX_SEGMENT_LENGTH = 64; - /** - * 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; - } + /** + * Maximum depth for nested commands. + */ + private static final int MAX_DEPTH = 10; - /** Get the command associated with this node, if any. - * @return the command, or empty if not set - */ - public Optional> getCommand() { - return Optional.ofNullable(command); - } + private CommandNode root; - /** 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 CommandTree() { + this.root = new CommandNode<>(null, null); } - private final CommandNode root; - - - /** - * Create an empty command tree with a root node. - * The root node has no label and serves as the starting point for all commands. - */ - public CommandTree() { + 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; @@ -120,12 +66,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(); @@ -153,9 +147,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(); @@ -169,11 +160,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) { @@ -187,7 +178,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; } } @@ -199,18 +190,54 @@ 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; } -} + + /** + * 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/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..485ee27 --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/api/parsing/ParseError.java @@ -0,0 +1,35 @@ +package fr.traqueur.commands.api.parsing; + +public record ParseError(Type type, + String argumentName, + String input, + String message) { + + 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); + } + + 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 new file mode 100644 index 0000000..fb0d376 --- /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 +) { + + /** + * 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); + } + + 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/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/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..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,123 +1,207 @@ package fr.traqueur.commands.api.updater; import fr.traqueur.commands.api.exceptions.UpdaterInitializationException; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; -import java.io.IOException; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +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.concurrent.CompletableFuture; import java.util.logging.Logger; /** - * 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 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("https://api.github.com/repos/Traqueur-dev/CommandsAPI/releases/latest").toURL(); + 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); } } - /** - * 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; + private Updater() {} + + public static void setLogger(Logger logger) { + LOGGER = logger; } + /* ------------------------------------------------------------ */ + /* Public API */ + /* ------------------------------------------------------------ */ + /** - * Set the logger to use for logging messages - * @param LOGGER The logger to use + * Async update check (non-blocking) */ - public static void setLogger(Logger LOGGER) { - Updater.LOGGER = LOGGER; + public static void checkUpdates() { + fetchLatestVersionAsync().thenAccept(latest -> { + if (latest == null) { + return; + } + + String current = getVersion(); + + 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 + ")"); + } + }); } /** - * Private constructor to prevent instantiation + * Async latest version fetch */ - private Updater() {} + public static CompletableFuture fetchLatestVersionAsync() { + return CompletableFuture.supplyAsync(() -> { + try { + URL metadataUrl = resolveMetadataUrl(); + if (metadataUrl == null) { + return null; + } - /** - * 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()); - } + 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; + } + }); } /** - * Get the version of the plugin - * @return The version of the plugin + * Current version from properties */ 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 IllegalStateException("commands.properties not found"); + } + + prop.load(is); return prop.getProperty("version"); - } catch (IOException e) { + } catch (Exception e) { throw new RuntimeException(e); } } + /* ------------------------------------------------------------ */ + /* Internal helpers */ + /* ------------------------------------------------------------ */ + /** - * Check if the plugin is up to date - * @return True if the plugin is up to date, false otherwise + * Resolve metadata URL once (releases -> snapshots) */ - public static boolean isUpToDate() { - try { - String latestVersion = fetchLatestVersion(); - return getVersion().equals(latestVersion); - } catch (Exception e) { - return false; + private static URL resolveMetadataUrl() { + if (RESOLVED_METADATA_URL != null) { + return RESOLVED_METADATA_URL; } - } - /** - * Get the latest version of the plugin - * @return The latest version of the plugin - */ - 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) { + 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; } } /** - * Get the latest version of the plugin - * @return The latest version of the plugin + * Lightweight HEAD check */ - 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()); - } - } finally { - connection.disconnect(); - } + private static boolean isValidMetadata(URL url) { + try { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("HEAD"); + connection.setConnectTimeout(3000); + connection.setReadTimeout(3000); - return response.toString(); + int code = connection.getResponseCode(); + return code >= 200 && code < 300; + } catch (Exception e) { + return false; + } } -} \ No newline at end of file +} 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 27101d3..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} @@ -45,4 +46,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 new file mode 100644 index 0000000..1abfdfc --- /dev/null +++ b/core/src/main/java/fr/traqueur/commands/impl/parsing/DefaultArgumentParser.java @@ -0,0 +1,135 @@ +package fr.traqueur.commands.impl.parsing; + +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; +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.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() + )); + } + + 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/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java b/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java index 67a6714..3e1148d 100644 --- a/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java +++ b/core/src/test/java/fr/traqueur/commands/api/CommandManagerTest.java @@ -1,66 +1,51 @@ 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 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 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 CommandManager manager; - private FakePlatform platform; - - 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) {} - } + private FakeLogger logger; + private MockCommandManager manager; + private MockPlatform platform; @BeforeEach void setUp() { - platform = new FakePlatform(); - manager = new CommandManager(platform) {}; - platform.injectManager(manager); - logger = Mockito.mock(InternalLogger.class); + manager = new MockCommandManager(); + platform = manager.getMockPlatform(); + logger = new FakeLogger(); 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(MockSender 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); @@ -72,36 +57,51 @@ 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"); + 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")); } @Test void testNoExtraAfterInfinite() throws Exception { - Command cmd = new DummyCommand(); + 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 void testBasicArgParsing_correctTypes() throws Exception { - Command cmd = new DummyCommand(); + Command cmd = new DummyCommand(); cmd.addArgs("num", Integer.class); cmd.addOptionalArgs("opt", String.class); @@ -116,25 +116,14 @@ void testBasicArgParsing_correctTypes() throws Exception { } @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.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)); + void addArgs_withOddArgs_shouldThrow() { + 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)); @@ -142,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()); @@ -161,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")); @@ -170,23 +159,47 @@ 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)); } - @Test - void addCommand_withUnknownType_shouldThrow() { - Command cmd = new DummyCommand(); - cmd.addArgs("bad:typeparser"); - assertThrows(RuntimeException.class, () -> manager.registerCommand(cmd)); + static class DummyCommand extends Command { + DummyCommand() { + super(null, "dummy"); + } + + DummyCommand(String name) { + super(null, name); + } + + @Override + public void execute(MockSender sender, Arguments args) { + } + } + + 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..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 @@ -1,119 +1,150 @@ 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.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.*; -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); + this.args = new Arguments(new InternalLogger(Logger.getLogger("ArgumentsTest"))); } @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)); + void testGetThrowsArgumentNotExistThrow() { + assertThrows(ArgumentNotExistException.class, () -> args.get("xxx")); } @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)); + void testInfiniteArgsBehavior() { + args.add("all", String.class, "Infinite arguments test"); + String allArgs = args.get("all"); + assertNotNull(allArgs); + assertEquals("Infinite arguments test", allArgs); } @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)); + void getOptional_onEmptyMapReturnsEmptyWithoutError() { + Optional opt = args.getOptional("anything"); + assertTrue(opt.isEmpty()); } + // --- New utility methods tests --- + @Test - void testStringCast_default() { - assertEquals("def", args.getAsString("missing", "def")); - args.add("s", String.class, "hello"); - assertEquals("hello", args.getAsString("s", "cfg")); + 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 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')); + void toMap_emptyArguments_returnsEmptyMap() { + Map map = args.toMap(); + assertTrue(map.isEmpty()); } @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()); + 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 testGetGeneric_andErrorLogging() { - args.add("gen", Integer.class, 5); - String wrong = args.getAs("gen", String.class, "def"); - assertEquals("def", wrong); + void isEmpty_returnsTrueWhenEmpty() { + assertTrue(args.isEmpty()); } @Test - void testGetThrowsArgumentNotExistLogged() { - assertNull(args.get("xxx")); - verify(logger).error(contains("xxx")); + void isEmpty_returnsFalseWhenNotEmpty() { + args.add("key", String.class, "value"); + assertFalse(args.isEmpty()); } @Test - void testInfiniteArgsBehavior() { - args.add("all", String.class, "Infinite arguments test"); - String allArgs = args.get("all"); - assertNotNull(allArgs); - assertEquals("Infinite arguments test", allArgs); + 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 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.")); + 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 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()); + 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..2e53b06 --- /dev/null +++ b/core/src/test/java/fr/traqueur/commands/api/models/CommandBuilderTest.java @@ -0,0 +1,365 @@ +package fr.traqueur.commands.api.models; + +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 static org.junit.jupiter.api.Assertions.*; + +class CommandBuilderTest { + + private MockCommandManager manager; + private MockPlatform platform; + + @BeforeEach + void setUp() { + manager = new MockCommandManager(); + platform = manager.getMockPlatform(); + } + + // --- 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(MockSender 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(MockSender sender) { + return true; + } + + @Override + public String errorMessage() { + return ""; + } + }, + new Requirement() { + @Override + public boolean check(MockSender 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(java.util.logging.Logger.getLogger("test")))); + + assertEquals("executed", received.get()); + } + + // --- Register --- + + @Test + void register_registersCommandInManager() { + Command cmd = manager.command("registered") + .executor((sender, args) -> { + }) + .register(); + + assertTrue(platform.getRegisteredLabels().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(MockSender 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()); + } + +} \ 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..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 @@ -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,110 @@ 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")); + } + + // --- v5.0.0 new tests --- + + @Test + void invoke_disabledCommand_sendsDisabledMessage() { + cmd.setEnabled(false); + when(messageHandler.getCommandDisabledMessage()).thenReturn("COMMAND_DISABLED"); - List suggests3 = invoker.suggest("user", "base", new String[]{"sub"}); - assertTrue(suggests3.contains("sub")); + boolean result = manager.getInvoker().invoke("user", "base", new String[]{}); - List suggests4 = invoker.suggest("user", "base", new String[]{"sub", ""}); - assertTrue(suggests4.contains("base")); + 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"); } - @Override public void execute(String sender, Arguments args) {} + DummyCommand() { + super(null, "base"); + } + + @Override + 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 25853a1..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,9 +1,8 @@ -// Placez ce fichier sous core/src/test/java/fr/traqueur/commands/api/ - 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; @@ -15,45 +14,23 @@ 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; - private CommandPlatform platform; + private MockCommandManager manager; @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) {} - }; + manager = new MockCommandManager(); cmd = new DummyCommand(); + cmd.setManager(manager); } @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")); } @@ -83,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()); @@ -91,60 +68,21 @@ 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()); - } - - @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()); + assertFalse(manager.getMockPlatform().hasCommand("dummy")); } @Test void usage_noSubs_noArgs() { - String usage = cmd.generateDefaultUsage(platform, null, "dummy"); + String usage = cmd.generateDefaultUsage(null, "dummy"); assertEquals("/dummy", usage); } @@ -152,7 +90,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 +98,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,9 +109,9 @@ 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('>')); + String inside = usage.substring(usage.indexOf('<') + 1, usage.indexOf('>')); List parts = Arrays.asList(inside.split("\\|")); assertTrue(parts.contains("suba")); assertTrue(parts.contains("subb")); @@ -186,10 +125,88 @@ 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("")); assertTrue(usage.contains("[opt:string]")); } -} + + @Test + void setEnabled_defaultIsTrue() { + assertTrue(cmd.isEnabled()); + } + + // --- v5.0.0 new tests --- + + @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)); + } + + private static class DummyCommand extends Command { + DummyCommand(String name) { + super(null, name); + } + + DummyCommand() { + super(null, "dummy"); + } + + @Override + public void execute(MockSender 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 4f5dcc2..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 @@ -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.*; @@ -25,13 +24,13 @@ 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)); + assertEquals(rootCmd, match.get().node().getCommand().orElse(null)); // full label - assertEquals("root", match.get().node.getFullLabel()); + assertEquals("root", match.get().node().getFullLabel()); } @Test @@ -39,45 +38,45 @@ 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); + 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); + assertEquals(subSubCmd, m2.get().node().getCommand().orElse(null)); + assertArrayEquals(new String[]{}, m2.get().args()); } @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); + assertEquals(rootCmd, m.get().node().getCommand().orElse(null)); + assertArrayEquals(new String[]{"a", "b", "c"}, m.get().args()); } @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()); } @@ -86,18 +85,18 @@ 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)); + 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()); + Optional> mRoot = tree.findNode("root", new String[]{}); + assertFalse(mRoot.get().node().getCommand().isPresent()); } @Test @@ -105,18 +104,18 @@ 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)); + 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()); } @@ -124,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"); @@ -138,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..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 @@ -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,60 +35,71 @@ 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")); - assertTrue(Updater.isUpToDate()); - mocks.verify(Updater::getVersion); - mocks.verify(Updater::fetchLatestVersion); - } - } + Updater.checkUpdates(); - @Test - void testIsUpToDate_withStaticMock_differentVersions() { - try (MockedStatic mocks = mockStatic(Updater.class)) { - mocks.when(Updater::isUpToDate).thenCallRealMethod(); - mocks.when(Updater::getVersion).thenReturn("1.0.0"); - mocks.when(Updater::fetchLatestVersion).thenReturn("2.0.0"); + assertTrue(logHandler.anyMatch(Level.WARNING, + r -> r.getMessage().contains("not up to date") + )); - assertFalse(Updater.isUpToDate()); + assertTrue(logHandler.anyMatch(Level.WARNING, + r -> r.getMessage().contains("Latest: 2.0.0") + )); } } @Test - void testCheckUpdates_logsWarningWhenNotUpToDate() { + void checkUpdatesAsync_logsUpToDateWhenVersionsMatch() { 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("1.0.0")); Updater.checkUpdates(); - assertTrue(logHandler.anyMatch(Level.WARNING, - rec -> rec.getMessage().contains("latest version is 2.0.0") + + assertTrue(logHandler.anyMatch(Level.INFO, + r -> r.getMessage().contains("up to date") )); } } - /** 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 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) { + + @Override + public void publish(LogRecord record) { + records.add(record); + } + + @Override + public void flush() {} + + @Override + public void close() {} + + 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)); } } } 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/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..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 @@ -1,10 +1,9 @@ -// 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; +import static org.junit.jupiter.api.Assertions.*; class InternalMessageHandlerTest { @@ -41,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 diff --git a/core/src/test/java/fr/traqueur/commands/test/mocks/MockCommandManager.java b/core/src/test/java/fr/traqueur/commands/test/mocks/MockCommandManager.java new file mode 100644 index 0000000..2c0cef6 --- /dev/null +++ b/core/src/test/java/fr/traqueur/commands/test/mocks/MockCommandManager.java @@ -0,0 +1,25 @@ +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; + + public MockCommandManager() { + this(new MockPlatform()); + } + + public MockCommandManager(MockPlatform platform) { + super(platform); + this.mockPlatform = platform; + } + + public MockPlatform getMockPlatform() { + return mockPlatform; + } +} diff --git a/core/src/test/java/fr/traqueur/commands/test/mocks/MockPlatform.java b/core/src/test/java/fr/traqueur/commands/test/mocks/MockPlatform.java new file mode 100644 index 0000000..3c14ffd --- /dev/null +++ b/core/src/test/java/fr/traqueur/commands/test/mocks/MockPlatform.java @@ -0,0 +1,88 @@ +package fr.traqueur.commands.test.mocks; + +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; + +/** + * 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(); + 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); + } +} 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/core/src/test/java/fr/traqueur/commands/test/mocks/MockSenderResolver.java b/core/src/test/java/fr/traqueur/commands/test/mocks/MockSenderResolver.java new file mode 100644 index 0000000..a564b51 --- /dev/null +++ b/core/src/test/java/fr/traqueur/commands/test/mocks/MockSenderResolver.java @@ -0,0 +1,27 @@ +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 + 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); + } +} 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-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 71fc4cc..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 @@ -1,7 +1,10 @@ package fr.traqueur.commands.test; +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.*; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.requests.GatewayIntent; @@ -10,7 +13,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 +23,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..."); @@ -51,8 +38,17 @@ public TestBot(String token) throws InterruptedException { CommandManager commandManager = new CommandManager<>(this, jda, LOGGER); commandManager.setDebug(true); - // Register commands - LOGGER.info("Registering commands..."); + AnnotationCommandProcessor annotationProcessor = + new AnnotationCommandProcessor<>(commandManager); + + // 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)); commandManager.registerCommand(new MathCommand(this)); @@ -71,4 +67,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 f892eba..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 @@ -1,8 +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.jda.JDAInteractionContext; import fr.traqueur.commands.test.TestBot; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -37,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 } @@ -51,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 } } @@ -68,18 +68,13 @@ 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.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)); + user.getAsMention(), reason)); } } @@ -95,17 +90,12 @@ 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; - } + public void execute(JDAInteractionContext context, JDAArguments arguments) { + 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)); + user.getAsMention(), reason)); } } @@ -119,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 } } @@ -134,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) { @@ -149,10 +140,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); @@ -170,12 +161,12 @@ 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.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)); } } -} \ 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 84dc086..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 @@ -1,8 +1,9 @@ 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.jda.JDAInteractionContext; import fr.traqueur.commands.test.TestBot; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -17,18 +18,15 @@ 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; - } + public void execute(JDAInteractionContext context, JDAArguments arguments) { + SlashCommandInteractionEvent event = (SlashCommandInteractionEvent) context.getEvent(); + 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(), @@ -37,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 19c8275..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 @@ -1,11 +1,9 @@ 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.jda.JDAInteractionContext; import fr.traqueur.commands.test.TestBot; -import net.dv8tion.jda.api.JDA; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; /** * Math command with subcommands demonstrating the command tree structure. @@ -29,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 } @@ -44,9 +42,9 @@ 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); + public void execute(JDAInteractionContext context, JDAArguments arguments) { + 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)); @@ -64,9 +62,9 @@ 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); + public void execute(JDAInteractionContext context, JDAArguments arguments) { + 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)); @@ -84,9 +82,9 @@ 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); + public void execute(JDAInteractionContext context, JDAArguments arguments) { + 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)); @@ -104,9 +102,9 @@ 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); + public void execute(JDAInteractionContext context, JDAArguments arguments) { + double a = arguments.get("a"); + double b = arguments.get("b"); if (b == 0) { jda(arguments).replyEphemeral("Cannot divide by zero!"); @@ -117,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 f6f1367..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 @@ -1,8 +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.jda.JDAInteractionContext; import fr.traqueur.commands.test.TestBot; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; @@ -17,15 +17,16 @@ 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 -> { 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(); }); }); } -} \ 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 548d490..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 @@ -1,8 +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.jda.JDAInteractionContext; import fr.traqueur.commands.test.TestBot; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.IMentionable; @@ -22,21 +22,21 @@ 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 - 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) - 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() @@ -55,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 new file mode 100644 index 0000000..b69a303 --- /dev/null +++ b/jda-test-bot/src/main/java/fr/traqueur/commands/test/commands/annoted/TestAnnotedCommands.java @@ -0,0 +1,31 @@ +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; +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(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(JDAInteractionContext context, String currentInput) { + List suggestions = Arrays.asList("option1", "option2", "option3"); + return suggestions.stream() + .filter(option -> option.startsWith(currentInput)) + .collect(Collectors.toList()); + } + +} 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/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 c1c7660..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,13 +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; @@ -17,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. @@ -33,24 +26,7 @@ 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)); + this.jdaPlatform = (JDAPlatform) super.getPlatform(); } /** @@ -97,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 new file mode 100644 index 0000000..07144c1 --- /dev/null +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAArgumentParser.java @@ -0,0 +1,132 @@ +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 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.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" + )); + } + + 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" + )); + } + + // 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 */ } + } + } +} 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..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,168 +1,114 @@ 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.entities.*; -import net.dv8tion.jda.api.entities.channel.unions.GuildChannelUnion; +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.OptionMapping; +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. - * This class listens for SlashCommandInteractionEvent and routes them to the appropriate command. - * - * @param The type of the bot instance. + * JDA executor that handles slash command and autocomplete events. */ public class JDAExecutor extends ListenerAdapter { - /** - * The command manager. - */ - private final CommandManager commandManager; + private final CommandManager commandManager; + private final JDAArgumentParser parser; - /** - * Constructor for JDAExecutor. - * - * @param commandManager The command manager. - */ - public JDAExecutor(CommandManager commandManager) { + 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); - 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) { + // 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()) { 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(context, 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(context, 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(JDAInteractionContext context, + 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()) .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)) { + if (!perm.isEmpty() && !commandManager.getPlatform().hasPermission(context, 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) { - for (Requirement req : command.getRequirements()) { - if (!req.check(event)) { + // Requirements check + for (Requirement req : command.getRequirements()) { + if (!req.check(context)) { String msg = req.errorMessage().isEmpty() ? commandManager.getMessageHandler().getRequirementMessage() .replace("%requirement%", req.getClass().getSimpleName()) @@ -171,126 +117,118 @@ 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) { - JDAArguments jdaArguments = new JDAArguments(commandManager.getLogger(), event); - List options = event.getOptions(); + @Override + public void onCommandAutoCompleteInteraction(@NotNull CommandAutoCompleteInteractionEvent event) { + String label = buildLabel(event); + String focusedOptionName = event.getFocusedOption().getName(); - for (OptionMapping option : options) { - populateArgument(jdaArguments, option); + if (commandManager.isDebug()) { + commandManager.getLogger().info("Received autocomplete for: " + label + " arg: " + focusedOptionName); } - return jdaArguments; - } + // Wrap the event + JDAInteractionContext context = JDAInteractionContext.wrap(event); - /** - * 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) { - String name = option.getName(); + // Find command + String[] labelParts = label.split("\\."); + Optional> found = + commandManager.getCommands().findNode(labelParts); - switch (option.getType()) { - case STRING: - arguments.add(name, String.class, option.getAsString()); - break; - case INTEGER: - arguments.add(name, Long.class, option.getAsLong()); - break; - case NUMBER: - arguments.add(name, Double.class, option.getAsDouble()); - break; - case BOOLEAN: - arguments.add(name, Boolean.class, option.getAsBoolean()); + 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; - case USER: - arguments.add(name, User.class, option.getAsUser()); - if (option.getAsMember() != null) { - arguments.add(name, Member.class, option.getAsMember()); + } + } + if (targetArg == null) { + for (Argument arg : command.getOptionalArgs()) { + if (arg.name().equals(focusedOptionName)) { + targetArg = arg; + break; } - 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) { + 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 { - command.execute(event, arguments); + 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) { - handleCommandError(event, label, e); + commandManager.getLogger().error("Error during autocomplete: " + e.getMessage()); + event.replyChoices(List.of()).queue(); } } /** - * Handle command execution errors. - * - * @param event The slash command event. - * @param label The command label. - * @param e The exception that occurred. + * Get the TabCompleter for an argument (custom or general). */ - 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(); + 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()); } - /** - * 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()); + return buildLabel(event.getName(), event.getSubcommandGroup(), event.getSubcommandName()); + } - if (event.getSubcommandGroup() != null) { - label.append(".").append(event.getSubcommandGroup()); - } + private String buildLabel(CommandAutoCompleteInteractionEvent event) { + return buildLabel(event.getName(), event.getSubcommandGroup(), event.getSubcommandName()); + } - if (event.getSubcommandName() != null) { - label.append(".").append(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 (subcommandName != null) { + label.append(".").append(subcommandName); } - return label.toString().toLowerCase(); } } \ No newline at end of file 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 dec2123..7d97aeb 100644 --- a/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDAPlatform.java @@ -4,15 +4,12 @@ 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; 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; @@ -25,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. @@ -41,16 +38,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. @@ -72,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)); } @@ -83,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; } @@ -97,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(); @@ -135,10 +132,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; } @@ -154,7 +151,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(); @@ -194,22 +191,27 @@ public void removeCommand(String label, boolean subcommand) { } } + @Override + public SenderResolver getSenderResolver() { + return new JDASenderResolver(); + } + /** * Add arguments to a slash command. * * @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.getOptinalArgs(); + 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.getOptinalArgs(); + 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,14 +245,41 @@ 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"; + private OptionData createOptionData(Argument arg, boolean required) { + String name = arg.name(); + + OptionType optionType = mapToOptionType(arg.type().key()); + + 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; + } - OptionType optionType = mapToOptionType(type); + /** + * 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 new OptionData(optionType, name, "Argument: " + name, required); + return false; } /** @@ -330,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 new file mode 100644 index 0000000..927894a --- /dev/null +++ b/jda/src/main/java/fr/traqueur/commands/jda/JDASenderResolver.java @@ -0,0 +1,62 @@ +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; + +/** + * Sender resolver for the JDA (Discord) platform. + * + *

Resolves method parameter types to appropriate objects:

+ *
    + *
  • {@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 { + + /** + * {@inheritDoc} + */ + @Override + public boolean canResolve(Class type) { + return JDAInteractionContext.class.isAssignableFrom(type) + || User.class.isAssignableFrom(type) + || Member.class.isAssignableFrom(type) + || MessageChannelUnion.class.isAssignableFrom(type); + } + + /** + * {@inheritDoc} + */ + @Override + public Object resolve(JDAInteractionContext context, Class type) { + if (JDAInteractionContext.class.isAssignableFrom(type)) { + return context; + } + if (User.class.isAssignableFrom(type)) { + return context.getUser(); + } + if (Member.class.isAssignableFrom(type)) { + return context.getMember(); // null if not in guild + } + if (MessageChannelUnion.class.isAssignableFrom(type)) { + return context.getChannel(); + } + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isGameOnly(Class type) { + // Member requires guild context (like Player requires game context) + return Member.class.isAssignableFrom(type); + } +} 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/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 59f93a7..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,9 +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.entities.Role; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import java.util.Arrays; import java.util.Collection; @@ -11,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; @@ -42,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; } @@ -64,4 +63,4 @@ public boolean check(SlashCommandInteractionEvent event) { public String errorMessage() { return errorMessage; } -} \ No newline at end of file +} 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 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-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/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..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 @@ -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)); - sender.sendMessage(this.getUsage()); + 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..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 @@ -10,19 +10,19 @@ public class SubTestCommand extends Command { public SubTestCommand(TestPlugin plugin) { super(plugin, "sub.inner"); - this.addArgs("test"); - this.addArgs("testStr", String.class, (sender, args) -> { - args.forEach(arg -> { - sender.sendMessage("Arg: " + arg); - }); - return List.of(); + this.addArgs("test", Integer.class); + this.addArg("testStr", String.class, (sender, args) -> { + args.forEach(arg -> { + sender.sendMessage("Arg: " + arg); + }); + return List.of(); }); this.addAlias("sub"); } @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-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/spigot/build.gradle b/spigot/build.gradle index 62d026c..e8f41a8 100644 --- a/spigot/build.gradle +++ b/spigot/build.gradle @@ -1,7 +1,3 @@ -plugins { - id 'maven-publish' -} - repositories { mavenCentral() maven { @@ -16,23 +12,5 @@ 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") -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - withSourcesJar() - withJavadocJar() -} - -publishing { - publications { - maven(MavenPublication) { - from components.java - groupId = project.group - artifactId = 'platform-spigot' - version = project.version - } - } -} + testImplementation("org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT") +} \ No newline at end of file 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..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; @@ -14,9 +15,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 +68,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,8 +173,13 @@ 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); } } + + @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/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..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 @@ -1,30 +1,47 @@ 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() {} + 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 +49,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..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 @@ -1,27 +1,43 @@ 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. */ - public PlayerArgument() {} + 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 +46,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 diff --git a/spigot/src/main/java/fr/traqueur/commands/spigot/requirements/WorldRequirement.java b/spigot/src/main/java/fr/traqueur/commands/spigot/requirements/WorldRequirement.java index ffd566e..9468eee 100644 --- a/spigot/src/main/java/fr/traqueur/commands/spigot/requirements/WorldRequirement.java +++ b/spigot/src/main/java/fr/traqueur/commands/spigot/requirements/WorldRequirement.java @@ -9,20 +9,11 @@ /** * The class WorldRequirement. *

- * 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..e588518 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,66 @@ 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) { + } + + @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()); bukkitStatic = Mockito.mockStatic(Bukkit.class); } diff --git a/velocity-test-plugin/build.gradle b/velocity-test-plugin/build.gradle index 3b6d4d7..f66ac0d 100644 --- a/velocity-test-plugin/build.gradle +++ b/velocity-test-plugin/build.gradle @@ -22,14 +22,14 @@ 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 { - 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/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..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 @@ -11,19 +11,19 @@ public class SubTestCommand extends Command { public SubTestCommand(VelocityTestPlugin plugin) { super(plugin, "sub.inner"); - this.addArgs("test"); - this.addArgs("testStr", String.class, (sender, args) -> { - args.forEach(arg -> { - sender.sendMessage(Component.text("Arg: " + arg)); - }); - return List.of(); + this.addArgs("test", Integer.class); + this.addArg("testStr", String.class, (sender, args) -> { + args.forEach(arg -> { + sender.sendMessage(Component.text("Arg: " + arg)); + }); + return List.of(); }); this.addAlias("sub"); } @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())); } 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..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,28 +1,48 @@ 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( - 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) { 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 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 - } - } -} 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..3e37df7 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,14 @@ 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 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; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; /** @@ -148,13 +148,18 @@ 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); } } + @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 79ea0eef9a47be433b50ef02fac60c938677fb8e Mon Sep 17 00:00:00 2001 From: Traqueur_ Date: Fri, 2 Jan 2026 19:04:18 +0100 Subject: [PATCH 2/2] fix(version): javadoc missing --- .../src/main/java/fr/traqueur/commands/annotations/Arg.java | 1 - .../src/main/java/fr/traqueur/commands/annotations/Infinite.java | 1 - 2 files changed, 2 deletions(-) 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 index ba589e4..011a52e 100644 --- a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Arg.java +++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Arg.java @@ -24,7 +24,6 @@ * } * * @since 5.0.0 - * @see Optional * @see Infinite */ @Retention(RetentionPolicy.RUNTIME) 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 index ad63b30..18c1d57 100644 --- a/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Infinite.java +++ b/annotations-addon/src/main/java/fr/traqueur/commands/annotations/Infinite.java @@ -31,7 +31,6 @@ * * @since 5.0.0 * @see Arg - * @see Optional */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER)