diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed35b23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Gradle +.gradle/ +build/ +!gradle/wrapper/gradle-wrapper.jar + +# IDE +.idea/ +*.iml +*.iws +*.ipr +out/ +.vscode/ + +# OS / Misc +.DS_Store +Thumbs.db +*.log diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..0b74eeb --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,84 @@ +# 📜 Instructions & Consignes du Projet - betterMcCommands + +Ce document consigne les directives d'architecture, rĂšgles de dĂ©veloppement et spĂ©cifications fonctionnelles du projet **betterMcCommands**. + +--- + +## 🎯 1. Vision du Projet +**betterMcCommands** est une bibliothĂšque Java conçue pour crĂ©er, structurer et gĂ©rer des commandes Minecraft de façon hautement dynamique, typĂ©e et Ă©vĂ©nementielle. + +--- + +## 📐 2. RĂšgles & Standards de DĂ©veloppement + +### đŸ”č Conception du Code +* **MĂ©thodes & Logique MĂ©tier d'Abord** : L'arbre de commande, le parsing des arguments et le systĂšme d'Ă©vĂ©nements doivent ĂȘtre totalement dĂ©couplĂ©s du moteur Bukkit/Paper pour permettre une testabilitĂ© unitaire Ă  100%. +* **Dynamisme & ModularitĂ©** : Injection sans dĂ©claration `plugin.yml`, enregistrement et dĂ©-enregistrement Ă  chaud. +* **Documentation Obligatoire** : **Chaque classe et mĂ©thode** doit comporter une Javadoc dĂ©taillĂ©e explicitant son rĂŽle, ses paramĂštres (`@param`), sa valeur de retour (`@return`) et ses exceptions (`@throws`). +* **Gestion des Textes & Couleurs** : IntĂ©gration native de **Kyori Adventure & MiniMessage** avec compatibilitĂ© pour les codes couleur legacy. + +--- + +## đŸ—‚ïž 3. Organisation des Packages + +``` +fr.luc.bettermccommands/ + ├── BetterMcCommands.java (Manager principal & Façade d'accĂšs) + ├── api/ (Interfaces et contrats publics) + │ ├── Command.java + │ ├── CommandNode.java + │ ├── CommandContext.java + │ ├── CommandExecutor.java + │ ├── CommandSenderType.java + │ ├── CommandResult.java + │ └── suggestion/ + │ ├── Suggestion.java + │ └── SuggestionProvider.java + ├── builder/ (Fluent Builder DSL) + │ ├── AbstractCommandBuilder.java + │ ├── CommandBuilder.java + │ └── SubCommandBuilder.java + ├── argument/ (SystĂšme d'arguments typĂ©s) + │ ├── CommandArgument.java + │ ├── ArgumentType.java + │ ├── Arguments.java + │ ├── type/ + │ │ ├── StringArgument.java + │ │ ├── IntegerArgument.java + │ │ ├── DoubleArgument.java + │ │ ├── BooleanArgument.java + │ │ ├── EnumArgument.java + │ │ ├── PlayerArgument.java + │ │ ├── OfflinePlayerArgument.java + │ │ ├── WorldArgument.java + │ │ ├── LocationArgument.java + │ │ └── DurationArgument.java + │ └── registry/ + │ └── ArgumentTypeRegistry.java + ├── event/ (Moteur d'Ă©vĂ©nements & Lifecycle) + │ ├── CommandEvent.java + │ ├── CancellableCommandEvent.java + │ ├── CommandPreExecuteEvent.java + │ ├── CommandPostExecuteEvent.java + │ ├── CommandPermissionDeniedEvent.java + │ ├── CommandSyntaxErrorEvent.java + │ ├── CommandArgumentParseException.java + │ ├── CommandTabCompleteEvent.java + │ ├── CommandCooldownEvent.java + │ ├── CommandEventListener.java + │ ├── CommandEventManager.java + │ └── annotation/ + │ └── CommandEventHandler.java + ├── cooldown/ (Gestionnaire de Cooldowns) + │ └── CooldownManager.java + ├── platform/ (Ponts plateformes) + │ ├── CommandPlatformAdapter.java + │ └── paper/ + │ ├── PaperCommandMapInjector.java + │ └── PaperCommandWrapper.java + └── demo/ (Exemples & DĂ©monstrations) + ├── DemoPlugin.java + └── commands/ + ├── DemoBaseCommand.java + └── DemoListener.java +``` diff --git a/README.md b/README.md index 8aa3376..3f702e1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,198 @@ -# betterMcCommands +# ⚡ betterMcCommands +Une bibliothĂšque Java moderne, fluide et Ă©vĂ©nementielle pour crĂ©er et gĂ©rer des commandes Minecraft de façon hautement dynamique sans aucune configuration dans `plugin.yml`. + +--- + +## 🌟 FonctionnalitĂ©s ClĂ©s + +- **DSL Fluide & DĂ©claratif** : CrĂ©ation chainable de commandes racines et sous-commandes imbriquĂ©es avec `BetterMcCommands.builder("ma-commande")`. +- **ZĂ©ro `plugin.yml`** : Injection et dĂ©-enregistrement Ă  chaud Ă  l'exĂ©cution dans la `CommandMap` du serveur. +- **Arguments TypĂ©s & Auto-complĂ©tion** : + - Types primitifs : `string`, `word`, `greedyString`, `integer`, `decimal`, `bool`, `enumOf`. + - Types Minecraft : `player`, `offlinePlayer`, `world`, `location`, `duration`. + - Arguments optionnels, valeurs par dĂ©faut et suggestions (Tab-Complete) dynamiques. +- **Moteur d'ÉvĂ©nements & Lifecycle** : + - Hooks par commande (`.onPreExecute()`, `.onPostExecute()`, `.onPermissionDenied()`, `.onSyntaxError()`, `.onCooldown()`, `.onTabComplete()`). + - Bus d'Ă©vĂ©nements global supportant les lambdas ou les classes d'Ă©coute avec `@CommandEventHandler`. + - ÉvĂ©nements annulables (`PreExecuteEvent`) pour vĂ©rifier des conditions de jeu (cooldowns, Ă©tat de combat, Ă©conomie). +- **Temps de recharge (Cooldowns) natifs** : Configuration directe avec permission de bypass optionnelle. +- **Formatage Moderne** : Support direct des composants Kyori Adventure et des balises MiniMessage (``, ``, etc.). + +--- + +## 📩 Installation & Configuration Gradle + +Ajoutez la dĂ©pendance dans votre `build.gradle.kts` : + +```kotlin +dependencies { + implementation("fr.luc:betterMcCommands:1.0.0-SNAPSHOT") +} +``` + +--- + +## 🚀 Guide de DĂ©marrage Rapide + +### 1. Initialisation dans votre Plugin + +```java +public class MyPlugin extends JavaPlugin { + + private BetterMcCommands commandsManager; + + @Override + public void onEnable() { + // Initialisation du gestionnaire pour ce plugin + this.commandsManager = BetterMcCommands.create(this); + + // Enregistrement de vos classes d'Ă©couteurs d'Ă©vĂ©nements + this.commandsManager.registerListeners(new MyCommandEventsListener()); + + // Enregistrement d'une commande + DemoBaseCommand.create().register(); + } + + @Override + public void onDisable() { + // DĂ©senregistrement Ă  chaud et nettoyage propre + if (commandsManager != null) { + commandsManager.unregisterAll(); + } + } +} +``` + +--- + +## 💡 Exemples de Commandes + +### DĂ©claration d'une commande racine avec sous-commandes et arguments typĂ©s + +```java +BetterMcCommands.builder("commande-demo") + .description("Commande de dĂ©monstration") + .aliases("demo", "cdemo") + .permission("bettermc.demo") + + // ÉvĂ©nement exĂ©cutĂ© avant la commande (annulable) + .onPreExecute(event -> { + if (event.isPlayer() && isInCombat(event.getPlayer())) { + event.setCancelled(true); + event.reply("Impossible d'exĂ©cuter cette commande en combat !"); + } + }) + + // ExĂ©cution de la commande racine : /commande-demo + .executes(context -> { + context.reply("=== DĂ©mo betterMcCommands ==="); + context.reply("Utilisez /demo give pour donner des ressources."); + }) + + // Sous-commande : /demo give [quantite] + .subcommand(BetterMcCommands.subBuilder("give") + .description("Donne des ressources Ă  un joueur") + .permission("bettermc.demo.give") + .argument(Arguments.player("cible").description("Joueur recevant les items")) + .argument(Arguments.integer("quantite", 1, 64).defaultValue(1)) + .executes(context -> { + Player target = context.getTargetPlayer("cible"); + int quantite = context.getInt("quantite"); + + context.replySuccess("Attribution de " + quantite + " items Ă  " + target.getName() + " !"); + }) + ) + + // Sous-commande avec argument gourmand (greedy) : /demo broadcast + .subcommand(BetterMcCommands.subBuilder("broadcast") + .description("Diffuse une annonce") + .argument(Arguments.greedyString("message")) + .executes(context -> { + String msg = context.getString("message"); + context.replySuccess("Diffusion : " + msg + ""); + }) + ) + + // Sous-commande avec Cooldown : /demo kit (recharge de 60 secondes) + .subcommand(BetterMcCommands.subBuilder("kit") + .cooldown(Duration.ofSeconds(60)) + .cooldownBypass("bettermc.bypass.kit") + .playerOnly() + .executes(context -> { + context.replySuccess("Kit reçu !"); + }) + ) + + .register(); +``` + +--- + +## 🎯 SystĂšme d'ÉvĂ©nements & Lifecycle + +Vous pouvez intercepter les Ă©vĂ©nements du cycle de vie des commandes soit directement sur le builder (`.onPreExecute(...)`, `.onPostExecute(...)`), soit dans une classe dĂ©diĂ©e avec `@CommandEventHandler` : + +```java +public class MyCommandEventsListener { + + @CommandEventHandler(priority = 10) + public void onPreExecute(CommandPreExecuteEvent event) { + System.out.println("Commande demandĂ©e : /" + event.getNode().getFullName() + " par " + event.getSender().getName()); + } + + @CommandEventHandler(command = "commande-demo") + public void onDemoPostExecute(CommandPostExecuteEvent event) { + System.out.println("Commande exĂ©cutĂ©e en " + event.getExecutionDuration().toMillis() + "ms."); + } + + @CommandEventHandler + public void onPermissionDenied(CommandPermissionDeniedEvent event) { + event.setCustomErrorMessage("AccĂšs refusĂ© ! Permission requise : " + event.getRequiredPermission() + ""); + } + + @CommandEventHandler + public void onCooldown(CommandCooldownEvent event) { + event.setCustomMessage("⏳ Patientez " + event.getRemainingCooldown().toSeconds() + "s avant de rĂ©utiliser cette commande."); + } +} +``` + +--- + +## 📂 Architecture des Packages + +``` +fr.luc.bettermccommands/ + ├── BetterMcCommands.java (Manager principal & Façade) + ├── api/ (Interfaces et modĂšles publics) + │ ├── Command.java + │ ├── CommandNode.java + │ ├── CommandContext.java + │ ├── CommandExecutor.java + │ ├── CommandSenderType.java + │ ├── CommandResult.java + │ └── suggestion/ + ├── builder/ (Fluent DSL Builder) + │ ├── AbstractCommandBuilder.java + │ ├── CommandBuilder.java + │ └── SubCommandBuilder.java + ├── argument/ (Arguments typĂ©s & Auto-complĂ©tion) + │ ├── CommandArgument.java + │ ├── ArgumentType.java + │ ├── Arguments.java + │ └── type/ + ├── event/ (Bus d'Ă©vĂ©nements & Lifecycle) + │ ├── CommandEvent.java + │ ├── CancellableCommandEvent.java + │ ├── CommandPreExecuteEvent.java + │ ├── CommandPostExecuteEvent.java + │ ├── CommandPermissionDeniedEvent.java + │ ├── CommandSyntaxErrorEvent.java + │ ├── CommandCooldownEvent.java + │ ├── CommandTabCompleteEvent.java + │ └── annotation/ + ├── cooldown/ (Gestion des cooldowns par joueur) + ├── platform/ (Injection runtime CommandMap Paper/Spigot) + └── demo/ (Plugin de dĂ©monstration et exemples) +``` diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..739d5d3 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + `java-library` + `maven-publish` + id("com.gradleup.shadow") version "8.3.6" +} + +group = "fr.luc" +version = "1.0.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + maven("https://oss.sonatype.org/content/groups/public/") + maven("https://jitpack.io") +} + +dependencies { + // Paper API 1.20.4 (Fournit Bukkit, CraftBukkit abstractions et Adventure natif) + compileOnly("io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT") + + // Kyori Adventure & MiniMessage + compileOnly("net.kyori:adventure-api:4.17.0") + compileOnly("net.kyori:adventure-text-minimessage:4.17.0") + + // Tests unitaires JUnit 5 & Mockito + testImplementation(platform("org.junit:junit-bom:5.10.2")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("org.mockito:mockito-core:5.11.0") + testImplementation("org.mockito:mockito-junit-jupiter:5.11.0") + + // Permet d'avoir Paper API et Adventure dans les tests + testImplementation("io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT") + testImplementation("net.kyori:adventure-api:4.17.0") + testImplementation("net.kyori:adventure-text-minimessage:4.17.0") +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + withSourcesJar() + withJavadocJar() +} + +tasks.withType { + options.encoding = "UTF-8" + options.release.set(17) +} + +tasks.withType { + options.encoding = "UTF-8" + (options as StandardJavadocDocletOptions).apply { + addStringOption("Xdoclint:none", "-quiet") + encoding = "UTF-8" + charSet = "UTF-8" + } +} + +tasks.shadowJar { + archiveClassifier.set("") +} + +tasks.build { + dependsOn(tasks.shadowJar) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3dc3522 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 +systemProp.file.encoding=UTF-8 +org.gradle.internal.worker.classpath.useArgumentFile=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..f94fe18 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..249efbb --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..d79e9f1 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "betterMcCommands" diff --git a/src/main/java/fr/luc/bettermccommands/BetterMcCommands.java b/src/main/java/fr/luc/bettermccommands/BetterMcCommands.java new file mode 100644 index 0000000..67524d1 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/BetterMcCommands.java @@ -0,0 +1,185 @@ +package fr.luc.bettermccommands; + +import fr.luc.bettermccommands.api.Command; +import fr.luc.bettermccommands.builder.CommandBuilder; +import fr.luc.bettermccommands.builder.SubCommandBuilder; +import fr.luc.bettermccommands.cooldown.CooldownManager; +import fr.luc.bettermccommands.event.CommandEvent; +import fr.luc.bettermccommands.event.CommandEventListener; +import fr.luc.bettermccommands.event.CommandEventManager; +import fr.luc.bettermccommands.platform.CommandDispatcher; +import fr.luc.bettermccommands.platform.paper.PaperCommandMapInjector; +import org.bukkit.plugin.Plugin; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Point d'entrĂ©e principal et gestionnaire central de la bibliothĂšque **betterMcCommands**. + * Fournit l'accĂšs aux constructeurs fluides, au bus d'Ă©vĂ©nements, aux cooldowns et Ă  l'injection dynamique. + */ +public class BetterMcCommands { + + private static BetterMcCommands instance; + + private final String pluginPrefix; + private final CommandEventManager eventManager; + private final CooldownManager cooldownManager; + private final CommandDispatcher dispatcher; + private final PaperCommandMapInjector injector; + private final Map registeredCommands = new ConcurrentHashMap<>(); + + /** + * Initialise une instance de betterMcCommands avec un prĂ©fixe personnalisĂ©. + * + * @param pluginPrefix Le prĂ©fixe utilisĂ© lors de l'enregistrement (ex: nom du plugin). + */ + public BetterMcCommands(String pluginPrefix) { + this.pluginPrefix = pluginPrefix != null ? pluginPrefix : "bettermc"; + this.eventManager = new CommandEventManager(); + this.cooldownManager = new CooldownManager(); + this.dispatcher = new CommandDispatcher(this); + this.injector = new PaperCommandMapInjector(); + + if (instance == null) { + instance = this; + } + } + + /** + * Initialise betterMcCommands pour un plugin Bukkit/Paper donnĂ©. + * + * @param plugin Le plugin propriĂ©taire. + * @return L'instance configurĂ©e. + */ + public static BetterMcCommands create(Plugin plugin) { + return new BetterMcCommands(plugin != null ? plugin.getName().toLowerCase() : "bettermc"); + } + + /** + * @return L'instance singleton par dĂ©faut de betterMcCommands. + */ + public static synchronized BetterMcCommands getInstance() { + if (instance == null) { + instance = new BetterMcCommands("bettermc"); + } + return instance; + } + + /** + * CrĂ©e un nouveau constructeur de commande racine. + * + * @param name Le nom de la commande (ex: "commande-demo"). + * @return Le {@link CommandBuilder} configurĂ©. + */ + public static CommandBuilder builder(String name) { + return new CommandBuilder(name); + } + + /** + * CrĂ©e un nouveau constructeur de sous-commande. + * + * @param name Le nom de la sous-commande (ex: "give"). + * @return Le {@link SubCommandBuilder} configurĂ©. + */ + public static SubCommandBuilder subBuilder(String name) { + return new SubCommandBuilder(name); + } + + /** + * Enregistre une commande construite auprĂšs du serveur et du gestionnaire. + * + * @param command La commande racine. + */ + public void registerCommand(Command command) { + command.setManager(this); + registeredCommands.put(command.getName().toLowerCase(), command); + injector.register(command, dispatcher, pluginPrefix); + } + + /** + * DĂ©senregistre une commande racine du serveur Ă  chaud. + * + * @param command La commande Ă  retirer. + */ + public void unregisterCommand(Command command) { + registeredCommands.remove(command.getName().toLowerCase()); + injector.unregister(command); + } + + /** + * DĂ©senregistre l'ensemble des commandes gĂ©rĂ©es par cette instance. + */ + public void unregisterAll() { + for (Command cmd : registeredCommands.values()) { + injector.unregister(cmd); + } + registeredCommands.clear(); + cooldownManager.clearAll(); + eventManager.unregisterAll(); + } + + /** + * Enregistre une classe d'Ă©couteurs d'Ă©vĂ©nements annotĂ©e avec {@link fr.luc.bettermccommands.event.annotation.CommandEventHandler}. + * + * @param listenerInstance L'instance contenant les mĂ©thodes d'Ă©coute. + */ + public void registerListeners(Object listenerInstance) { + eventManager.registerListeners(listenerInstance); + } + + /** + * Enregistre un Ă©couteur fonctionnel pour un type d'Ă©vĂ©nement donnĂ©. + * + * @param eventType Le type d'Ă©vĂ©nement Ă  Ă©couter. + * @param listener Le callback d'exĂ©cution. + * @param Le type de l'Ă©vĂ©nement. + */ + public void on(Class eventType, CommandEventListener listener) { + eventManager.register(eventType, listener); + } + + /** + * @return Le bus d'Ă©vĂ©nements de commandes. + */ + public CommandEventManager getEventManager() { + return eventManager; + } + + /** + * @return Le gestionnaire de temps de recharge (cooldowns). + */ + public CooldownManager getCooldownManager() { + return cooldownManager; + } + + /** + * @return Le dispatcher d'exĂ©cution. + */ + public CommandDispatcher getDispatcher() { + return dispatcher; + } + + /** + * @return L'injecteur CommandMap. + */ + public PaperCommandMapInjector getInjector() { + return injector; + } + + /** + * @return La collection des commandes racines enregistrĂ©es. + */ + public Collection getRegisteredCommands() { + return Collections.unmodifiableCollection(registeredCommands.values()); + } + + /** + * @return Le prĂ©fixe de namespace de cette instance. + */ + public String getPluginPrefix() { + return pluginPrefix; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/api/Command.java b/src/main/java/fr/luc/bettermccommands/api/Command.java new file mode 100644 index 0000000..dfb44c2 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/api/Command.java @@ -0,0 +1,75 @@ +package fr.luc.bettermccommands.api; + +import fr.luc.bettermccommands.BetterMcCommands; + +/** + * ReprĂ©sente une commande racine complĂšte enregistrĂ©e auprĂšs du serveur Minecraft. + */ +public class Command extends CommandNode { + + private boolean registered = false; + private BetterMcCommands manager; + + /** + * CrĂ©e une commande racine avec son nom principal. + * + * @param name Le nom de la commande (sans le slash initial). + */ + public Command(String name) { + super(name); + } + + /** + * @return true si cette commande est actuellement enregistrĂ©e sur le serveur, sinon false. + */ + public boolean isRegistered() { + return registered; + } + + /** + * DĂ©finit l'Ă©tat d'enregistrement de la commande. + * + * @param registered L'Ă©tat d'enregistrement. + */ + public void setRegistered(boolean registered) { + this.registered = registered; + } + + /** + * @return L'instance du gestionnaire {@link BetterMcCommands} gĂ©rant cette commande. + */ + public BetterMcCommands getManager() { + return manager; + } + + /** + * Associe cette commande Ă  son gestionnaire principal. + * + * @param manager L'instance de {@link BetterMcCommands}. + */ + public void setManager(BetterMcCommands manager) { + this.manager = manager; + } + + /** + * Enregistre cette commande auprĂšs de la plateforme Minecraft via son gestionnaire. + */ + public void register() { + if (manager != null) { + manager.registerCommand(this); + } else { + BetterMcCommands.getInstance().registerCommand(this); + } + } + + /** + * DĂ©senregistre cette commande du serveur Minecraft Ă  chaud. + */ + public void unregister() { + if (manager != null) { + manager.unregisterCommand(this); + } else { + BetterMcCommands.getInstance().unregisterCommand(this); + } + } +} diff --git a/src/main/java/fr/luc/bettermccommands/api/CommandContext.java b/src/main/java/fr/luc/bettermccommands/api/CommandContext.java new file mode 100644 index 0000000..c32a8db --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/api/CommandContext.java @@ -0,0 +1,321 @@ +package fr.luc.bettermccommands.api; + +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.ChatColor; +import org.bukkit.World; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; + +import java.time.Duration; +import java.util.*; + +/** + * Contexte complet d'exĂ©cution d'une commande. + * Contient l'Ă©metteur, les arguments analysĂ©s et typĂ©s, les mĂ©tadonnĂ©es et des utilitaires de rĂ©ponse. + */ +public class CommandContext { + + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + + private final CommandSender sender; + private final String label; + private final String[] rawArgs; + private final Map arguments; + private final Map metadata; + + /** + * Construit un nouveau contexte de commande. + * + * @param sender L'Ă©metteur de la commande. + * @param label Le nom ou alias utilisĂ© pour invoquer la commande. + * @param rawArgs Les arguments bruts sous forme de tableau de chaĂźnes. + * @param arguments La table des arguments rĂ©solus et typĂ©s. + */ + public CommandContext(CommandSender sender, String label, String[] rawArgs, Map arguments) { + this.sender = Objects.requireNonNull(sender, "sender cannot be null"); + this.label = label != null ? label : ""; + this.rawArgs = rawArgs != null ? rawArgs : new String[0]; + this.arguments = arguments != null ? new HashMap<>(arguments) : new HashMap<>(); + this.metadata = new HashMap<>(); + } + + /** + * @return L'Ă©metteur de la commande (Player, ConsoleCommandSender, etc.). + */ + public CommandSender getSender() { + return sender; + } + + /** + * @return true si l'Ă©metteur est un {@link Player}, sinon false. + */ + public boolean isPlayer() { + return sender instanceof Player; + } + + /** + * @return true si l'Ă©metteur est la console du serveur. + */ + public boolean isConsole() { + return sender instanceof ConsoleCommandSender; + } + + /** + * RĂ©cupĂšre le joueur Ă©metteur de la commande. + * + * @return L'instance de {@link Player}. + * @throws IllegalStateException si l'Ă©metteur n'est pas un joueur. + */ + public Player getPlayer() { + if (!isPlayer()) { + throw new IllegalStateException("CommandSender is not a Player: " + sender.getClass().getSimpleName()); + } + return (Player) sender; + } + + /** + * RĂ©cupĂšre la console Ă©mettrice de la commande. + * + * @return L'instance de {@link ConsoleCommandSender}. + * @throws IllegalStateException si l'Ă©metteur n'est pas la console. + */ + public ConsoleCommandSender getConsole() { + if (!isConsole()) { + throw new IllegalStateException("CommandSender is not a ConsoleCommandSender: " + sender.getClass().getSimpleName()); + } + return (ConsoleCommandSender) sender; + } + + /** + * @return Le label ou alias utilisĂ© lors de l'exĂ©cution. + */ + public String getLabel() { + return label; + } + + /** + * @return Le tableau des arguments bruts passĂ©s lors de l'appel. + */ + public String[] getRawArgs() { + return rawArgs.clone(); + } + + /** + * @return Une vue non modifiable des arguments analysĂ©s. + */ + public Map getArguments() { + return Collections.unmodifiableMap(arguments); + } + + /** + * VĂ©rifie si un argument du nom donnĂ© a Ă©tĂ© renseignĂ© et analysĂ©. + * + * @param name Le nom de l'argument. + * @return true si l'argument est prĂ©sent, sinon false. + */ + public boolean hasArgument(String name) { + return arguments.containsKey(name); + } + + /** + * RĂ©cupĂšre un argument typĂ© par son nom. + * + * @param name Le nom de l'argument. + * @param type La classe attendue pour l'argument. + * @param Le type gĂ©nĂ©rique. + * @return L'instance de l'argument analysĂ©. + * @throws IllegalArgumentException si l'argument est manquant ou n'est pas du type attendu. + */ + @SuppressWarnings("unchecked") + public T get(String name, Class type) { + Object val = arguments.get(name); + if (val == null) { + throw new IllegalArgumentException("Argument '" + name + "' is missing from context."); + } + if (!type.isInstance(val)) { + throw new IllegalArgumentException("Argument '" + name + "' is of type " + + val.getClass().getSimpleName() + ", expected " + type.getSimpleName()); + } + return (T) val; + } + + /** + * RĂ©cupĂšre un argument de maniĂšre optionnelle. + * + * @param name Le nom de l'argument. + * @param type La classe attendue pour l'argument. + * @param Le type gĂ©nĂ©rique. + * @return Un {@link Optional} contenant la valeur si prĂ©sente. + */ + @SuppressWarnings("unchecked") + public Optional getOptional(String name, Class type) { + Object val = arguments.get(name); + if (val != null && type.isInstance(val)) { + return Optional.of((T) val); + } + return Optional.empty(); + } + + /** + * RĂ©cupĂšre un argument sous forme de chaĂźne de caractĂšres. + * + * @param name Le nom de l'argument. + * @return La chaĂźne analysĂ©e. + */ + public String getString(String name) { + return get(name, String.class); + } + + /** + * RĂ©cupĂšre un argument entier. + * + * @param name Le nom de l'argument. + * @return L'entier analysĂ©. + */ + public int getInt(String name) { + return get(name, Integer.class); + } + + /** + * RĂ©cupĂšre un argument double. + * + * @param name Le nom de l'argument. + * @return Le double analysĂ©. + */ + public double getDouble(String name) { + return get(name, Double.class); + } + + /** + * RĂ©cupĂšre un argument boolĂ©en. + * + * @param name Le nom de l'argument. + * @return Le boolĂ©en analysĂ©. + */ + public boolean getBoolean(String name) { + return get(name, Boolean.class); + } + + /** + * RĂ©cupĂšre un joueur cible ciblĂ© par l'argument. + * + * @param name Le nom de l'argument. + * @return Le joueur cible. + */ + public Player getTargetPlayer(String name) { + return get(name, Player.class); + } + + /** + * RĂ©cupĂšre un monde ciblĂ© par l'argument. + * + * @param name Le nom de l'argument. + * @return Le monde cible. + */ + public World getWorld(String name) { + return get(name, World.class); + } + + /** + * RĂ©cupĂšre une durĂ©e ciblĂ©e par l'argument. + * + * @param name Le nom de l'argument. + * @return La durĂ©e analysĂ©e. + */ + public Duration getDuration(String name) { + return get(name, Duration.class); + } + + /** + * DĂ©finit une mĂ©tadonnĂ©e personnalisĂ©e dans le contexte d'exĂ©cution. + * + * @param key La clĂ© de la mĂ©tadonnĂ©e. + * @param value La valeur associĂ©e. + */ + public void setMetadata(String key, Object value) { + this.metadata.put(key, value); + } + + /** + * RĂ©cupĂšre une mĂ©tadonnĂ©e du contexte. + * + * @param key La clĂ©. + * @param type Le type attendu. + * @param Le type gĂ©nĂ©rique. + * @return Un {@link Optional} contenant la mĂ©tadonnĂ©e si prĂ©sente. + */ + @SuppressWarnings("unchecked") + public Optional getMetadata(String key, Class type) { + Object val = metadata.get(key); + if (val != null && type.isInstance(val)) { + return Optional.of((T) val); + } + return Optional.empty(); + } + + /** + * Envoie un message formatĂ© avec MiniMessage Ă  l'Ă©metteur de la commande. + * + * @param miniMessageText Le texte formatĂ© avec tags MiniMessage (ex: "SuccĂšs !"). + */ + public void reply(String miniMessageText) { + if (miniMessageText == null || miniMessageText.isEmpty()) { + return; + } + try { + if (sender instanceof Audience) { + ((Audience) sender).sendMessage(MINI_MESSAGE.deserialize(miniMessageText)); + } else { + sender.sendMessage(ChatColor.translateAlternateColorCodes('&', miniMessageText)); + } + } catch (Throwable t) { + sender.sendMessage(miniMessageText); + } + } + + /** + * Envoie un composant Kyori Adventure directement Ă  l'Ă©metteur. + * + * @param component Le composant texte Adventure. + */ + public void reply(Component component) { + if (component == null) { + return; + } + if (sender instanceof Audience) { + ((Audience) sender).sendMessage(component); + } else { + sender.sendMessage(component.toString()); + } + } + + /** + * Envoie un message de succĂšs (prĂ©fixĂ© en vert). + * + * @param message Le message de succĂšs. + */ + public void replySuccess(String message) { + reply("[✔] " + message + ""); + } + + /** + * Envoie un message d'erreur (prĂ©fixĂ© en rouge). + * + * @param message Le message d'erreur. + */ + public void replyError(String message) { + reply("[✖] " + message + ""); + } + + /** + * Envoie un message informatif (prĂ©fixĂ© en bleu/aqua). + * + * @param message Le message d'information. + */ + public void replyInfo(String message) { + reply("[â„č] " + message + ""); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/api/CommandExecutor.java b/src/main/java/fr/luc/bettermccommands/api/CommandExecutor.java new file mode 100644 index 0000000..dbedbeb --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/api/CommandExecutor.java @@ -0,0 +1,16 @@ +package fr.luc.bettermccommands.api; + +/** + * Interface fonctionnelle reprĂ©sentant l'action exĂ©cutĂ©e par une commande ou sous-commande. + */ +@FunctionalInterface +public interface CommandExecutor { + + /** + * ExĂ©cute la logique mĂ©tier de la commande avec le contexte fourni. + * + * @param context Le contexte d'exĂ©cution contenant l'Ă©metteur, les arguments et les mĂ©tadonnĂ©es. + * @throws Exception En cas d'erreur lors du traitement de la commande. + */ + void execute(CommandContext context) throws Exception; +} diff --git a/src/main/java/fr/luc/bettermccommands/api/CommandNode.java b/src/main/java/fr/luc/bettermccommands/api/CommandNode.java new file mode 100644 index 0000000..451daba --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/api/CommandNode.java @@ -0,0 +1,252 @@ +package fr.luc.bettermccommands.api; + +import fr.luc.bettermccommands.argument.CommandArgument; +import fr.luc.bettermccommands.event.*; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * ReprĂ©sente un nƓud dans l'arborescence des commandes (commande racine ou sous-commande). + */ +public class CommandNode { + + protected final String name; + protected String description; + protected String permission; + protected CommandSenderType senderType = CommandSenderType.ALL; + protected final Set aliases = new LinkedHashSet<>(); + protected CommandNode parent; + protected final Map subCommands = new LinkedHashMap<>(); + protected final List> arguments = new ArrayList<>(); + protected CommandExecutor executor; + protected Duration cooldown = Duration.ZERO; + protected String cooldownBypassPermission; + + // Écouteurs d'Ă©vĂ©nements locaux attachĂ©s directement Ă  ce nƓud + protected final List> preExecuteListeners = new CopyOnWriteArrayList<>(); + protected final List> postExecuteListeners = new CopyOnWriteArrayList<>(); + protected final List> permissionDeniedListeners = new CopyOnWriteArrayList<>(); + protected final List> syntaxErrorListeners = new CopyOnWriteArrayList<>(); + protected final List> cooldownListeners = new CopyOnWriteArrayList<>(); + protected final List> tabCompleteListeners = new CopyOnWriteArrayList<>(); + + /** + * CrĂ©e un nouveau nƓud de commande avec son nom identifiant. + * + * @param name Le nom du nƓud. + */ + public CommandNode(String name) { + this.name = Objects.requireNonNull(name, "Command node name cannot be null").toLowerCase(); + } + + /** + * @return Le nom unique de ce nƓud. + */ + public String getName() { + return name; + } + + /** + * @return La description explicative du rĂŽle de ce nƓud. + */ + public String getDescription() { + return description != null ? description : ""; + } + + public void setDescription(String description) { + this.description = description; + } + + /** + * @return La permission requise pour exĂ©cuter ce nƓud (ou null). + */ + public String getPermission() { + return permission; + } + + public void setPermission(String permission) { + this.permission = permission; + } + + /** + * @return Le type d'Ă©metteur requis (Joueur, Console, Tous). + */ + public CommandSenderType getSenderType() { + return senderType; + } + + public void setSenderType(CommandSenderType senderType) { + this.senderType = senderType; + } + + /** + * @return L'ensemble des alias enregistrĂ©s pour ce nƓud. + */ + public Set getAliases() { + return Collections.unmodifiableSet(aliases); + } + + public void addAliases(Collection aliases) { + this.aliases.addAll(aliases); + } + + /** + * @return Le nƓud parent, ou {@code null} s'il s'agit de la commande racine. + */ + public CommandNode getParent() { + return parent; + } + + /** + * DĂ©finit le nƓud parent. + * + * @param parent Le nƓud parent. + */ + public void setParent(CommandNode parent) { + this.parent = parent; + } + + /** + * @return Une vue non modifiable des sous-commandes de ce nƓud. + */ + public Collection getSubCommands() { + return Collections.unmodifiableSet(new LinkedHashSet<>(subCommands.values())); + } + + /** + * Ajoute une sous-commande et enregistre ses alias. + * + * @param subNode La sous-commande Ă  associer. + */ + public void addSubCommand(CommandNode subNode) { + subNode.setParent(this); + this.subCommands.put(subNode.getName().toLowerCase(), subNode); + for (String alias : subNode.getAliases()) { + this.subCommands.put(alias.toLowerCase(), subNode); + } + } + + /** + * Recherche une sous-commande par nom ou alias. + * + * @param nameOrAlias Le nom ou alias recherchĂ©. + * @return Le {@link CommandNode} correspondant, ou {@code null}. + */ + public CommandNode findSubCommand(String nameOrAlias) { + if (nameOrAlias == null) return null; + return subCommands.get(nameOrAlias.toLowerCase()); + } + + /** + * @return true si ce nƓud possĂšde des sous-commandes enregistrĂ©es. + */ + public boolean hasSubCommands() { + return !subCommands.isEmpty(); + } + + /** + * @return La liste ordonnĂ©e des arguments attendus pour ce nƓud. + */ + public List> getArguments() { + return Collections.unmodifiableList(arguments); + } + + public void addArguments(Collection> arguments) { + this.arguments.addAll(arguments); + } + + /** + * @return Le gestionnaire d'exĂ©cution mĂ©tier associĂ©. + */ + public CommandExecutor getExecutor() { + return executor; + } + + public void setExecutor(CommandExecutor executor) { + this.executor = executor; + } + + /** + * @return La durĂ©e du temps de recharge configurĂ©. + */ + public Duration getCooldown() { + return cooldown; + } + + public void setCooldown(Duration cooldown) { + this.cooldown = cooldown; + } + + /** + * @return La permission permettant d'ignorer le cooldown (ou null). + */ + public String getCooldownBypassPermission() { + return cooldownBypassPermission; + } + + public void setCooldownBypassPermission(String cooldownBypassPermission) { + this.cooldownBypassPermission = cooldownBypassPermission; + } + + /** + * Calcule le chemin complet du nƓud (ex: "demo admin setrank"). + * + * @return La chaĂźne hiĂ©rarchique complĂšte. + */ + public String getFullName() { + if (parent == null) { + return name; + } + return parent.getFullName() + " " + name; + } + + /** + * @return Le nom de la commande racine au sommet de l'arborescence. + */ + public String getRootName() { + if (parent == null) { + return name; + } + return parent.getRootName(); + } + + /** + * Construit la syntaxe d'utilisation complĂšte (ex: "/demo give [quantite]"). + * + * @return La chaĂźne d'aide formatĂ©e. + */ + public String getUsage() { + StringBuilder builder = new StringBuilder("/").append(getFullName()); + for (CommandArgument arg : arguments) { + builder.append(" ").append(arg.getUsage()); + } + return builder.toString(); + } + + // Gestion des Ă©couteurs locaux + public List> getPreExecuteListeners() { + return preExecuteListeners; + } + + public List> getPostExecuteListeners() { + return postExecuteListeners; + } + + public List> getPermissionDeniedListeners() { + return permissionDeniedListeners; + } + + public List> getSyntaxErrorListeners() { + return syntaxErrorListeners; + } + + public List> getCooldownListeners() { + return cooldownListeners; + } + + public List> getTabCompleteListeners() { + return tabCompleteListeners; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/api/CommandResult.java b/src/main/java/fr/luc/bettermccommands/api/CommandResult.java new file mode 100644 index 0000000..51d8f50 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/api/CommandResult.java @@ -0,0 +1,42 @@ +package fr.luc.bettermccommands.api; + +/** + * ReprĂ©sente l'Ă©tat et le rĂ©sultat final de l'Ă©valuation ou de l'exĂ©cution d'une commande. + */ +public enum CommandResult { + + /** + * La commande a Ă©tĂ© exĂ©cutĂ©e avec succĂšs. + */ + SUCCESS, + + /** + * L'exĂ©cution a Ă©chouĂ© en raison d'une exception non gĂ©rĂ©e ou d'un Ă©chec mĂ©tier. + */ + FAILED, + + /** + * L'exĂ©cution a Ă©tĂ© annulĂ©e par un Ă©couteur d'Ă©vĂ©nement (ex: CommandPreExecuteEvent). + */ + CANCELLED, + + /** + * L'Ă©metteur n'a pas les permissions requises ou le bon type d'Ă©metteur. + */ + PERMISSION_DENIED, + + /** + * La syntaxe est invalide (arguments manquants ou sous-commande inconnue). + */ + SYNTAX_ERROR, + + /** + * Un argument n'a pas pu ĂȘtre converti / parsĂ© vers son type cible. + */ + ARGUMENT_PARSE_ERROR, + + /** + * La commande a Ă©tĂ© bloquĂ©e car le joueur est sous cooldown. + */ + COOLDOWN +} diff --git a/src/main/java/fr/luc/bettermccommands/api/CommandSenderType.java b/src/main/java/fr/luc/bettermccommands/api/CommandSenderType.java new file mode 100644 index 0000000..e4d6d65 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/api/CommandSenderType.java @@ -0,0 +1,43 @@ +package fr.luc.bettermccommands.api; + +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; + +/** + * DĂ©finit le type d'Ă©metteur autorisĂ© Ă  exĂ©cuter une commande ou sous-commande. + */ +public enum CommandSenderType { + + /** + * Tous les Ă©metteurs sont autorisĂ©s (Joueur, Console, CommandBlock, etc.). + */ + ALL, + + /** + * Seuls les joueurs connectĂ©s sont autorisĂ©s. + */ + PLAYER_ONLY, + + /** + * Seule la console du serveur est autorisĂ©e. + */ + CONSOLE_ONLY; + + /** + * VĂ©rifie si l'Ă©metteur donnĂ© correspond au type requis. + * + * @param sender L'Ă©metteur de la commande. + * @return true si l'Ă©metteur est autorisĂ©, sinon false. + */ + public boolean isAllowed(CommandSender sender) { + if (sender == null) { + return false; + } + return switch (this) { + case ALL -> true; + case PLAYER_ONLY -> sender instanceof Player; + case CONSOLE_ONLY -> sender instanceof ConsoleCommandSender; + }; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/api/suggestion/Suggestion.java b/src/main/java/fr/luc/bettermccommands/api/suggestion/Suggestion.java new file mode 100644 index 0000000..4dfb4a6 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/api/suggestion/Suggestion.java @@ -0,0 +1,84 @@ +package fr.luc.bettermccommands.api.suggestion; + +import java.util.Objects; + +/** + * ReprĂ©sente une suggestion d'auto-complĂ©tion (Tab-Complete) avec son texte et une Ă©ventuelle description. + */ +public class Suggestion { + + private final String value; + private final String tooltip; + + /** + * CrĂ©e une suggestion simple sans infobulle. + * + * @param value Le texte de la suggestion. + */ + public Suggestion(String value) { + this(value, null); + } + + /** + * CrĂ©e une suggestion avec une infobulle/description explicative. + * + * @param value Le texte insĂ©rĂ© lors de la complĂ©tion. + * @param tooltip L'infobulle ou description affichĂ©e au joueur (optionnelle). + */ + public Suggestion(String value, String tooltip) { + this.value = Objects.requireNonNull(value, "value cannot be null"); + this.tooltip = tooltip; + } + + /** + * CrĂ©e une nouvelle suggestion simple. + * + * @param value Le texte de la suggestion. + * @return L'instance de {@link Suggestion}. + */ + public static Suggestion of(String value) { + return new Suggestion(value); + } + + /** + * CrĂ©e une nouvelle suggestion avec infobulle. + * + * @param value Le texte de la suggestion. + * @param tooltip L'infobulle explicative. + * @return L'instance de {@link Suggestion}. + */ + public static Suggestion of(String value, String tooltip) { + return new Suggestion(value, tooltip); + } + + /** + * @return Le texte de la suggestion. + */ + public String getValue() { + return value; + } + + /** + * @return L'infobulle / description de la suggestion, ou {@code null} si non dĂ©finie. + */ + public String getTooltip() { + return tooltip; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Suggestion that)) return false; + return Objects.equals(value, that.value) && Objects.equals(tooltip, that.tooltip); + } + + @Override + public int hashCode() { + return Objects.hash(value, tooltip); + } + + @Override + public String toString() { + return tooltip == null ? value : value + " (" + tooltip + ")"; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/api/suggestion/SuggestionProvider.java b/src/main/java/fr/luc/bettermccommands/api/suggestion/SuggestionProvider.java new file mode 100644 index 0000000..234a3d6 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/api/suggestion/SuggestionProvider.java @@ -0,0 +1,59 @@ +package fr.luc.bettermccommands.api.suggestion; + +import fr.luc.bettermccommands.api.CommandContext; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Fournisseur dynamique de suggestions pour l'auto-complĂ©tion (Tab-Complete). + */ +@FunctionalInterface +public interface SuggestionProvider { + + /** + * Calcule et retourne la liste des suggestions pour un argument donnĂ© dans un contexte prĂ©cis. + * + * @param context Le contexte actuel de la commande. + * @param currentInput La saisie en cours de frappe par l'utilisateur. + * @return La collection de suggestions correspondantes. + */ + Collection getSuggestions(CommandContext context, String currentInput); + + /** + * CrĂ©e un fournisseur Ă  partir d'une liste statique de chaĂźnes de caractĂšres. + * + * @param values Les valeurs possibles. + * @return Un {@link SuggestionProvider} filtrant les valeurs par prĂ©fixe insensible Ă  la casse. + */ + static SuggestionProvider strings(String... values) { + return strings(Arrays.asList(values)); + } + + /** + * CrĂ©e un fournisseur Ă  partir d'une collection statique de chaĂźnes. + * + * @param values La collection de valeurs textuelles. + * @return Un {@link SuggestionProvider} filtrant par prĂ©fixe. + */ + static SuggestionProvider strings(Collection values) { + return (context, currentInput) -> { + String lower = currentInput == null ? "" : currentInput.toLowerCase(); + return values.stream() + .filter(v -> v.toLowerCase().startsWith(lower)) + .map(Suggestion::of) + .collect(Collectors.toList()); + }; + } + + /** + * CrĂ©e un fournisseur vide ne retournant aucune suggestion. + * + * @return Un {@link SuggestionProvider} vide. + */ + static SuggestionProvider empty() { + return (context, currentInput) -> List.of(); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/ArgumentType.java b/src/main/java/fr/luc/bettermccommands/argument/ArgumentType.java new file mode 100644 index 0000000..2b8062d --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/ArgumentType.java @@ -0,0 +1,55 @@ +package fr.luc.bettermccommands.argument; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.suggestion.Suggestion; + +import java.util.Collection; +import java.util.Collections; + +/** + * Contrat pour convertir une chaĂźne de caractĂšres en un objet Java typĂ© et fournir des suggestions de complĂ©tion. + * + * @param Le type de donnĂ©es retournĂ© par le parseur. + */ +public interface ArgumentType { + + /** + * Analyse et convertit l'entrĂ©e textuelle en objet Java du type {@link T}. + * + * @param argumentName Le nom de l'argument en cours de traitement. + * @param input La valeur textuelle brute entrĂ©e par le joueur. + * @param context Le contexte d'exĂ©cution actuel. + * @return L'objet converti et validĂ©. + * @throws CommandArgumentParseException si la chaĂźne ne respecte pas le format attendu. + */ + T parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException; + + /** + * Fournit les suggestions d'auto-complĂ©tion (Tab-Complete) pour ce type d'argument. + * + * @param context Le contexte d'exĂ©cution de la commande. + * @param currentInput La chaĂźne actuellement tapĂ©e par l'utilisateur. + * @return La collection de suggestions proposĂ©es. + */ + default Collection suggest(CommandContext context, String currentInput) { + return Collections.emptyList(); + } + + /** + * Retourne le nom lisible de ce type (ex: "Entier", "Joueur", "Texte"). + * + * @return Le nom convivial du type. + */ + String getTypeName(); + + /** + * Retourne le format d'affichage pour l'aide/usage (ex: "", "[quantitĂ©]"). + * + * @param argumentName Le nom de l'argument. + * @param optional Indique si l'argument est optionnel. + * @return La chaĂźne d'affichage d'usage. + */ + default String getUsagePlaceholder(String argumentName, boolean optional) { + return optional ? "[" + argumentName + "]" : "<" + argumentName + ">"; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/Arguments.java b/src/main/java/fr/luc/bettermccommands/argument/Arguments.java new file mode 100644 index 0000000..46c67e3 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/Arguments.java @@ -0,0 +1,188 @@ +package fr.luc.bettermccommands.argument; + +import fr.luc.bettermccommands.argument.type.*; +import org.bukkit.Location; +import org.bukkit.OfflinePlayer; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.time.Duration; + +/** + * Usine statique (Factory) simplifiant la dĂ©claration et la crĂ©ation des arguments de commande. + */ +public final class Arguments { + + private Arguments() { + // EmpĂȘche l'instanciation + } + + /** + * CrĂ©e un argument de type mot simple (dĂ©limitĂ© par un espace). + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument word(String name) { + return new CommandArgument<>(name, StringArgument.word()); + } + + /** + * CrĂ©e un argument de type chaĂźne de caractĂšres. + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument string(String name) { + return new CommandArgument<>(name, StringArgument.string()); + } + + /** + * CrĂ©e un argument de type chaĂźne gourmande (consommant tous les mots jusqu'Ă  la fin de la commande). + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ© en mode greedy. + */ + public static CommandArgument greedyString(String name) { + return new CommandArgument<>(name, StringArgument.string()).greedy(); + } + + /** + * CrĂ©e un argument de type nombre entier non restreint. + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument integer(String name) { + return new CommandArgument<>(name, IntegerArgument.integer()); + } + + /** + * CrĂ©e un argument de type nombre entier avec une borne minimale. + * + * @param name Le nom de l'argument. + * @param min La valeur minimale autorisĂ©e. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument integer(String name, int min) { + return new CommandArgument<>(name, IntegerArgument.min(min)); + } + + /** + * CrĂ©e un argument de type nombre entier bornĂ© entre min et max. + * + * @param name Le nom de l'argument. + * @param min La valeur minimale. + * @param max La valeur maximale. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument integer(String name, int min, int max) { + return new CommandArgument<>(name, IntegerArgument.range(min, max)); + } + + /** + * CrĂ©e un argument de type nombre dĂ©cimal (double). + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument decimal(String name) { + return new CommandArgument<>(name, DoubleArgument.number()); + } + + /** + * CrĂ©e un argument de type nombre dĂ©cimal bornĂ©. + * + * @param name Le nom de l'argument. + * @param min La valeur minimale. + * @param max La valeur maximale. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument decimal(String name, double min, double max) { + return new CommandArgument<>(name, DoubleArgument.range(min, max)); + } + + /** + * CrĂ©e un argument boolĂ©en (true/false, oui/non). + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument bool(String name) { + return new CommandArgument<>(name, BooleanArgument.bool()); + } + + /** + * CrĂ©e un argument pour cibler un joueur connectĂ©. + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument player(String name) { + return new CommandArgument<>(name, PlayerArgument.player()); + } + + /** + * CrĂ©e un argument pour cibler un joueur hors-ligne ou en ligne. + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument offlinePlayer(String name) { + return new CommandArgument<>(name, OfflinePlayerArgument.offlinePlayer()); + } + + /** + * CrĂ©e un argument pour cibler un monde Bukkit. + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument world(String name) { + return new CommandArgument<>(name, WorldArgument.world()); + } + + /** + * CrĂ©e un argument pour des coordonnĂ©es gĂ©ographiques. + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument location(String name) { + return new CommandArgument<>(name, LocationArgument.location()); + } + + /** + * CrĂ©e un argument pour une durĂ©e temporelle (ex: 30s, 15m, 2h). + * + * @param name Le nom de l'argument. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument duration(String name) { + return new CommandArgument<>(name, DurationArgument.duration()); + } + + /** + * CrĂ©e un argument basĂ© sur un enum Java arbitraire. + * + * @param name Le nom de l'argument. + * @param enumClass La classe de l'enum. + * @param Le type de l'enum. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static > CommandArgument enumOf(String name, Class enumClass) { + return new CommandArgument<>(name, EnumArgument.of(enumClass)); + } + + /** + * CrĂ©e un argument avec un type personnalisĂ©. + * + * @param name Le nom de l'argument. + * @param type L'instance de {@link ArgumentType}. + * @param Le type de l'objet. + * @return Le {@link CommandArgument} configurĂ©. + */ + public static CommandArgument custom(String name, ArgumentType type) { + return new CommandArgument<>(name, type); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/CommandArgument.java b/src/main/java/fr/luc/bettermccommands/argument/CommandArgument.java new file mode 100644 index 0000000..a8e60c4 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/CommandArgument.java @@ -0,0 +1,184 @@ +package fr.luc.bettermccommands.argument; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.suggestion.Suggestion; +import fr.luc.bettermccommands.api.suggestion.SuggestionProvider; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * ReprĂ©sente la dĂ©finition d'un argument au sein d'une commande ou sous-commande. + * + * @param Le type de l'argument. + */ +public class CommandArgument { + + private final String name; + private final ArgumentType type; + private String description; + private boolean optional; + private Supplier defaultValueSupplier; + private SuggestionProvider customSuggestionProvider; + private boolean greedy; + + /** + * CrĂ©e un argument obligatoire avec son nom et son type. + * + * @param name Le nom unique de l'argument au sein du nƓud de commande. + * @param type Le type de conversion et de validation de l'argument. + */ + public CommandArgument(String name, ArgumentType type) { + this.name = Objects.requireNonNull(name, "Argument name cannot be null"); + this.type = Objects.requireNonNull(type, "Argument type cannot be null"); + this.optional = false; + this.greedy = false; + } + + /** + * DĂ©finit la description de cet argument. + * + * @param description Le texte descriptif. + * @return Cette instance d'argument pour chaĂźnage. + */ + public CommandArgument description(String description) { + this.description = description; + return this; + } + + /** + * Marque cet argument comme optionnel (sans valeur par dĂ©faut, retourne null si omis). + * + * @return Cette instance d'argument. + */ + public CommandArgument optional() { + this.optional = true; + this.defaultValueSupplier = () -> null; + return this; + } + + /** + * Marque cet argument comme optionnel avec une valeur par dĂ©faut constante. + * + * @param defaultValue La valeur par dĂ©faut Ă  appliquer si l'argument est omis. + * @return Cette instance d'argument. + */ + public CommandArgument defaultValue(T defaultValue) { + this.optional = true; + this.defaultValueSupplier = () -> defaultValue; + return this; + } + + /** + * Marque cet argument comme optionnel avec un fournisseur dynamique de valeur par dĂ©faut. + * + * @param supplier Le fournisseur de valeur par dĂ©faut. + * @return Cette instance d'argument. + */ + public CommandArgument defaultValue(Supplier supplier) { + this.optional = true; + this.defaultValueSupplier = supplier; + return this; + } + + /** + * DĂ©finit un fournisseur de suggestions personnalisĂ© pour surcharger celui du type de base. + * + * @param provider Le fournisseur de suggestions. + * @return Cette instance d'argument. + */ + public CommandArgument suggest(SuggestionProvider provider) { + this.customSuggestionProvider = provider; + return this; + } + + /** + * DĂ©finit une liste statique de suggestions possibles pour cet argument. + * + * @param suggestions Les valeurs suggĂ©rĂ©es. + * @return Cette instance d'argument. + */ + public CommandArgument suggest(String... suggestions) { + this.customSuggestionProvider = SuggestionProvider.strings(suggestions); + return this; + } + + /** + * Marque l'argument comme "greedy" (il consomme tous les mots restants jusqu'Ă  la fin de la commande). + * Utile pour les messages de broadcast, raisons de bannissement, descriptions longues. + * + * @return Cette instance d'argument. + */ + public CommandArgument greedy() { + this.greedy = true; + return this; + } + + /** + * @return Le nom de l'argument. + */ + public String getName() { + return name; + } + + /** + * @return Le type associĂ©. + */ + public ArgumentType getType() { + return type; + } + + /** + * @return La description explicative de l'argument. + */ + public String getDescription() { + return description; + } + + /** + * @return true si l'argument est optionnel, sinon false. + */ + public boolean isOptional() { + return optional; + } + + /** + * @return true si l'argument est greedy (consomme le reste de la ligne). + */ + public boolean isGreedy() { + return greedy; + } + + /** + * Calcule la valeur par dĂ©faut pour cet argument. + * + * @return La valeur par dĂ©faut, ou {@code null}. + */ + public T getDefaultValue() { + return defaultValueSupplier != null ? defaultValueSupplier.get() : null; + } + + /** + * RĂ©cupĂšre les suggestions applicables pour cet argument dans le contexte donnĂ©. + * + * @param context Le contexte d'exĂ©cution. + * @param currentInput La saisie en cours. + * @return Les suggestions calculĂ©es. + */ + public Collection getSuggestions(CommandContext context, String currentInput) { + if (customSuggestionProvider != null) { + return customSuggestionProvider.getSuggestions(context, currentInput); + } + return type.suggest(context, currentInput); + } + + /** + * Retourne la reprĂ©sentation textuelle de l'argument pour l'aide. + * + * @return Ex: "" ou "[quantite]" + */ + public String getUsage() { + return type.getUsagePlaceholder(name, optional); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/CommandArgumentParseException.java b/src/main/java/fr/luc/bettermccommands/argument/CommandArgumentParseException.java new file mode 100644 index 0000000..6a3ebce --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/CommandArgumentParseException.java @@ -0,0 +1,47 @@ +package fr.luc.bettermccommands.argument; + +/** + * Exception levĂ©e lorsqu'un argument textuel ne peut pas ĂȘtre converti vers son type cible. + */ +public class CommandArgumentParseException extends Exception { + + private final String argumentName; + private final String rawInput; + private final String expectedTypeName; + + /** + * CrĂ©e une nouvelle exception de parsing d'argument. + * + * @param argumentName Le nom de l'argument concernĂ©. + * @param rawInput La chaĂźne brute fournie par l'utilisateur. + * @param expectedTypeName Le nom convivial du type attendu. + * @param message Le message d'erreur explicatif. + */ + public CommandArgumentParseException(String argumentName, String rawInput, String expectedTypeName, String message) { + super(message); + this.argumentName = argumentName; + this.rawInput = rawInput; + this.expectedTypeName = expectedTypeName; + } + + /** + * @return Le nom de l'argument. + */ + public String getArgumentName() { + return argumentName; + } + + /** + * @return La saisie brute entrĂ©e par le joueur. + */ + public String getRawInput() { + return rawInput; + } + + /** + * @return Le nom du type cible attendu. + */ + public String getExpectedTypeName() { + return expectedTypeName; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/type/BooleanArgument.java b/src/main/java/fr/luc/bettermccommands/argument/type/BooleanArgument.java new file mode 100644 index 0000000..961b04f --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/type/BooleanArgument.java @@ -0,0 +1,52 @@ +package fr.luc.bettermccommands.argument.type; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.suggestion.Suggestion; +import fr.luc.bettermccommands.argument.ArgumentType; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; + +import java.util.Collection; +import java.util.List; + +/** + * Type d'argument pour les valeurs boolĂ©ennes (true/false, oui/non, on/off, 1/0). + */ +public class BooleanArgument implements ArgumentType { + + private static final BooleanArgument INSTANCE = new BooleanArgument(); + + /** + * @return L'instance singleton de {@link BooleanArgument}. + */ + public static BooleanArgument bool() { + return INSTANCE; + } + + @Override + public Boolean parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + String lower = input.trim().toLowerCase(); + if (lower.equals("true") || lower.equals("oui") || lower.equals("on") || lower.equals("1") || lower.equals("yes")) { + return true; + } + if (lower.equals("false") || lower.equals("non") || lower.equals("off") || lower.equals("0") || lower.equals("no")) { + return false; + } + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "'" + input + "' n'est pas un boolĂ©en valide (attendu: true/false, oui/non, on/off)."); + } + + @Override + public Collection suggest(CommandContext context, String currentInput) { + String lower = currentInput == null ? "" : currentInput.toLowerCase(); + List options = List.of("true", "false", "oui", "non"); + return options.stream() + .filter(opt -> opt.startsWith(lower)) + .map(Suggestion::of) + .toList(); + } + + @Override + public String getTypeName() { + return "BoolĂ©en"; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/type/DoubleArgument.java b/src/main/java/fr/luc/bettermccommands/argument/type/DoubleArgument.java new file mode 100644 index 0000000..40b9b52 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/type/DoubleArgument.java @@ -0,0 +1,96 @@ +package fr.luc.bettermccommands.argument.type; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.argument.ArgumentType; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; + +/** + * Type d'argument pour les nombres dĂ©cimaux (double) avec bornes optionnelles. + */ +public class DoubleArgument implements ArgumentType { + + private final Double min; + private final Double max; + + /** + * CrĂ©e un argument dĂ©cimal sans bornes. + */ + public DoubleArgument() { + this(null, null); + } + + /** + * CrĂ©e un argument dĂ©cimal avec bornes. + * + * @param min La valeur minimale autorisĂ©e. + * @param max La valeur maximale autorisĂ©e. + */ + public DoubleArgument(Double min, Double max) { + this.min = min; + this.max = max; + } + + /** + * CrĂ©e un type d'argument dĂ©cimal standard. + * + * @return L'instance configurĂ©e. + */ + public static DoubleArgument number() { + return new DoubleArgument(); + } + + /** + * CrĂ©e un type d'argument dĂ©cimal avec une valeur minimale. + * + * @param min Le minimum. + * @return L'instance configurĂ©e. + */ + public static DoubleArgument min(double min) { + return new DoubleArgument(min, null); + } + + /** + * CrĂ©e un type d'argument dĂ©cimal avec un intervalle fermĂ© [min, max]. + * + * @param min Le minimum. + * @param max Le maximum. + * @return L'instance configurĂ©e. + */ + public static DoubleArgument range(double min, double max) { + return new DoubleArgument(min, max); + } + + @Override + public Double parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + double val; + try { + val = Double.parseDouble(input.replace(',', '.')); + } catch (NumberFormatException e) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "'" + input + "' n'est pas un nombre dĂ©cimal valide."); + } + + if (min != null && val < min) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Le nombre doit ĂȘtre supĂ©rieur ou Ă©gal Ă  " + min + " (valeur reçue: " + val + ")."); + } + if (max != null && val > max) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Le nombre doit ĂȘtre infĂ©rieur ou Ă©gal Ă  " + max + " (valeur reçue: " + val + ")."); + } + + return val; + } + + @Override + public String getTypeName() { + if (min != null && max != null) { + return "DĂ©cimal [" + min + ".." + max + "]"; + } else if (min != null) { + return "DĂ©cimal (>=" + min + ")"; + } else if (max != null) { + return "DĂ©cimal (<=" + max + ")"; + } + return "DĂ©cimal"; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/type/DurationArgument.java b/src/main/java/fr/luc/bettermccommands/argument/type/DurationArgument.java new file mode 100644 index 0000000..56338cb --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/type/DurationArgument.java @@ -0,0 +1,79 @@ +package fr.luc.bettermccommands.argument.type; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.suggestion.Suggestion; +import fr.luc.bettermccommands.argument.ArgumentType; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Type d'argument pour analyser des durĂ©es temporelles textuelles (ex: "30s", "10m", "2h", "1d", "7d"). + */ +public class DurationArgument implements ArgumentType { + + private static final DurationArgument INSTANCE = new DurationArgument(); + private static final Pattern DURATION_PATTERN = Pattern.compile("(\\d+)\\s*([smhdw]|sec|min|heure|jour|semaine|seconds?|minutes?|hours?|days?|weeks?)", Pattern.CASE_INSENSITIVE); + + /** + * @return L'instance singleton de {@link DurationArgument}. + */ + public static DurationArgument duration() { + return INSTANCE; + } + + @Override + public Duration parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + String clean = input.trim(); + if (clean.matches("^\\d+$")) { + // Nombre seul = secondes par dĂ©faut + long seconds = Long.parseLong(clean); + return Duration.ofSeconds(seconds); + } + + Matcher matcher = DURATION_PATTERN.matcher(clean); + long totalSeconds = 0; + boolean found = false; + + while (matcher.find()) { + found = true; + long amount = Long.parseLong(matcher.group(1)); + String unit = matcher.group(2).toLowerCase(); + + switch (unit) { + case "s", "sec", "second", "seconds" -> totalSeconds += amount; + case "m", "min", "minute", "minutes" -> totalSeconds += amount * 60; + case "h", "heure", "heures", "hour", "hours" -> totalSeconds += amount * 3600; + case "d", "j", "jour", "jours", "day", "days" -> totalSeconds += amount * 86400; + case "w", "semaine", "semaines", "week", "weeks" -> totalSeconds += amount * 604800; + default -> throw new CommandArgumentParseException(argumentName, input, getTypeName(), "UnitĂ© de durĂ©e inconnue: " + unit); + } + } + + if (!found || totalSeconds <= 0) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Format de durĂ©e invalide '" + input + "' (exemples valides: 30s, 15m, 2h, 1d)."); + } + + return Duration.ofSeconds(totalSeconds); + } + + @Override + public Collection suggest(CommandContext context, String currentInput) { + String lower = currentInput == null ? "" : currentInput.toLowerCase(); + List common = List.of("30s", "1m", "5m", "15m", "30m", "1h", "12h", "1d", "7d"); + return common.stream() + .filter(s -> s.startsWith(lower)) + .map(s -> Suggestion.of(s, "DurĂ©e " + s)) + .toList(); + } + + @Override + public String getTypeName() { + return "DurĂ©e"; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/type/EnumArgument.java b/src/main/java/fr/luc/bettermccommands/argument/type/EnumArgument.java new file mode 100644 index 0000000..486cfb8 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/type/EnumArgument.java @@ -0,0 +1,70 @@ +package fr.luc.bettermccommands.argument.type; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.suggestion.Suggestion; +import fr.luc.bettermccommands.argument.ArgumentType; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Type d'argument gĂ©nĂ©rique permettant de parser n'importe quel {@link Enum} Java. + * + * @param Le type d'enum. + */ +public class EnumArgument> implements ArgumentType { + + private final Class enumClass; + + /** + * CrĂ©e un parseur d'enum pour la classe donnĂ©e. + * + * @param enumClass La classe d'enum. + */ + public EnumArgument(Class enumClass) { + this.enumClass = Objects.requireNonNull(enumClass, "enumClass cannot be null"); + } + + /** + * Fabrique un nouveau type d'argument enum. + * + * @param enumClass La classe d'enum. + * @param Le type de l'enum. + * @return L'instance configurĂ©e de {@link EnumArgument}. + */ + public static > EnumArgument of(Class enumClass) { + return new EnumArgument<>(enumClass); + } + + @Override + public E parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + for (E constant : enumClass.getEnumConstants()) { + if (constant.name().equalsIgnoreCase(input)) { + return constant; + } + } + String validValues = Arrays.stream(enumClass.getEnumConstants()) + .map(e -> e.name().toLowerCase()) + .collect(Collectors.joining(", ")); + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Valeur invalide '" + input + "'. Valeurs attendues : [" + validValues + "]."); + } + + @Override + public Collection suggest(CommandContext context, String currentInput) { + String lower = currentInput == null ? "" : currentInput.toLowerCase(); + return Arrays.stream(enumClass.getEnumConstants()) + .map(e -> e.name().toLowerCase()) + .filter(name -> name.startsWith(lower)) + .map(Suggestion::of) + .toList(); + } + + @Override + public String getTypeName() { + return enumClass.getSimpleName(); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/type/IntegerArgument.java b/src/main/java/fr/luc/bettermccommands/argument/type/IntegerArgument.java new file mode 100644 index 0000000..ede16bc --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/type/IntegerArgument.java @@ -0,0 +1,96 @@ +package fr.luc.bettermccommands.argument.type; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.argument.ArgumentType; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; + +/** + * Type d'argument pour les nombres entiers avec bornes minimale et maximale optionnelles. + */ +public class IntegerArgument implements ArgumentType { + + private final Integer min; + private final Integer max; + + /** + * CrĂ©e un argument entier sans restriction de bornes. + */ + public IntegerArgument() { + this(null, null); + } + + /** + * CrĂ©e un argument entier avec bornes optionnelles. + * + * @param min La valeur minimale autorisĂ©e (ou null). + * @param max La valeur maximale autorisĂ©e (ou null). + */ + public IntegerArgument(Integer min, Integer max) { + this.min = min; + this.max = max; + } + + /** + * CrĂ©e un type d'argument entier non bornĂ©. + * + * @return L'instance de {@link IntegerArgument}. + */ + public static IntegerArgument integer() { + return new IntegerArgument(); + } + + /** + * CrĂ©e un type d'argument entier avec un minimum. + * + * @param min La valeur minimale. + * @return L'instance configurĂ©e. + */ + public static IntegerArgument min(int min) { + return new IntegerArgument(min, null); + } + + /** + * CrĂ©e un type d'argument entier avec un intervalle fermĂ© [min, max]. + * + * @param min La valeur minimale. + * @param max La valeur maximale. + * @return L'instance configurĂ©e. + */ + public static IntegerArgument range(int min, int max) { + return new IntegerArgument(min, max); + } + + @Override + public Integer parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + int val; + try { + val = Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "'" + input + "' n'est pas un nombre entier valide."); + } + + if (min != null && val < min) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Le nombre doit ĂȘtre supĂ©rieur ou Ă©gal Ă  " + min + " (valeur reçue: " + val + ")."); + } + if (max != null && val > max) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Le nombre doit ĂȘtre infĂ©rieur ou Ă©gal Ă  " + max + " (valeur reçue: " + val + ")."); + } + + return val; + } + + @Override + public String getTypeName() { + if (min != null && max != null) { + return "Entier [" + min + ".." + max + "]"; + } else if (min != null) { + return "Entier (>=" + min + ")"; + } else if (max != null) { + return "Entier (<=" + max + ")"; + } + return "Entier"; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/type/LocationArgument.java b/src/main/java/fr/luc/bettermccommands/argument/type/LocationArgument.java new file mode 100644 index 0000000..5d5f334 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/type/LocationArgument.java @@ -0,0 +1,78 @@ +package fr.luc.bettermccommands.argument.type; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.argument.ArgumentType; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; + +/** + * Type d'argument pour parser des coordonnĂ©es gĂ©ographiques (format "x,y,z" ou "x,y,z,monde" avec support des coordonnĂ©es relatives '~'). + */ +public class LocationArgument implements ArgumentType { + + private static final LocationArgument INSTANCE = new LocationArgument(); + + /** + * @return L'instance singleton de {@link LocationArgument}. + */ + public static LocationArgument location() { + return INSTANCE; + } + + @Override + public Location parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + String[] parts = input.split(","); + if (parts.length < 3) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Les coordonnĂ©es doivent ĂȘtre sous la forme 'x,y,z' ou 'x,y,z,monde'."); + } + + World world = null; + Location reference = null; + if (context.isPlayer()) { + Player p = context.getPlayer(); + reference = p.getLocation(); + world = reference.getWorld(); + } + + if (parts.length >= 4) { + world = org.bukkit.Bukkit.getWorld(parts[3].trim()); + if (world == null) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Monde introuvable: '" + parts[3].trim() + "'."); + } + } + + if (world == null) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Impossible de dĂ©terminer le monde (Ă©metteur non-joueur et aucun monde spĂ©cifiĂ©)."); + } + + try { + double x = parseCoord(parts[0].trim(), reference != null ? reference.getX() : 0); + double y = parseCoord(parts[1].trim(), reference != null ? reference.getY() : 0); + double z = parseCoord(parts[2].trim(), reference != null ? reference.getZ() : 0); + return new Location(world, x, y, z); + } catch (NumberFormatException e) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "CoordonnĂ©es invalides dans '" + input + "'."); + } + } + + private double parseCoord(String val, double origin) { + if (val.startsWith("~")) { + if (val.length() == 1) { + return origin; + } + return origin + Double.parseDouble(val.substring(1)); + } + return Double.parseDouble(val); + } + + @Override + public String getTypeName() { + return "CoordonnĂ©es (x,y,z)"; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/type/OfflinePlayerArgument.java b/src/main/java/fr/luc/bettermccommands/argument/type/OfflinePlayerArgument.java new file mode 100644 index 0000000..07fffaa --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/type/OfflinePlayerArgument.java @@ -0,0 +1,71 @@ +package fr.luc.bettermccommands.argument.type; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.suggestion.Suggestion; +import fr.luc.bettermccommands.argument.ArgumentType; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +/** + * Type d'argument pour cibler un joueur hors-ligne ou en ligne (par nom ou UUID). + */ +public class OfflinePlayerArgument implements ArgumentType { + + private static final OfflinePlayerArgument INSTANCE = new OfflinePlayerArgument(); + + /** + * @return L'instance singleton de {@link OfflinePlayerArgument}. + */ + public static OfflinePlayerArgument offlinePlayer() { + return INSTANCE; + } + + @Override + @SuppressWarnings("deprecation") + public OfflinePlayer parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + // Test si entrĂ©e au format UUID + try { + UUID uuid = UUID.fromString(input); + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); + if (offlinePlayer.hasPlayedBefore() || offlinePlayer.isOnline()) { + return offlinePlayer; + } + } catch (IllegalArgumentException ignored) { + // Pas un UUID, recherche par nom + } + + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(input); + if (!offlinePlayer.hasPlayedBefore() && !offlinePlayer.isOnline()) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Le joueur '" + input + "' n'a jamais jouĂ© sur ce serveur."); + } + + return offlinePlayer; + } + + @Override + public Collection suggest(CommandContext context, String currentInput) { + String lower = currentInput == null ? "" : currentInput.toLowerCase(); + try { + return Arrays.stream(Bukkit.getOfflinePlayers()) + .map(OfflinePlayer::getName) + .filter(name -> name != null && name.toLowerCase().startsWith(lower)) + .map(Suggestion::of) + .toList(); + } catch (Throwable t) { + return List.of(); + } + } + + @Override + public String getTypeName() { + return "Joueur (Hors-Ligne)"; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/type/PlayerArgument.java b/src/main/java/fr/luc/bettermccommands/argument/type/PlayerArgument.java new file mode 100644 index 0000000..434924d --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/type/PlayerArgument.java @@ -0,0 +1,62 @@ +package fr.luc.bettermccommands.argument.type; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.suggestion.Suggestion; +import fr.luc.bettermccommands.argument.ArgumentType; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Collection; +import java.util.List; + +/** + * Type d'argument pour cibler un joueur connectĂ© en ligne. + */ +public class PlayerArgument implements ArgumentType { + + private static final PlayerArgument INSTANCE = new PlayerArgument(); + + /** + * @return L'instance singleton de {@link PlayerArgument}. + */ + public static PlayerArgument player() { + return INSTANCE; + } + + @Override + public Player parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + Player player = Bukkit.getPlayerExact(input); + if (player == null) { + // Recherche par dĂ©but de pseudo si exact Ă©choue + player = Bukkit.getPlayer(input); + } + + if (player == null || !player.isOnline()) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Le joueur '" + input + "' est introuvable ou hors-ligne."); + } + + return player; + } + + @Override + public Collection suggest(CommandContext context, String currentInput) { + String lower = currentInput == null ? "" : currentInput.toLowerCase(); + try { + return Bukkit.getOnlinePlayers().stream() + .map(Player::getName) + .filter(name -> name.toLowerCase().startsWith(lower)) + .map(Suggestion::of) + .toList(); + } catch (Throwable t) { + // Utile lors des tests unitaires si Bukkit.getOnlinePlayers() n'est pas initialisĂ© + return List.of(); + } + } + + @Override + public String getTypeName() { + return "Joueur"; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/type/StringArgument.java b/src/main/java/fr/luc/bettermccommands/argument/type/StringArgument.java new file mode 100644 index 0000000..4592432 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/type/StringArgument.java @@ -0,0 +1,60 @@ +package fr.luc.bettermccommands.argument.type; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.argument.ArgumentType; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; + +/** + * Type d'argument pour les chaĂźnes de caractĂšres (mot unique ou chaĂźne libre). + */ +public class StringArgument implements ArgumentType { + + private final String typeName; + + /** + * CrĂ©e un type d'argument pour une chaĂźne de caractĂšres simple. + */ + public StringArgument() { + this("Texte"); + } + + /** + * CrĂ©e un type d'argument texte avec un nom explicite. + * + * @param typeName Le nom lisible. + */ + public StringArgument(String typeName) { + this.typeName = typeName != null ? typeName : "Texte"; + } + + /** + * CrĂ©e une instance standard pour un mot unique. + * + * @return L'instance de {@link StringArgument}. + */ + public static StringArgument word() { + return new StringArgument("Mot"); + } + + /** + * CrĂ©e une instance standard pour une chaĂźne de caractĂšres libre. + * + * @return L'instance de {@link StringArgument}. + */ + public static StringArgument string() { + return new StringArgument("Texte"); + } + + @Override + public String parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + if (input == null || input.isEmpty()) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), "Le texte ne peut pas ĂȘtre vide."); + } + return input; + } + + @Override + public String getTypeName() { + return typeName; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/argument/type/WorldArgument.java b/src/main/java/fr/luc/bettermccommands/argument/type/WorldArgument.java new file mode 100644 index 0000000..457dac0 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/argument/type/WorldArgument.java @@ -0,0 +1,55 @@ +package fr.luc.bettermccommands.argument.type; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.suggestion.Suggestion; +import fr.luc.bettermccommands.argument.ArgumentType; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; +import org.bukkit.Bukkit; +import org.bukkit.World; + +import java.util.Collection; +import java.util.List; + +/** + * Type d'argument pour cibler un monde Bukkit chargĂ©. + */ +public class WorldArgument implements ArgumentType { + + private static final WorldArgument INSTANCE = new WorldArgument(); + + /** + * @return L'instance singleton de {@link WorldArgument}. + */ + public static WorldArgument world() { + return INSTANCE; + } + + @Override + public World parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + World world = Bukkit.getWorld(input); + if (world == null) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), + "Le monde '" + input + "' n'existe pas ou n'est pas chargĂ©."); + } + return world; + } + + @Override + public Collection suggest(CommandContext context, String currentInput) { + String lower = currentInput == null ? "" : currentInput.toLowerCase(); + try { + return Bukkit.getWorlds().stream() + .map(World::getName) + .filter(name -> name.toLowerCase().startsWith(lower)) + .map(Suggestion::of) + .toList(); + } catch (Throwable t) { + return List.of(); + } + } + + @Override + public String getTypeName() { + return "Monde"; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/builder/AbstractCommandBuilder.java b/src/main/java/fr/luc/bettermccommands/builder/AbstractCommandBuilder.java new file mode 100644 index 0000000..0f5d8a1 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/builder/AbstractCommandBuilder.java @@ -0,0 +1,286 @@ +package fr.luc.bettermccommands.builder; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.CommandExecutor; +import fr.luc.bettermccommands.api.CommandNode; +import fr.luc.bettermccommands.api.CommandSenderType; +import fr.luc.bettermccommands.argument.CommandArgument; +import fr.luc.bettermccommands.event.*; + +import java.time.Duration; +import java.util.*; + +/** + * Classe abstraite de base fournissant le DSL fluide pour configurer un nƓud de commande. + * + * @param Le type concret du Builder (pour permettre le chaĂźnage fluide). + * @param Le type concret de nƓud produit (Command ou CommandNode). + */ +@SuppressWarnings("unchecked") +public abstract class AbstractCommandBuilder, N extends CommandNode> { + + protected final String name; + protected String description; + protected String permission; + protected CommandSenderType senderType = CommandSenderType.ALL; + protected final Set aliases = new LinkedHashSet<>(); + protected final List> arguments = new ArrayList<>(); + protected final List subCommandBuilders = new ArrayList<>(); + protected CommandExecutor executor; + protected Duration cooldown = Duration.ZERO; + protected String cooldownBypassPermission; + + protected final List> preExecuteListeners = new ArrayList<>(); + protected final List> postExecuteListeners = new ArrayList<>(); + protected final List> permissionDeniedListeners = new ArrayList<>(); + protected final List> syntaxErrorListeners = new ArrayList<>(); + protected final List> cooldownListeners = new ArrayList<>(); + protected final List> tabCompleteListeners = new ArrayList<>(); + + /** + * Initialise le constructeur avec le nom du nƓud. + * + * @param name Le nom de la commande ou sous-commande. + */ + public AbstractCommandBuilder(String name) { + this.name = Objects.requireNonNull(name, "Name cannot be null"); + } + + /** + * DĂ©finit la description de la commande. + * + * @param description Le texte descriptif. + * @return Cette instance du builder pour chaĂźnage. + */ + public B description(String description) { + this.description = description; + return (B) this; + } + + /** + * DĂ©finit la permission Bukkit requise pour exĂ©cuter cette commande. + * + * @param permission La chaĂźne de permission (ex: "bettermc.demo"). + * @return Cette instance du builder. + */ + public B permission(String permission) { + this.permission = permission; + return (B) this; + } + + /** + * Restreint l'exĂ©cution de la commande Ă  un type d'Ă©metteur spĂ©cifique. + * + * @param senderType Le type d'Ă©metteur requis. + * @return Cette instance du builder. + */ + public B senderType(CommandSenderType senderType) { + this.senderType = Objects.requireNonNull(senderType, "senderType cannot be null"); + return (B) this; + } + + /** + * Restreint l'exĂ©cution de la commande exclusivement aux joueurs connectĂ©s. + * + * @return Cette instance du builder. + */ + public B playerOnly() { + this.senderType = CommandSenderType.PLAYER_ONLY; + return (B) this; + } + + /** + * Restreint l'exĂ©cution de la commande exclusivement Ă  la console du serveur. + * + * @return Cette instance du builder. + */ + public B consoleOnly() { + this.senderType = CommandSenderType.CONSOLE_ONLY; + return (B) this; + } + + /** + * Ajoute un ou plusieurs alias pour cette commande. + * + * @param aliases Les alias supplĂ©mentaires. + * @return Cette instance du builder. + */ + public B aliases(String... aliases) { + if (aliases != null) { + for (String alias : aliases) { + if (alias != null && !alias.trim().isEmpty()) { + this.aliases.add(alias.trim().toLowerCase()); + } + } + } + return (B) this; + } + + /** + * Ajoute un argument typĂ© Ă  la commande. + * + * @param argument L'argument Ă  ajouter. + * @return Cette instance du builder. + */ + public B argument(CommandArgument argument) { + this.arguments.add(Objects.requireNonNull(argument, "Argument cannot be null")); + return (B) this; + } + + /** + * Ajoute une sous-commande via son builder. + * + * @param subCommandBuilder Le builder de la sous-commande. + * @return Cette instance du builder. + */ + public B subcommand(SubCommandBuilder subCommandBuilder) { + this.subCommandBuilders.add(Objects.requireNonNull(subCommandBuilder, "SubCommandBuilder cannot be null")); + return (B) this; + } + + /** + * DĂ©finit le gestionnaire d'exĂ©cution mĂ©tier appelĂ© lors de l'invocation. + * + * @param executor La fonction d'exĂ©cution de la commande. + * @return Cette instance du builder. + */ + public B executes(CommandExecutor executor) { + this.executor = executor; + return (B) this; + } + + /** + * DĂ©finit un temps de recharge (Cooldown) entre deux exĂ©cutions par un mĂȘme joueur. + * + * @param cooldown La durĂ©e du cooldown. + * @return Cette instance du builder. + */ + public B cooldown(Duration cooldown) { + this.cooldown = cooldown != null ? cooldown : Duration.ZERO; + return (B) this; + } + + /** + * DĂ©finit un temps de recharge en secondes. + * + * @param seconds Le nombre de secondes. + * @return Cette instance du builder. + */ + public B cooldown(long seconds) { + return cooldown(Duration.ofSeconds(seconds)); + } + + /** + * SpĂ©cifie la permission permettant d'ignorer le cooldown de cette commande. + * + * @param bypassPermission La permission de bypass. + * @return Cette instance du builder. + */ + public B cooldownBypass(String bypassPermission) { + this.cooldownBypassPermission = bypassPermission; + return (B) this; + } + + // --- Écouteurs d'Ă©vĂ©nements (Event Hooks) --- + + /** + * Attache un Ă©couteur dĂ©clenchĂ© juste avant l'exĂ©cution de cette commande. + * + * @param listener L'Ă©couteur. + * @return Cette instance du builder. + */ + public B onPreExecute(CommandEventListener listener) { + this.preExecuteListeners.add(listener); + return (B) this; + } + + /** + * Attache un Ă©couteur dĂ©clenchĂ© aprĂšs l'exĂ©cution de cette commande. + * + * @param listener L'Ă©couteur. + * @return Cette instance du builder. + */ + public B onPostExecute(CommandEventListener listener) { + this.postExecuteListeners.add(listener); + return (B) this; + } + + /** + * Attache un Ă©couteur dĂ©clenchĂ© en cas de refus de permission ou mauvais Ă©metteur. + * + * @param listener L'Ă©couteur. + * @return Cette instance du builder. + */ + public B onPermissionDenied(CommandEventListener listener) { + this.permissionDeniedListeners.add(listener); + return (B) this; + } + + /** + * Attache un Ă©couteur dĂ©clenchĂ© en cas d'erreur de syntaxe ou argument manquant. + * + * @param listener L'Ă©couteur. + * @return Cette instance du builder. + */ + public B onSyntaxError(CommandEventListener listener) { + this.syntaxErrorListeners.add(listener); + return (B) this; + } + + /** + * Attache un Ă©couteur dĂ©clenchĂ© lorsqu'un joueur tente d'exĂ©cuter la commande sous cooldown. + * + * @param listener L'Ă©couteur. + * @return Cette instance du builder. + */ + public B onCooldown(CommandEventListener listener) { + this.cooldownListeners.add(listener); + return (B) this; + } + + /** + * Attache un Ă©couteur dĂ©clenchĂ© lors du calcul des suggestions d'auto-complĂ©tion. + * + * @param listener L'Ă©couteur. + * @return Cette instance du builder. + */ + public B onTabComplete(CommandEventListener listener) { + this.tabCompleteListeners.add(listener); + return (B) this; + } + + /** + * Construit et configure l'instance du nƓud de commande. + * + * @return Le nƓud construit. + */ + public abstract N build(); + + /** + * Applique les propriĂ©tĂ©s configurĂ©es par le builder sur le nƓud cible. + * + * @param node Le nƓud Ă  initialiser. + */ + protected void applyProperties(CommandNode node) { + node.setDescription(this.description); + node.setPermission(this.permission); + node.setSenderType(this.senderType); + node.addAliases(this.aliases); + node.addArguments(this.arguments); + node.setExecutor(this.executor); + node.setCooldown(this.cooldown); + node.setCooldownBypassPermission(this.cooldownBypassPermission); + + node.getPreExecuteListeners().addAll(this.preExecuteListeners); + node.getPostExecuteListeners().addAll(this.postExecuteListeners); + node.getPermissionDeniedListeners().addAll(this.permissionDeniedListeners); + node.getSyntaxErrorListeners().addAll(this.syntaxErrorListeners); + node.getCooldownListeners().addAll(this.cooldownListeners); + node.getTabCompleteListeners().addAll(this.tabCompleteListeners); + + for (SubCommandBuilder subBuilder : subCommandBuilders) { + CommandNode subNode = subBuilder.build(); + node.addSubCommand(subNode); + } + } +} diff --git a/src/main/java/fr/luc/bettermccommands/builder/CommandBuilder.java b/src/main/java/fr/luc/bettermccommands/builder/CommandBuilder.java new file mode 100644 index 0000000..b6b8ae4 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/builder/CommandBuilder.java @@ -0,0 +1,49 @@ +package fr.luc.bettermccommands.builder; + +import fr.luc.bettermccommands.BetterMcCommands; +import fr.luc.bettermccommands.api.Command; + +/** + * Constructeur fluide (Builder) pour dĂ©clarer et enregistrer une commande racine Minecraft. + */ +public class CommandBuilder extends AbstractCommandBuilder { + + /** + * CrĂ©e un nouveau constructeur de commande racine. + * + * @param name Le nom principal de la commande. + */ + public CommandBuilder(String name) { + super(name); + } + + @Override + public Command build() { + Command command = new Command(this.name); + applyProperties(command); + return command; + } + + /** + * Construit la commande et l'enregistre immĂ©diatement auprĂšs du gestionnaire {@link BetterMcCommands}. + * + * @return L'instance de {@link Command} enregistrĂ©e. + */ + public Command register() { + Command command = build(); + BetterMcCommands.getInstance().registerCommand(command); + return command; + } + + /** + * Construit la commande et l'enregistre auprĂšs d'une instance spĂ©cifique de {@link BetterMcCommands}. + * + * @param manager L'instance du gestionnaire. + * @return L'instance de {@link Command} enregistrĂ©e. + */ + public Command register(BetterMcCommands manager) { + Command command = build(); + manager.registerCommand(command); + return command; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/builder/SubCommandBuilder.java b/src/main/java/fr/luc/bettermccommands/builder/SubCommandBuilder.java new file mode 100644 index 0000000..cc370f2 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/builder/SubCommandBuilder.java @@ -0,0 +1,25 @@ +package fr.luc.bettermccommands.builder; + +import fr.luc.bettermccommands.api.CommandNode; + +/** + * Constructeur fluide (Builder) pour dĂ©clarer des sous-commandes imbriquĂ©es. + */ +public class SubCommandBuilder extends AbstractCommandBuilder { + + /** + * CrĂ©e un nouveau constructeur de sous-commande. + * + * @param name Le nom de la sous-commande. + */ + public SubCommandBuilder(String name) { + super(name); + } + + @Override + public CommandNode build() { + CommandNode node = new CommandNode(this.name); + applyProperties(node); + return node; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/cooldown/CooldownManager.java b/src/main/java/fr/luc/bettermccommands/cooldown/CooldownManager.java new file mode 100644 index 0000000..e7defe8 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/cooldown/CooldownManager.java @@ -0,0 +1,125 @@ +package fr.luc.bettermccommands.cooldown; + +import fr.luc.bettermccommands.api.CommandNode; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * GĂšre les temps de recharge (Cooldowns) par commande et par joueur. + */ +public class CooldownManager { + + private final Map> cooldowns = new ConcurrentHashMap<>(); + + /** + * VĂ©rifie si l'Ă©metteur est actuellement soumis Ă  un temps de recharge pour le nƓud de commande donnĂ©. + * + * @param node Le nƓud de commande. + * @param sender L'Ă©metteur testĂ©. + * @return true si l'Ă©metteur est en cooldown, sinon false. + */ + public boolean isOnCooldown(CommandNode node, CommandSender sender) { + if (!(sender instanceof Player player)) { + return false; // La console n'a pas de cooldown + } + if (node.getCooldown() == null || node.getCooldown().isZero() || node.getCooldown().isNegative()) { + return false; + } + if (node.getCooldownBypassPermission() != null && player.hasPermission(node.getCooldownBypassPermission())) { + return false; + } + + Map nodeMap = cooldowns.get(node.getFullName()); + if (nodeMap == null) { + return false; + } + + Instant expiration = nodeMap.get(player.getUniqueId()); + if (expiration == null) { + return false; + } + + if (Instant.now().isAfter(expiration)) { + nodeMap.remove(player.getUniqueId()); + return false; + } + + return true; + } + + /** + * Calcule le temps restant avant expiration du cooldown pour un Ă©metteur. + * + * @param node Le nƓud de commande. + * @param sender L'Ă©metteur testĂ©. + * @return La durĂ©e restante, ou {@link Duration#ZERO} si aucun cooldown n'est actif. + */ + public Duration getRemainingCooldown(CommandNode node, CommandSender sender) { + if (!(sender instanceof Player player)) { + return Duration.ZERO; + } + + Map nodeMap = cooldowns.get(node.getFullName()); + if (nodeMap == null) { + return Duration.ZERO; + } + + Instant expiration = nodeMap.get(player.getUniqueId()); + if (expiration == null) { + return Duration.ZERO; + } + + Instant now = Instant.now(); + if (now.isAfter(expiration)) { + nodeMap.remove(player.getUniqueId()); + return Duration.ZERO; + } + + return Duration.between(now, expiration); + } + + /** + * Applique le temps de recharge configurĂ© sur le nƓud Ă  l'Ă©metteur. + * + * @param node Le nƓud de commande. + * @param sender L'Ă©metteur. + */ + public void applyCooldown(CommandNode node, CommandSender sender) { + if (!(sender instanceof Player player)) { + return; + } + Duration duration = node.getCooldown(); + if (duration == null || duration.isZero() || duration.isNegative()) { + return; + } + + cooldowns.computeIfAbsent(node.getFullName(), k -> new ConcurrentHashMap<>()) + .put(player.getUniqueId(), Instant.now().plus(duration)); + } + + /** + * RĂ©initialise / supprime le cooldown d'un joueur pour une commande donnĂ©e. + * + * @param node Le nƓud de commande. + * @param player Le joueur. + */ + public void resetCooldown(CommandNode node, Player player) { + Map nodeMap = cooldowns.get(node.getFullName()); + if (nodeMap != null) { + nodeMap.remove(player.getUniqueId()); + } + } + + /** + * Nettoie l'ensemble des temps de recharge enregistrĂ©s. + */ + public void clearAll() { + cooldowns.clear(); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/demo/DemoPlugin.java b/src/main/java/fr/luc/bettermccommands/demo/DemoPlugin.java new file mode 100644 index 0000000..6b93946 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/demo/DemoPlugin.java @@ -0,0 +1,42 @@ +package fr.luc.bettermccommands.demo; + +import fr.luc.bettermccommands.BetterMcCommands; +import fr.luc.bettermccommands.demo.commands.DemoBaseCommand; +import fr.luc.bettermccommands.demo.commands.DemoListener; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Exemple de plugin Bukkit/Paper intĂ©grant la bibliothĂšque betterMcCommands. + */ +public class DemoPlugin extends JavaPlugin { + + private BetterMcCommands commandsManager; + + @Override + public void onEnable() { + getLogger().info("Initialisation de betterMcCommands Demo..."); + + // 1. Initialisation du gestionnaire pour ce plugin + this.commandsManager = BetterMcCommands.create(this); + + // 2. Enregistrement des Ă©couteurs d'Ă©vĂ©nements de commandes + this.commandsManager.registerListeners(new DemoListener()); + + // 3. Enregistrement de la commande de dĂ©monstration /commande-demo + DemoBaseCommand.create().register(); + + getLogger().info("betterMcCommands Demo activĂ© avec succĂšs !"); + } + + @Override + public void onDisable() { + getLogger().info("DĂ©sactivation de betterMcCommands Demo..."); + + // Nettoyage et dĂ©senregistrement Ă  chaud de toutes les commandes + if (commandsManager != null) { + commandsManager.unregisterAll(); + } + + getLogger().info("betterMcCommands Demo dĂ©sactivĂ©."); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/demo/commands/DemoBaseCommand.java b/src/main/java/fr/luc/bettermccommands/demo/commands/DemoBaseCommand.java new file mode 100644 index 0000000..9bdfffe --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/demo/commands/DemoBaseCommand.java @@ -0,0 +1,136 @@ +package fr.luc.bettermccommands.demo.commands; + +import fr.luc.bettermccommands.BetterMcCommands; +import fr.luc.bettermccommands.api.Command; +import fr.luc.bettermccommands.argument.Arguments; +import org.bukkit.entity.Player; + +import java.time.Duration; + +/** + * Commande de dĂ©monstration illustrant la puissance et la flexibilitĂ© de betterMcCommands : + * - Commande racine avec alias et restrictions + * - Sous-commandes simples et imbriquĂ©es + * - Arguments typĂ©s obligatoires et optionnels (Player, Integer, Duration, Greedy String, Enum) + * - Cooldowns natifs + * - ÉvĂ©nements de cycle de vie (onPreExecute, onPermissionDenied, etc.) + */ +public class DemoBaseCommand { + + public enum Rank { + JOUEUR, VIP, MODERATEUR, ADMINISTRATEUR + } + + /** + * Construit et retourne la commande de dĂ©monstration /commande-demo. + * + * @return La commande prĂȘte Ă  ĂȘtre enregistrĂ©e. + */ + public static Command create() { + return BetterMcCommands.builder("commande-demo") + .description("Commande de dĂ©monstration pour betterMcCommands") + .aliases("demo", "cdemo") + .permission("bettermc.demo") + + // ÉvĂ©nement local dĂ©clenchĂ© avant l'exĂ©cution de la commande + .onPreExecute(event -> { + if (event.isPlayer()) { + Player player = event.getPlayer(); + // Exemple de vĂ©rification : simuler un check de combat ou d'Ă©tat + if (event.getContext() != null && event.getContext().hasArgument("bloque")) { + event.setCancelled(true); + event.reply("Action annulĂ©e par le systĂšme de sĂ©curitĂ© !"); + } + } + }) + + // ExĂ©cution de la commande racine : /commande-demo + .executes(context -> { + context.reply("=== DĂ©monstration betterMcCommands ==="); + context.reply("Bienvenue " + context.getSender().getName() + " !"); + context.reply("Sous-commandes disponibles :"); + context.reply(" ‱ /demo give [quantite] - Donne des items"); + context.reply(" ‱ /demo broadcast - Diffuse une annonce"); + context.reply(" ‱ /demo tempban - Bannit temporairement"); + context.reply(" ‱ /demo cooldown-test - Teste un cooldown de 10s"); + context.reply(" ‱ /demo admin rank set - Sous-commandes imbriquĂ©es"); + }) + + // 1. Sous-commande : /demo give [quantite] + .subcommand(BetterMcCommands.subBuilder("give") + .description("Donne des items Ă  un joueur cible") + .permission("bettermc.demo.give") + .argument(Arguments.player("cible").description("Le joueur qui recevra les items")) + .argument(Arguments.integer("quantite", 1, 64).defaultValue(1).description("Nombre d'items (1 Ă  64)")) + .executes(context -> { + Player target = context.getTargetPlayer("cible"); + int amount = context.getInt("quantite"); + + context.replySuccess("Attribution de " + amount + " ressource(s) Ă  " + target.getName() + " !"); + target.sendMessage("§aVous avez reçu §6" + amount + " §aresource(s) de la part de §b" + context.getSender().getName() + "§a."); + }) + ) + + // 2. Sous-commande : /demo broadcast + .subcommand(BetterMcCommands.subBuilder("broadcast") + .description("Diffuse un message global Ă  tous les joueurs du serveur") + .aliases("bc", "annonce") + .permission("bettermc.demo.broadcast") + .argument(Arguments.greedyString("message").description("Le message complet Ă  diffuser")) + .executes(context -> { + String message = context.getString("message"); + context.replySuccess("Diffusion de l'annonce : " + message + ""); + }) + ) + + // 3. Sous-commande : /demo tempban + .subcommand(BetterMcCommands.subBuilder("tempban") + .description("Sanctionne temporairement un joueur") + .permission("bettermc.demo.tempban") + .argument(Arguments.offlinePlayer("joueur").description("Le joueur Ă  sanctionner")) + .argument(Arguments.duration("duree").description("DurĂ©e de la sanction (ex: 30m, 2h, 7d)")) + .argument(Arguments.greedyString("raison").defaultValue("Infraction aux rĂšgles").description("Motif du bannissement")) + .executes(context -> { + var target = context.get("joueur", org.bukkit.OfflinePlayer.class); + Duration duration = context.getDuration("duree"); + String reason = context.getString("raison"); + + context.replySuccess("Joueur " + target.getName() + " banni pour " + + duration.toMinutes() + " minutes. Motif : " + reason + "."); + }) + ) + + // 4. Sous-commande avec Cooldown : /demo cooldown-test + .subcommand(BetterMcCommands.subBuilder("cooldown-test") + .description("Teste le systĂšme de cooldown (10 secondes)") + .cooldown(Duration.ofSeconds(10)) + .cooldownBypass("bettermc.bypass.cooldown") + .playerOnly() + .executes(context -> { + context.reply("Cooldown validĂ© ! Vous venez d'exĂ©cuter la commande. RĂ©essayez immĂ©diatement pour tester le blocage."); + }) + ) + + // 5. Arborescence imbriquĂ©e : /demo admin rank set + .subcommand(BetterMcCommands.subBuilder("admin") + .description("Commandes administratives") + .permission("bettermc.admin") + .subcommand(BetterMcCommands.subBuilder("rank") + .description("Gestion des rangs des joueurs") + .subcommand(BetterMcCommands.subBuilder("set") + .description("DĂ©finit le rang d'un joueur") + .argument(Arguments.player("joueur")) + .argument(Arguments.enumOf("grade", Rank.class)) + .executes(context -> { + Player target = context.getTargetPlayer("joueur"); + Rank rank = context.get("grade", Rank.class); + + context.replySuccess("Le rang de " + target.getName() + " a Ă©tĂ© dĂ©fini sur " + rank.name() + "."); + }) + ) + ) + ) + + .build(); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/demo/commands/DemoListener.java b/src/main/java/fr/luc/bettermccommands/demo/commands/DemoListener.java new file mode 100644 index 0000000..2693b4f --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/demo/commands/DemoListener.java @@ -0,0 +1,62 @@ +package fr.luc.bettermccommands.demo.commands; + +import fr.luc.bettermccommands.event.*; +import fr.luc.bettermccommands.event.annotation.CommandEventHandler; + +/** + * Exemple de classe d'Ă©couteurs d'Ă©vĂ©nements de commandes utilisant l'annotation {@link CommandEventHandler}. + */ +public class DemoListener { + + /** + * Intercepte toutes les commandes avant leur exĂ©cution. + * + * @param event L'Ă©vĂ©nement de prĂ©-exĂ©cution. + */ + @CommandEventHandler(priority = 10) + public void onAnyCommandPreExecute(CommandPreExecuteEvent event) { + System.out.println("[betterMcCommands Log] ExĂ©cution demandĂ©e pour : /" + event.getNode().getFullName() + + " par " + event.getSender().getName()); + } + + /** + * Intercepte spĂ©cifiquement les Ă©vĂ©nements sur la commande 'commande-demo'. + * + * @param event L'Ă©vĂ©nement de post-exĂ©cution. + */ + @CommandEventHandler(command = "commande-demo") + public void onDemoPostExecute(CommandPostExecuteEvent event) { + if (event.isSuccessful()) { + System.out.println("[betterMcCommands Metric] /" + event.getNode().getFullName() + + " exĂ©cutĂ©e en " + event.getExecutionDuration().toMillis() + "ms."); + } else { + System.err.println("[betterMcCommands Alert] Échec d'exĂ©cution sur /" + event.getNode().getFullName()); + } + } + + /** + * Intercepte les refus de permission pour personnaliser le message ou jouer un son. + * + * @param event L'Ă©vĂ©nement de permission refusĂ©e. + */ + @CommandEventHandler + public void onPermissionDenied(CommandPermissionDeniedEvent event) { + if (event.getRequiredPermission() != null) { + event.setCustomErrorMessage("AccĂšs Restreint : Permission requise [" + + event.getRequiredPermission() + "]."); + } + } + + /** + * Intercepte les tentatives d'exĂ©cution sous cooldown. + * + * @param event L'Ă©vĂ©nement de cooldown. + */ + @CommandEventHandler + public void onCooldown(CommandCooldownEvent event) { + long seconds = event.getRemainingCooldown().toSeconds(); + event.setCustomMessage("⏳ Doucement ! Vous devez encore attendre " + + (seconds > 0 ? seconds + "s" : event.getRemainingCooldown().toMillis() + "ms") + + " avant de rĂ©utiliser /" + event.getNode().getFullName() + "."); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/event/CancellableCommandEvent.java b/src/main/java/fr/luc/bettermccommands/event/CancellableCommandEvent.java new file mode 100644 index 0000000..4d0d2ed --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/CancellableCommandEvent.java @@ -0,0 +1,35 @@ +package fr.luc.bettermccommands.event; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.CommandNode; +import org.bukkit.command.CommandSender; +import org.bukkit.event.Cancellable; + +/** + * Classe de base pour les Ă©vĂ©nements de commande pouvant ĂȘtre annulĂ©s par les Ă©couteurs. + */ +public abstract class CancellableCommandEvent extends CommandEvent implements Cancellable { + + private boolean cancelled = false; + + /** + * CrĂ©e un Ă©vĂ©nement annulable. + * + * @param node Le nƓud de commande. + * @param sender L'Ă©metteur. + * @param context Le contexte. + */ + public CancellableCommandEvent(CommandNode node, CommandSender sender, CommandContext context) { + super(node, sender, context); + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/event/CommandCooldownEvent.java b/src/main/java/fr/luc/bettermccommands/event/CommandCooldownEvent.java new file mode 100644 index 0000000..2d45446 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/CommandCooldownEvent.java @@ -0,0 +1,54 @@ +package fr.luc.bettermccommands.event; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.CommandNode; +import org.bukkit.command.CommandSender; + +import java.time.Duration; + +/** + * ÉvĂ©nement dĂ©clenchĂ© lorsqu'un joueur tente d'exĂ©cuter une commande soumise Ă  un temps de recharge (Cooldown) actif. + *

+ * Si l'Ă©vĂ©nement est annulĂ© ({@code setCancelled(true)}), l'exĂ©cution de la commande est autorisĂ©e exceptionnellement. + */ +public class CommandCooldownEvent extends CancellableCommandEvent { + + private final Duration remainingCooldown; + private String customMessage; + + /** + * CrĂ©e un Ă©vĂ©nement de cooldown de commande. + * + * @param node Le nƓud de commande. + * @param sender L'Ă©metteur sous cooldown. + * @param context Le contexte de la commande. + * @param remainingCooldown La durĂ©e restante avant expiration. + */ + public CommandCooldownEvent(CommandNode node, CommandSender sender, CommandContext context, Duration remainingCooldown) { + super(node, sender, context); + this.remainingCooldown = remainingCooldown; + } + + /** + * @return La durĂ©e restante avant la fin du cooldown. + */ + public Duration getRemainingCooldown() { + return remainingCooldown; + } + + /** + * @return Le message personnalisĂ© Ă©ventuel, ou {@code null}. + */ + public String getCustomMessage() { + return customMessage; + } + + /** + * DĂ©finit un message d'avertissement de cooldown sur-mesure. + * + * @param customMessage Le message au format MiniMessage ou couleurs Minecraft. + */ + public void setCustomMessage(String customMessage) { + this.customMessage = customMessage; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/event/CommandEvent.java b/src/main/java/fr/luc/bettermccommands/event/CommandEvent.java new file mode 100644 index 0000000..33996ae --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/CommandEvent.java @@ -0,0 +1,83 @@ +package fr.luc.bettermccommands.event; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.CommandNode; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.util.Objects; + +/** + * Classe de base pour tous les Ă©vĂ©nements du cycle de vie d'une commande. + */ +public abstract class CommandEvent { + + private final CommandNode node; + private final CommandSender sender; + private final CommandContext context; + + /** + * CrĂ©e un nouvel Ă©vĂ©nement de commande. + * + * @param node Le nƓud de commande ciblĂ©. + * @param sender L'Ă©metteur de la commande. + * @param context Le contexte d'exĂ©cution (peut ĂȘtre partiel ou null). + */ + public CommandEvent(CommandNode node, CommandSender sender, CommandContext context) { + this.node = Objects.requireNonNull(node, "node cannot be null"); + this.sender = Objects.requireNonNull(sender, "sender cannot be null"); + this.context = context; + } + + /** + * @return Le nƓud de commande concernĂ© par cet Ă©vĂ©nement. + */ + public CommandNode getNode() { + return node; + } + + /** + * @return L'Ă©metteur ayant invoquĂ© la commande. + */ + public CommandSender getSender() { + return sender; + } + + /** + * @return true si l'Ă©metteur est un joueur. + */ + public boolean isPlayer() { + return sender instanceof Player; + } + + /** + * @return Le joueur Ă©metteur. + * @throws IllegalStateException si l'Ă©metteur n'est pas un joueur. + */ + public Player getPlayer() { + if (!isPlayer()) { + throw new IllegalStateException("CommandSender is not a player: " + sender.getClass().getSimpleName()); + } + return (Player) sender; + } + + /** + * @return Le contexte d'exĂ©cution s'il est dĂ©jĂ  instanciĂ©, sinon {@code null}. + */ + public CommandContext getContext() { + return context; + } + + /** + * Envoie une rĂ©ponse textuelle formatĂ©e Ă  l'Ă©metteur de la commande. + * + * @param miniMessageText Le message au format MiniMessage ou couleurs Minecraft. + */ + public void reply(String miniMessageText) { + if (context != null) { + context.reply(miniMessageText); + } else { + sender.sendMessage(miniMessageText); + } + } +} diff --git a/src/main/java/fr/luc/bettermccommands/event/CommandEventListener.java b/src/main/java/fr/luc/bettermccommands/event/CommandEventListener.java new file mode 100644 index 0000000..224e6ae --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/CommandEventListener.java @@ -0,0 +1,17 @@ +package fr.luc.bettermccommands.event; + +/** + * Interface fonctionnelle pour Ă©couter un Ă©vĂ©nement de commande typĂ©. + * + * @param Le type d'Ă©vĂ©nement Ă©coutĂ©. + */ +@FunctionalInterface +public interface CommandEventListener { + + /** + * InvoquĂ© lorsque l'Ă©vĂ©nement de commande survient. + * + * @param event L'instance de l'Ă©vĂ©nement. + */ + void onEvent(T event); +} diff --git a/src/main/java/fr/luc/bettermccommands/event/CommandEventManager.java b/src/main/java/fr/luc/bettermccommands/event/CommandEventManager.java new file mode 100644 index 0000000..9712b86 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/CommandEventManager.java @@ -0,0 +1,139 @@ +package fr.luc.bettermccommands.event; + +import fr.luc.bettermccommands.event.annotation.CommandEventHandler; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Gestionnaire et bus central des Ă©vĂ©nements de commande. + * Supporte Ă  la fois l'enregistrement fonctionnel (lambdas) et dĂ©claratif (classes annotĂ©es avec {@link CommandEventHandler}). + */ +public class CommandEventManager { + + private record ListenerRegistration(Class eventType, CommandEventListener listener, String commandFilter, int priority) {} + + private final List registrations = new CopyOnWriteArrayList<>(); + + /** + * Enregistre un Ă©couteur fonctionnel pour un type d'Ă©vĂ©nement donnĂ©. + * + * @param eventType Le type d'Ă©vĂ©nement Ă  Ă©couter. + * @param listener Le consommateur de l'Ă©vĂ©nement. + * @param Le type de l'Ă©vĂ©nement. + */ + @SuppressWarnings("unchecked") + public void register(Class eventType, CommandEventListener listener) { + register(eventType, listener, null, 0); + } + + /** + * Enregistre un Ă©couteur fonctionnel avec filtre sur le nom de commande et prioritĂ©. + * + * @param eventType Le type d'Ă©vĂ©nement. + * @param listener Le callback. + * @param commandFilter Le filtre sur le nom de la commande (ou null). + * @param priority La prioritĂ© d'exĂ©cution. + * @param Le type de l'Ă©vĂ©nement. + */ + @SuppressWarnings("unchecked") + public void register(Class eventType, CommandEventListener listener, String commandFilter, int priority) { + Objects.requireNonNull(eventType, "eventType cannot be null"); + Objects.requireNonNull(listener, "listener cannot be null"); + + registrations.add(new ListenerRegistration( + eventType, + (CommandEventListener) listener, + commandFilter != null && !commandFilter.isEmpty() ? commandFilter.toLowerCase() : null, + priority + )); + sortRegistrations(); + } + + /** + * Analyse une instance d'Ă©couteur et enregistre toutes ses mĂ©thodes annotĂ©es avec {@link CommandEventHandler}. + * + * @param listenerInstance L'instance de la classe contenant des mĂ©thodes annotĂ©es. + */ + public void registerListeners(Object listenerInstance) { + Objects.requireNonNull(listenerInstance, "listenerInstance cannot be null"); + + for (Method method : listenerInstance.getClass().getDeclaredMethods()) { + if (!method.isAnnotationPresent(CommandEventHandler.class)) { + continue; + } + + CommandEventHandler annotation = method.getAnnotation(CommandEventHandler.class); + Class[] params = method.getParameterTypes(); + if (params.length != 1 || !CommandEvent.class.isAssignableFrom(params[0])) { + throw new IllegalArgumentException("Method " + method.getName() + " in " + + listenerInstance.getClass().getName() + " must have exactly 1 parameter extending CommandEvent."); + } + + @SuppressWarnings("unchecked") + Class eventType = (Class) params[0]; + method.setAccessible(true); + + CommandEventListener listener = event -> { + try { + method.invoke(listenerInstance, event); + } catch (Exception e) { + System.err.println("[betterMcCommands] Error dispatching event " + event.getClass().getSimpleName() + + " to listener " + listenerInstance.getClass().getSimpleName() + "#" + method.getName()); + e.printStackTrace(); + } + }; + + registrations.add(new ListenerRegistration( + eventType, + listener, + annotation.command().isEmpty() ? null : annotation.command().toLowerCase(), + annotation.priority() + )); + } + sortRegistrations(); + } + + /** + * DĂ©clenche un Ă©vĂ©nement et notifie tous les Ă©couteurs enregistrĂ©s correspondants. + * + * @param event L'Ă©vĂ©nement Ă  diffuser. + */ + public void dispatch(CommandEvent event) { + if (event == null) { + return; + } + + String cmdName = event.getNode().getRootName().toLowerCase(); + + for (ListenerRegistration reg : registrations) { + if (!reg.eventType().isInstance(event)) { + continue; + } + + if (reg.commandFilter() != null && !reg.commandFilter().equalsIgnoreCase(cmdName)) { + continue; + } + + try { + reg.listener().onEvent(event); + } catch (Exception e) { + System.err.println("[betterMcCommands] Exception in event listener for " + event.getClass().getSimpleName() + ": " + e.getMessage()); + e.printStackTrace(); + } + } + } + + /** + * Supprime tous les Ă©couteurs enregistrĂ©s. + */ + public void unregisterAll() { + registrations.clear(); + } + + private void sortRegistrations() { + registrations.sort(Comparator.comparingInt(ListenerRegistration::priority)); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/event/CommandPermissionDeniedEvent.java b/src/main/java/fr/luc/bettermccommands/event/CommandPermissionDeniedEvent.java new file mode 100644 index 0000000..0fb9b22 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/CommandPermissionDeniedEvent.java @@ -0,0 +1,66 @@ +package fr.luc.bettermccommands.event; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.CommandNode; +import fr.luc.bettermccommands.api.CommandSenderType; +import org.bukkit.command.CommandSender; + +/** + * ÉvĂ©nement dĂ©clenchĂ© lorsqu'un Ă©metteur tente d'exĂ©cuter une commande sans disposer de la permission requise + * ou sans avoir le type d'Ă©metteur attendu (ex: console voulant exĂ©cuter une commande rĂ©servĂ©e aux joueurs). + *

+ * Si l'Ă©vĂ©nement est annulĂ© ({@code setCancelled(true)}), le message d'erreur par dĂ©faut n'est pas envoyĂ©, + * ce qui permet d'afficher un message ou de jouer un son personnalisĂ©. + */ +public class CommandPermissionDeniedEvent extends CancellableCommandEvent { + + private final String requiredPermission; + private final CommandSenderType requiredSenderType; + private String customErrorMessage; + + /** + * CrĂ©e l'Ă©vĂ©nement de refus de permission / restriction d'Ă©metteur. + * + * @param node Le nƓud ciblĂ©. + * @param sender L'Ă©metteur bloquĂ©. + * @param context Le contexte d'exĂ©cution (peut ĂȘtre null). + * @param requiredPermission La permission manquante (ou null). + * @param requiredSenderType Le type d'Ă©metteur requis. + */ + public CommandPermissionDeniedEvent(CommandNode node, CommandSender sender, CommandContext context, + String requiredPermission, CommandSenderType requiredSenderType) { + super(node, sender, context); + this.requiredPermission = requiredPermission; + this.requiredSenderType = requiredSenderType; + } + + /** + * @return La permission manquante, ou {@code null} s'il s'agit d'une restriction de type d'Ă©metteur. + */ + public String getRequiredPermission() { + return requiredPermission; + } + + /** + * @return Le type d'Ă©metteur requis par la commande. + */ + public CommandSenderType getRequiredSenderType() { + return requiredSenderType; + } + + /** + * @return Le message d'erreur personnalisĂ© Ă  envoyer, ou {@code null} pour le message par dĂ©faut. + */ + public String getCustomErrorMessage() { + return customErrorMessage; + } + + /** + * DĂ©finit un message d'erreur sur-mesure Ă  envoyer au joueur. + * + * @param customErrorMessage Le message formatĂ©. + */ + public void setCustomErrorMessage(String customErrorMessage) { + this.customErrorMessage = customErrorMessage; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/event/CommandPostExecuteEvent.java b/src/main/java/fr/luc/bettermccommands/event/CommandPostExecuteEvent.java new file mode 100644 index 0000000..ee0c376 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/CommandPostExecuteEvent.java @@ -0,0 +1,65 @@ +package fr.luc.bettermccommands.event; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.CommandNode; +import fr.luc.bettermccommands.api.CommandResult; +import org.bukkit.command.CommandSender; + +import java.time.Duration; + +/** + * ÉvĂ©nement dĂ©clenchĂ© aprĂšs l'exĂ©cution d'une commande (avec succĂšs ou Ă©chec). + * Utile pour les mĂ©triques, les journaux d'audit (logging) et les statistiques d'utilisation. + */ +public class CommandPostExecuteEvent extends CommandEvent { + + private final CommandResult result; + private final Duration executionDuration; + private final Throwable exception; + + /** + * CrĂ©e un Ă©vĂ©nement de post-exĂ©cution. + * + * @param node Le nƓud exĂ©cutĂ©. + * @param sender L'Ă©metteur. + * @param context Le contexte de la commande. + * @param result Le rĂ©sultat final. + * @param executionDuration La durĂ©e totale d'exĂ©cution. + * @param exception L'exception Ă©ventuelle survenue lors de l'exĂ©cution (ou null). + */ + public CommandPostExecuteEvent(CommandNode node, CommandSender sender, CommandContext context, + CommandResult result, Duration executionDuration, Throwable exception) { + super(node, sender, context); + this.result = result; + this.executionDuration = executionDuration; + this.exception = exception; + } + + /** + * @return Le rĂ©sultat d'exĂ©cution de la commande. + */ + public CommandResult getResult() { + return result; + } + + /** + * @return La durĂ©e nĂ©cessaire au traitement de la commande. + */ + public Duration getExecutionDuration() { + return executionDuration; + } + + /** + * @return L'exception levĂ©e lors de l'exĂ©cution, ou {@code null} en cas de succĂšs. + */ + public Throwable getException() { + return exception; + } + + /** + * @return true si la commande s'est exĂ©cutĂ©e avec succĂšs sans lever d'exception. + */ + public boolean isSuccessful() { + return result == CommandResult.SUCCESS && exception == null; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/event/CommandPreExecuteEvent.java b/src/main/java/fr/luc/bettermccommands/event/CommandPreExecuteEvent.java new file mode 100644 index 0000000..34e5492 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/CommandPreExecuteEvent.java @@ -0,0 +1,23 @@ +package fr.luc.bettermccommands.event; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.CommandNode; +import org.bukkit.command.CommandSender; + +/** + * ÉvĂ©nement dĂ©clenchĂ© juste avant l'exĂ©cution du gestionnaire mĂ©tier de la commande. + * Permet d'annuler la commande, de vĂ©rifier des prĂ©-conditions (Ă©tat de combat, Ă©conomie, inventaire) ou d'injecter des mĂ©tadonnĂ©es. + */ +public class CommandPreExecuteEvent extends CancellableCommandEvent { + + /** + * Construit l'Ă©vĂ©nement de prĂ©-exĂ©cution. + * + * @param node Le nƓud de commande sur le point d'ĂȘtre exĂ©cutĂ©. + * @param sender L'Ă©metteur. + * @param context Le contexte complet avec les arguments rĂ©solus. + */ + public CommandPreExecuteEvent(CommandNode node, CommandSender sender, CommandContext context) { + super(node, sender, context); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/event/CommandSyntaxErrorEvent.java b/src/main/java/fr/luc/bettermccommands/event/CommandSyntaxErrorEvent.java new file mode 100644 index 0000000..d14bbbf --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/CommandSyntaxErrorEvent.java @@ -0,0 +1,64 @@ +package fr.luc.bettermccommands.event; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.CommandNode; +import org.bukkit.command.CommandSender; + +/** + * ÉvĂ©nement dĂ©clenchĂ© lorsqu'une commande est appelĂ©e avec une syntaxe incorrecte + * (arguments obligatoires manquants, sous-commande inexistante ou erreur de conversion d'argument). + *

+ * Si l'Ă©vĂ©nement est annulĂ© ({@code setCancelled(true)}), le message d'aide automatique n'est pas envoyĂ©. + */ +public class CommandSyntaxErrorEvent extends CancellableCommandEvent { + + private final String errorReason; + private final String usage; + private String customMessage; + + /** + * CrĂ©e un Ă©vĂ©nement d'erreur de syntaxe. + * + * @param node Le nƓud de commande concernĂ©. + * @param sender L'Ă©metteur. + * @param context Le contexte de commande (peut ĂȘtre null ou partiel). + * @param errorReason L'explication de l'erreur. + * @param usage La syntaxe attendue (ex: "/demo give [quantite]"). + */ + public CommandSyntaxErrorEvent(CommandNode node, CommandSender sender, CommandContext context, + String errorReason, String usage) { + super(node, sender, context); + this.errorReason = errorReason; + this.usage = usage; + } + + /** + * @return La raison dĂ©taillĂ©e de l'erreur de syntaxe. + */ + public String getErrorReason() { + return errorReason; + } + + /** + * @return La ligne d'usage correcte pour cette commande. + */ + public String getUsage() { + return usage; + } + + /** + * @return Le message d'erreur personnalisĂ© Ă  envoyer, ou {@code null}. + */ + public String getCustomMessage() { + return customMessage; + } + + /** + * DĂ©finit un message personnalisĂ© Ă  la place de l'aide par dĂ©faut. + * + * @param customMessage Le message personnalisĂ©. + */ + public void setCustomMessage(String customMessage) { + this.customMessage = customMessage; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/event/CommandTabCompleteEvent.java b/src/main/java/fr/luc/bettermccommands/event/CommandTabCompleteEvent.java new file mode 100644 index 0000000..57e6e82 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/CommandTabCompleteEvent.java @@ -0,0 +1,78 @@ +package fr.luc.bettermccommands.event; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.api.CommandNode; +import fr.luc.bettermccommands.api.suggestion.Suggestion; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; +import java.util.List; + +/** + * ÉvĂ©nement dĂ©clenchĂ© lors du calcul des suggestions d'auto-complĂ©tion (Tab-Complete). + * Permet d'ajouter, modifier, filtrer ou rĂ©ordonner les suggestions renvoyĂ©es au client. + */ +public class CommandTabCompleteEvent extends CancellableCommandEvent { + + private final List suggestions; + private final String currentInput; + private final int argumentIndex; + + /** + * CrĂ©e l'Ă©vĂ©nement de complĂ©tion. + * + * @param node Le nƓud de commande concernĂ©. + * @param sender L'Ă©metteur recevant les suggestions. + * @param context Le contexte partiel. + * @param suggestions La liste modifiable des suggestions calculĂ©es. + * @param currentInput La chaĂźne tapĂ©e en cours. + * @param argumentIndex L'index de l'argument complĂ©tĂ©. + */ + public CommandTabCompleteEvent(CommandNode node, CommandSender sender, CommandContext context, + List suggestions, String currentInput, int argumentIndex) { + super(node, sender, context); + this.suggestions = new ArrayList<>(suggestions); + this.currentInput = currentInput != null ? currentInput : ""; + this.argumentIndex = argumentIndex; + } + + /** + * @return La liste modifiable des suggestions qui seront retournĂ©es au client. + */ + public List getSuggestions() { + return suggestions; + } + + /** + * Ajoute une suggestion simple Ă  la liste. + * + * @param value La chaĂźne suggĂ©rĂ©e. + */ + public void addSuggestion(String value) { + this.suggestions.add(Suggestion.of(value)); + } + + /** + * Ajoute une suggestion avec infobulle Ă  la liste. + * + * @param value Le texte insĂ©rĂ©. + * @param tooltip L'infobulle affichĂ©e. + */ + public void addSuggestion(String value, String tooltip) { + this.suggestions.add(Suggestion.of(value, tooltip)); + } + + /** + * @return Le mot en cours de saisie par le joueur. + */ + public String getCurrentInput() { + return currentInput; + } + + /** + * @return L'index de l'argument en cours de frappe. + */ + public int getArgumentIndex() { + return argumentIndex; + } +} diff --git a/src/main/java/fr/luc/bettermccommands/event/annotation/CommandEventHandler.java b/src/main/java/fr/luc/bettermccommands/event/annotation/CommandEventHandler.java new file mode 100644 index 0000000..3ac0410 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/event/annotation/CommandEventHandler.java @@ -0,0 +1,30 @@ +package fr.luc.bettermccommands.event.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annote une mĂ©thode d'une classe d'Ă©coute pour recevoir les Ă©vĂ©nements de commande betterMcCommands. + * La mĂ©thode doit accepter un unique paramĂštre hĂ©ritant de {@link fr.luc.bettermccommands.event.CommandEvent}. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandEventHandler { + + /** + * Filtre optionnel sur le nom racine de la commande (ex: "commande-demo"). + * Si laissĂ© vide (""), la mĂ©thode recevra les Ă©vĂ©nements de toutes les commandes. + * + * @return Le nom de commande filtrĂ© ou vide pour tous. + */ + String command() default ""; + + /** + * PrioritĂ© d'exĂ©cution de l'Ă©couteur (les valeurs plus petites sont exĂ©cutĂ©es en premier). + * + * @return La prioritĂ© numĂ©rique (dĂ©faut: 0). + */ + int priority() default 0; +} diff --git a/src/main/java/fr/luc/bettermccommands/platform/CommandDispatcher.java b/src/main/java/fr/luc/bettermccommands/platform/CommandDispatcher.java new file mode 100644 index 0000000..db9f308 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/platform/CommandDispatcher.java @@ -0,0 +1,337 @@ +package fr.luc.bettermccommands.platform; + +import fr.luc.bettermccommands.BetterMcCommands; +import fr.luc.bettermccommands.api.*; +import fr.luc.bettermccommands.api.suggestion.Suggestion; +import fr.luc.bettermccommands.argument.CommandArgument; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; +import fr.luc.bettermccommands.event.*; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Moteur central de routage, de validation, de parsing et d'exĂ©cution des commandes. + */ +public class CommandDispatcher { + + private final BetterMcCommands manager; + + /** + * CrĂ©e un nouveau dispatcher associĂ© Ă  un gestionnaire {@link BetterMcCommands}. + * + * @param manager L'instance du gestionnaire. + */ + public CommandDispatcher(BetterMcCommands manager) { + this.manager = Objects.requireNonNull(manager, "manager cannot be null"); + } + + /** + * ExĂ©cute une commande racine avec les arguments bruts fournis. + * + * @param rootCommand La commande racine. + * @param sender L'Ă©metteur de la commande. + * @param label Le label ou alias utilisĂ©. + * @param args Les arguments bruts. + * @return Le rĂ©sultat d'exĂ©cution {@link CommandResult}. + */ + public CommandResult execute(Command rootCommand, CommandSender sender, String label, String[] args) { + Instant startTime = Instant.now(); + + // 1. RĂ©solution de la sous-commande ciblĂ©e dans l'arborescence + CommandNode targetNode = rootCommand; + int argIndex = 0; + + while (argIndex < args.length && targetNode.hasSubCommands()) { + String candidate = args[argIndex]; + CommandNode sub = targetNode.findSubCommand(candidate); + if (sub != null) { + targetNode = sub; + argIndex++; + } else { + break; + } + } + + // Arguments restants destinĂ©s aux paramĂštres de la commande ciblĂ©e + String[] remainingArgs = Arrays.copyOfRange(args, argIndex, args.length); + + // 2. VĂ©rification du type d'Ă©metteur (Console vs Player) + if (!targetNode.getSenderType().isAllowed(sender)) { + CommandPermissionDeniedEvent event = new CommandPermissionDeniedEvent( + targetNode, sender, null, null, targetNode.getSenderType()); + dispatchEvent(targetNode, event); + + if (!event.isCancelled()) { + if (event.getCustomErrorMessage() != null) { + event.reply(event.getCustomErrorMessage()); + } else if (targetNode.getSenderType() == CommandSenderType.PLAYER_ONLY) { + sender.sendMessage("§cCette commande est rĂ©servĂ©e aux joueurs en jeu."); + } else if (targetNode.getSenderType() == CommandSenderType.CONSOLE_ONLY) { + sender.sendMessage("§cCette commande ne peut ĂȘtre exĂ©cutĂ©e que depuis la console."); + } + } + return CommandResult.PERMISSION_DENIED; + } + + // 3. VĂ©rification des permissions + if (targetNode.getPermission() != null && !targetNode.getPermission().isEmpty()) { + if (!sender.hasPermission(targetNode.getPermission())) { + CommandPermissionDeniedEvent event = new CommandPermissionDeniedEvent( + targetNode, sender, null, targetNode.getPermission(), targetNode.getSenderType()); + dispatchEvent(targetNode, event); + + if (!event.isCancelled()) { + if (event.getCustomErrorMessage() != null) { + event.reply(event.getCustomErrorMessage()); + } else { + sender.sendMessage("§cVous n'avez pas la permission requise (§7" + targetNode.getPermission() + "§c)."); + } + } + return CommandResult.PERMISSION_DENIED; + } + } + + // 4. VĂ©rification du temps de recharge (Cooldown) + if (manager.getCooldownManager().isOnCooldown(targetNode, sender)) { + Duration remaining = manager.getCooldownManager().getRemainingCooldown(targetNode, sender); + CommandCooldownEvent event = new CommandCooldownEvent(targetNode, sender, null, remaining); + dispatchEvent(targetNode, event); + + if (!event.isCancelled()) { + if (event.getCustomMessage() != null) { + event.reply(event.getCustomMessage()); + } else { + long seconds = remaining.toSeconds(); + String timeStr = seconds > 0 ? seconds + "s" : remaining.toMillis() + "ms"; + sender.sendMessage("§cVeuillez patienter §6" + timeStr + " §cavant de rĂ©utiliser cette commande."); + } + return CommandResult.COOLDOWN; + } + } + + // 5. Analyse et parsing des arguments + Map parsedArguments = new LinkedHashMap<>(); + CommandContext partialContext = new CommandContext(sender, label, args, parsedArguments); + + List> expectedArgs = targetNode.getArguments(); + int expectedIndex = 0; + int remainingIndex = 0; + + while (expectedIndex < expectedArgs.size()) { + CommandArgument arg = expectedArgs.get(expectedIndex); + + if (arg.isGreedy()) { + if (remainingIndex >= remainingArgs.length) { + if (!arg.isOptional()) { + return handleSyntaxError(targetNode, sender, partialContext, + "Argument gourmand manquant : " + arg.getName(), targetNode.getUsage()); + } else { + parsedArguments.put(arg.getName(), arg.getDefaultValue()); + } + } else { + String greedyInput = String.join(" ", Arrays.copyOfRange(remainingArgs, remainingIndex, remainingArgs.length)); + try { + Object parsed = parseArgumentValue(arg, greedyInput, partialContext); + parsedArguments.put(arg.getName(), parsed); + } catch (CommandArgumentParseException e) { + return handleSyntaxError(targetNode, sender, partialContext, e.getMessage(), targetNode.getUsage()); + } + remainingIndex = remainingArgs.length; // Tous les arguments sont consommĂ©s + } + expectedIndex++; + break; + } + + if (remainingIndex >= remainingArgs.length) { + if (arg.isOptional()) { + parsedArguments.put(arg.getName(), arg.getDefaultValue()); + } else { + return handleSyntaxError(targetNode, sender, partialContext, + "Argument obligatoire manquant : <" + arg.getName() + ">", targetNode.getUsage()); + } + } else { + String input = remainingArgs[remainingIndex]; + try { + Object parsed = parseArgumentValue(arg, input, partialContext); + parsedArguments.put(arg.getName(), parsed); + } catch (CommandArgumentParseException e) { + return handleSyntaxError(targetNode, sender, partialContext, e.getMessage(), targetNode.getUsage()); + } + remainingIndex++; + } + expectedIndex++; + } + + // Si des arguments surnumĂ©raires ont Ă©tĂ© passĂ©s alors qu'aucun argument greedy n'est dĂ©fini + if (remainingIndex < remainingArgs.length && targetNode.getExecutor() != null && !expectedArgs.isEmpty() + && !expectedArgs.get(expectedArgs.size() - 1).isGreedy()) { + // Trop d'arguments fournis + return handleSyntaxError(targetNode, sender, partialContext, + "Trop d'arguments fournis pour cette commande.", targetNode.getUsage()); + } + + // Si le nƓud n'a pas d'exĂ©cuteur mais a des sous-commandes, afficher l'aide des sous-commandes + if (targetNode.getExecutor() == null) { + sendSubcommandsHelp(targetNode, sender); + return CommandResult.SUCCESS; + } + + // 6. Contexte final + CommandContext fullContext = new CommandContext(sender, label, args, parsedArguments); + + // 7. ÉvĂ©nement PreExecute + CommandPreExecuteEvent preEvent = new CommandPreExecuteEvent(targetNode, sender, fullContext); + dispatchEvent(targetNode, preEvent); + + if (preEvent.isCancelled()) { + return CommandResult.CANCELLED; + } + + // 8. ExĂ©cution du gestionnaire mĂ©tier + Throwable executionError = null; + CommandResult result; + + try { + targetNode.getExecutor().execute(fullContext); + result = CommandResult.SUCCESS; + manager.getCooldownManager().applyCooldown(targetNode, sender); + } catch (Throwable t) { + executionError = t; + result = CommandResult.FAILED; + sender.sendMessage("§cUne erreur interne est survenue lors de l'exĂ©cution de la commande."); + t.printStackTrace(); + } + + // 9. ÉvĂ©nement PostExecute + Duration duration = Duration.between(startTime, Instant.now()); + CommandPostExecuteEvent postEvent = new CommandPostExecuteEvent( + targetNode, sender, fullContext, result, duration, executionError); + dispatchEvent(targetNode, postEvent); + + return result; + } + + /** + * Calcule la liste de suggestions pour l'auto-complĂ©tion (Tab-Complete). + * + * @param rootCommand La commande racine. + * @param sender L'Ă©metteur. + * @param alias L'alias utilisĂ©. + * @param args Les arguments en cours de frappe. + * @return La liste des chaĂźnes de suggestion pour le client. + */ + public List tabComplete(Command rootCommand, CommandSender sender, String alias, String[] args) { + if (args.length == 0) { + return Collections.emptyList(); + } + + CommandNode targetNode = rootCommand; + int argIndex = 0; + + // Navigation dans les sous-commandes + while (argIndex < args.length - 1 && targetNode.hasSubCommands()) { + String candidate = args[argIndex]; + CommandNode sub = targetNode.findSubCommand(candidate); + if (sub != null) { + targetNode = sub; + argIndex++; + } else { + break; + } + } + + String currentInput = args[args.length - 1].toLowerCase(); + int relativeArgIndex = args.length - 1 - argIndex; + + List rawSuggestions = new ArrayList<>(); + + // Si le nƓud a des sous-commandes et qu'on est sur le premier mot aprĂšs le nƓud + if (relativeArgIndex == 0 && targetNode.hasSubCommands()) { + for (CommandNode sub : targetNode.getSubCommands()) { + if (sub.getPermission() == null || sender.hasPermission(sub.getPermission())) { + if (sub.getName().toLowerCase().startsWith(currentInput)) { + rawSuggestions.add(Suggestion.of(sub.getName(), sub.getDescription())); + } + } + } + } + + // Si le nƓud a des arguments dĂ©finis Ă  cet index + List> nodeArgs = targetNode.getArguments(); + if (relativeArgIndex >= 0 && relativeArgIndex < nodeArgs.size()) { + CommandArgument argument = nodeArgs.get(relativeArgIndex); + CommandContext partialContext = new CommandContext(sender, alias, args, Collections.emptyMap()); + rawSuggestions.addAll(argument.getSuggestions(partialContext, currentInput)); + } + + // DĂ©clenchement de l'Ă©vĂ©nement TabComplete + CommandTabCompleteEvent tabEvent = new CommandTabCompleteEvent( + targetNode, sender, null, rawSuggestions, currentInput, relativeArgIndex); + dispatchEvent(targetNode, tabEvent); + + if (tabEvent.isCancelled()) { + return Collections.emptyList(); + } + + return tabEvent.getSuggestions().stream() + .map(Suggestion::getValue) + .distinct() + .collect(Collectors.toList()); + } + + @SuppressWarnings("unchecked") + private T parseArgumentValue(CommandArgument arg, String input, CommandContext context) throws CommandArgumentParseException { + return arg.getType().parse(arg.getName(), input, context); + } + + private CommandResult handleSyntaxError(CommandNode node, CommandSender sender, CommandContext context, + String reason, String usage) { + CommandSyntaxErrorEvent event = new CommandSyntaxErrorEvent(node, sender, context, reason, usage); + dispatchEvent(node, event); + + if (!event.isCancelled()) { + if (event.getCustomMessage() != null) { + event.reply(event.getCustomMessage()); + } else { + sender.sendMessage("§cSyntaxe incorrecte : " + reason); + sender.sendMessage("§7Utilisation : §e" + usage); + } + } + return CommandResult.SYNTAX_ERROR; + } + + private void sendSubcommandsHelp(CommandNode node, CommandSender sender) { + sender.sendMessage("§6=== Aide : §e/" + node.getFullName() + " §6==="); + for (CommandNode sub : node.getSubCommands()) { + if (sub.getPermission() == null || sender.hasPermission(sub.getPermission())) { + String desc = sub.getDescription().isEmpty() ? "" : " §7- " + sub.getDescription(); + sender.sendMessage("§e" + sub.getUsage() + desc); + } + } + } + + private void dispatchEvent(CommandNode node, CommandEvent event) { + // 1. Écouteurs locaux sur le nƓud + if (event instanceof CommandPreExecuteEvent pre) { + node.getPreExecuteListeners().forEach(l -> l.onEvent(pre)); + } else if (event instanceof CommandPostExecuteEvent post) { + node.getPostExecuteListeners().forEach(l -> l.onEvent(post)); + } else if (event instanceof CommandPermissionDeniedEvent perm) { + node.getPermissionDeniedListeners().forEach(l -> l.onEvent(perm)); + } else if (event instanceof CommandSyntaxErrorEvent syn) { + node.getSyntaxErrorListeners().forEach(l -> l.onEvent(syn)); + } else if (event instanceof CommandCooldownEvent cool) { + node.getCooldownListeners().forEach(l -> l.onEvent(cool)); + } else if (event instanceof CommandTabCompleteEvent tab) { + node.getTabCompleteListeners().forEach(l -> l.onEvent(tab)); + } + + // 2. Écouteurs globaux dans le bus d'Ă©vĂ©nements + manager.getEventManager().dispatch(event); + } +} diff --git a/src/main/java/fr/luc/bettermccommands/platform/paper/PaperCommandMapInjector.java b/src/main/java/fr/luc/bettermccommands/platform/paper/PaperCommandMapInjector.java new file mode 100644 index 0000000..aab70e8 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/platform/paper/PaperCommandMapInjector.java @@ -0,0 +1,119 @@ +package fr.luc.bettermccommands.platform.paper; + +import fr.luc.bettermccommands.BetterMcCommands; +import fr.luc.bettermccommands.api.Command; +import fr.luc.bettermccommands.platform.CommandDispatcher; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.CommandMap; +import org.bukkit.command.SimpleCommandMap; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Gestionnaire d'injection et de retrait dynamique des commandes dans la {@link CommandMap} de Paper / Bukkit. + * Permet l'enregistrement Ă  chaud sans nĂ©cessiter de redĂ©marrage ni de dĂ©claration dans {@code plugin.yml}. + */ +public class PaperCommandMapInjector { + + private final Map registeredWrappers = new ConcurrentHashMap<>(); + private CommandMap commandMap; + private Map knownCommandsMap; + + /** + * Initialise l'injecteur et rĂ©sout la {@link CommandMap} du serveur. + */ + public PaperCommandMapInjector() { + resolveCommandMap(); + } + + /** + * Enregistre une commande betterMcCommands auprĂšs du serveur Bukkit/Paper. + * + * @param command La commande racine Ă  enregistrer. + * @param dispatcher Le dispatcher d'exĂ©cution. + * @param prefix Le prĂ©fixe d'enregistrement (ex: nom du plugin ou "bettermc"). + */ + public synchronized void register(Command command, CommandDispatcher dispatcher, String prefix) { + if (commandMap == null) { + resolveCommandMap(); + } + + if (commandMap == null) { + throw new IllegalStateException("Unable to resolve Bukkit CommandMap for dynamic registration."); + } + + PaperCommandWrapper wrapper = new PaperCommandWrapper(command, dispatcher); + commandMap.register(prefix != null ? prefix : "bettermc", wrapper); + registeredWrappers.put(command.getName().toLowerCase(), wrapper); + command.setRegistered(true); + } + + /** + * DĂ©senregistre une commande Ă  chaud du serveur Bukkit/Paper. + * + * @param command La commande Ă  retirer. + */ + public synchronized void unregister(Command command) { + PaperCommandWrapper wrapper = registeredWrappers.remove(command.getName().toLowerCase()); + if (wrapper == null) { + return; + } + + wrapper.unregister(commandMap); + + if (knownCommandsMap != null) { + knownCommandsMap.remove(wrapper.getName().toLowerCase()); + for (String alias : wrapper.getAliases()) { + knownCommandsMap.remove(alias.toLowerCase()); + } + if (wrapper.getLabel() != null) { + knownCommandsMap.remove(wrapper.getLabel().toLowerCase()); + } + } + + command.setRegistered(false); + } + + /** + * DĂ©senregistre l'ensemble des commandes injectĂ©es. + */ + public synchronized void unregisterAll() { + for (PaperCommandWrapper wrapper : registeredWrappers.values()) { + wrapper.getRootCommand().unregister(); + } + registeredWrappers.clear(); + } + + @SuppressWarnings("unchecked") + private void resolveCommandMap() { + Server server = Bukkit.getServer(); + if (server == null) { + return; // Environnement de test unitaire oĂč le serveur Bukkit n'est pas instanciĂ© + } + + try { + // Tentative directe via la mĂ©thode getCommandMap() moderne + try { + Method getCommandMapMethod = server.getClass().getMethod("getCommandMap"); + this.commandMap = (CommandMap) getCommandMapMethod.invoke(server); + } catch (NoSuchMethodException ignored) { + // Fallback rĂ©flexion sur le champ commandMap de CraftServer + Field commandMapField = server.getClass().getDeclaredField("commandMap"); + commandMapField.setAccessible(true); + this.commandMap = (CommandMap) commandMapField.get(server); + } + + if (this.commandMap instanceof SimpleCommandMap simpleMap) { + Field knownCommandsField = SimpleCommandMap.class.getDeclaredField("knownCommands"); + knownCommandsField.setAccessible(true); + this.knownCommandsMap = (Map) knownCommandsField.get(simpleMap); + } + } catch (Exception e) { + System.err.println("[betterMcCommands] Failed to access Bukkit CommandMap via reflection: " + e.getMessage()); + } + } +} diff --git a/src/main/java/fr/luc/bettermccommands/platform/paper/PaperCommandWrapper.java b/src/main/java/fr/luc/bettermccommands/platform/paper/PaperCommandWrapper.java new file mode 100644 index 0000000..97f7ee9 --- /dev/null +++ b/src/main/java/fr/luc/bettermccommands/platform/paper/PaperCommandWrapper.java @@ -0,0 +1,50 @@ +package fr.luc.bettermccommands.platform.paper; + +import fr.luc.bettermccommands.api.Command; +import fr.luc.bettermccommands.platform.CommandDispatcher; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; +import java.util.List; + +/** + * Enveloppe Bukkit permettant d'enregistrer une commande {@link Command} betterMcCommands dans le systĂšme natif Bukkit/Paper. + */ +public class PaperCommandWrapper extends org.bukkit.command.Command { + + private final Command rootCommand; + private final CommandDispatcher dispatcher; + + /** + * CrĂ©e l'enveloppe de commande Bukkit. + * + * @param rootCommand La commande racine betterMcCommands. + * @param dispatcher Le dispatcher responsable de l'exĂ©cution. + */ + public PaperCommandWrapper(Command rootCommand, CommandDispatcher dispatcher) { + super(rootCommand.getName(), rootCommand.getDescription(), rootCommand.getUsage(), new ArrayList<>(rootCommand.getAliases())); + this.rootCommand = rootCommand; + this.dispatcher = dispatcher; + if (rootCommand.getPermission() != null) { + setPermission(rootCommand.getPermission()); + } + } + + /** + * @return La commande racine betterMcCommands associĂ©e. + */ + public Command getRootCommand() { + return rootCommand; + } + + @Override + public boolean execute(CommandSender sender, String commandLabel, String[] args) { + dispatcher.execute(rootCommand, sender, commandLabel, args); + return true; + } + + @Override + public List tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException { + return dispatcher.tabComplete(rootCommand, sender, alias, args); + } +} diff --git a/src/test/java/fr/luc/bettermccommands/ArgumentParsingTest.java b/src/test/java/fr/luc/bettermccommands/ArgumentParsingTest.java new file mode 100644 index 0000000..b831cf6 --- /dev/null +++ b/src/test/java/fr/luc/bettermccommands/ArgumentParsingTest.java @@ -0,0 +1,97 @@ +package fr.luc.bettermccommands; + +import fr.luc.bettermccommands.api.CommandContext; +import fr.luc.bettermccommands.argument.CommandArgumentParseException; +import fr.luc.bettermccommands.argument.type.*; +import fr.luc.bettermccommands.mock.SampleRank; +import org.bukkit.command.CommandSender; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.Duration; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +public class ArgumentParsingTest { + + private CommandContext dummyContext; + + @BeforeEach + void setUp() { + CommandSender sender = Mockito.mock(CommandSender.class); + dummyContext = new CommandContext(sender, "test", new String[0], Collections.emptyMap()); + } + + @Test + @DisplayName("IntegerArgument doit parser les entiers valides et respecter les bornes") + void testIntegerArgument() throws Exception { + IntegerArgument arg = IntegerArgument.range(1, 10); + + assertEquals(5, arg.parse("count", "5", dummyContext)); + assertEquals(1, arg.parse("count", "1", dummyContext)); + assertEquals(10, arg.parse("count", "10", dummyContext)); + + assertThrows(CommandArgumentParseException.class, () -> arg.parse("count", "0", dummyContext)); + assertThrows(CommandArgumentParseException.class, () -> arg.parse("count", "11", dummyContext)); + assertThrows(CommandArgumentParseException.class, () -> arg.parse("count", "notANumber", dummyContext)); + } + + @Test + @DisplayName("DoubleArgument doit supporter les formats avec point et virgule") + void testDoubleArgument() throws Exception { + DoubleArgument arg = DoubleArgument.min(0.5); + + assertEquals(1.5, arg.parse("amount", "1.5", dummyContext), 0.001); + assertEquals(2.5, arg.parse("amount", "2,5", dummyContext), 0.001); + + assertThrows(CommandArgumentParseException.class, () -> arg.parse("amount", "0.2", dummyContext)); + assertThrows(CommandArgumentParseException.class, () -> arg.parse("amount", "abc", dummyContext)); + } + + @Test + @DisplayName("BooleanArgument doit convertir les formats true/false, oui/non, 1/0") + void testBooleanArgument() throws Exception { + BooleanArgument arg = BooleanArgument.bool(); + + assertTrue(arg.parse("flag", "true", dummyContext)); + assertTrue(arg.parse("flag", "oui", dummyContext)); + assertTrue(arg.parse("flag", "1", dummyContext)); + assertTrue(arg.parse("flag", "yes", dummyContext)); + + assertFalse(arg.parse("flag", "false", dummyContext)); + assertFalse(arg.parse("flag", "non", dummyContext)); + assertFalse(arg.parse("flag", "0", dummyContext)); + assertFalse(arg.parse("flag", "no", dummyContext)); + + assertThrows(CommandArgumentParseException.class, () -> arg.parse("flag", "invalid", dummyContext)); + } + + @Test + @DisplayName("EnumArgument doit convertir les constantes enum insensiblement Ă  la casse") + void testEnumArgument() throws Exception { + EnumArgument arg = EnumArgument.of(SampleRank.class); + + assertEquals(SampleRank.ADMIN, arg.parse("rank", "admin", dummyContext)); + assertEquals(SampleRank.MEMBER, arg.parse("rank", "MEMBER", dummyContext)); + assertEquals(SampleRank.VIP, arg.parse("rank", "Vip", dummyContext)); + + assertThrows(CommandArgumentParseException.class, () -> arg.parse("rank", "OWNER", dummyContext)); + } + + @Test + @DisplayName("DurationArgument doit parser les chaĂźnes temporelles avec unitĂ©s variĂ©es") + void testDurationArgument() throws Exception { + DurationArgument arg = DurationArgument.duration(); + + assertEquals(Duration.ofSeconds(30), arg.parse("time", "30s", dummyContext)); + assertEquals(Duration.ofMinutes(15), arg.parse("time", "15m", dummyContext)); + assertEquals(Duration.ofHours(2), arg.parse("time", "2h", dummyContext)); + assertEquals(Duration.ofDays(1), arg.parse("time", "1d", dummyContext)); + assertEquals(Duration.ofSeconds(100), arg.parse("time", "1m 40s", dummyContext)); + + assertThrows(CommandArgumentParseException.class, () -> arg.parse("time", "invalid", dummyContext)); + } +} diff --git a/src/test/java/fr/luc/bettermccommands/CommandEventLifecycleTest.java b/src/test/java/fr/luc/bettermccommands/CommandEventLifecycleTest.java new file mode 100644 index 0000000..99a4917 --- /dev/null +++ b/src/test/java/fr/luc/bettermccommands/CommandEventLifecycleTest.java @@ -0,0 +1,120 @@ +package fr.luc.bettermccommands; + +import fr.luc.bettermccommands.api.Command; +import fr.luc.bettermccommands.api.CommandResult; +import fr.luc.bettermccommands.event.*; +import fr.luc.bettermccommands.mock.SampleEventListener; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +public class CommandEventLifecycleTest { + + private BetterMcCommands manager; + + @BeforeEach + void setUp() { + manager = new BetterMcCommands("test"); + } + + @Test + @DisplayName("L'annulation de PreExecuteEvent doit bloquer l'exĂ©cution") + void testPreExecuteCancellation() { + AtomicBoolean executed = new AtomicBoolean(false); + + Command command = BetterMcCommands.builder("tpall") + .onPreExecute(event -> event.setCancelled(true)) + .executes(context -> executed.set(true)) + .build(); + + Player player = Mockito.mock(Player.class); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + + CommandResult result = manager.getDispatcher().execute(command, player, "tpall", new String[0]); + + assertEquals(CommandResult.CANCELLED, result); + assertFalse(executed.get()); + } + + @Test + @DisplayName("PostExecuteEvent doit capturer le succĂšs et la durĂ©e") + void testPostExecuteEvent() { + AtomicReference capturedEvent = new AtomicReference<>(); + + manager.on(CommandPostExecuteEvent.class, capturedEvent::set); + + Command command = BetterMcCommands.builder("heal") + .executes(context -> { + // Simule un petit traitement + Thread.sleep(10); + }) + .build(); + + Player player = Mockito.mock(Player.class); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + + CommandResult result = manager.getDispatcher().execute(command, player, "heal", new String[0]); + + assertEquals(CommandResult.SUCCESS, result); + assertNotNull(capturedEvent.get()); + assertTrue(capturedEvent.get().isSuccessful()); + assertTrue(capturedEvent.get().getExecutionDuration().toMillis() >= 5); + } + + @Test + @DisplayName("Le systĂšme de Cooldown doit bloquer les exĂ©cutions successives d'un joueur") + void testCommandCooldown() { + Command command = BetterMcCommands.builder("kit") + .cooldown(Duration.ofSeconds(60)) + .executes(context -> {}) + .build(); + + Player player = Mockito.mock(Player.class); + UUID uuid = UUID.randomUUID(); + when(player.getUniqueId()).thenReturn(uuid); + + // 1Ăšre exĂ©cution -> RĂ©ussie + CommandResult result1 = manager.getDispatcher().execute(command, player, "kit", new String[0]); + assertEquals(CommandResult.SUCCESS, result1); + + // 2Ăšme exĂ©cution immĂ©diate -> BloquĂ©e par Cooldown + CommandResult result2 = manager.getDispatcher().execute(command, player, "kit", new String[0]); + assertEquals(CommandResult.COOLDOWN, result2); + + // Reset cooldown + manager.getCooldownManager().resetCooldown(command, player); + + // 3Ăšme exĂ©cution aprĂšs reset -> RĂ©ussie + CommandResult result3 = manager.getDispatcher().execute(command, player, "kit", new String[0]); + assertEquals(CommandResult.SUCCESS, result3); + } + + @Test + @DisplayName("Les Ă©couteurs annotĂ©s @CommandEventHandler doivent ĂȘtre appelĂ©s correctement") + void testAnnotatedEventListeners() { + SampleEventListener listener = new SampleEventListener(); + manager.registerListeners(listener); + + Command command = BetterMcCommands.builder("custom") + .executes(context -> {}) + .build(); + + Player player = Mockito.mock(Player.class); + when(player.getUniqueId()).thenReturn(UUID.randomUUID()); + + manager.getDispatcher().execute(command, player, "custom", new String[0]); + + assertTrue(listener.preFired.get()); + assertTrue(listener.postFired.get()); + } +} diff --git a/src/test/java/fr/luc/bettermccommands/CommandExecutionTest.java b/src/test/java/fr/luc/bettermccommands/CommandExecutionTest.java new file mode 100644 index 0000000..1045ece --- /dev/null +++ b/src/test/java/fr/luc/bettermccommands/CommandExecutionTest.java @@ -0,0 +1,139 @@ +package fr.luc.bettermccommands; + +import fr.luc.bettermccommands.api.Command; +import fr.luc.bettermccommands.api.CommandResult; +import fr.luc.bettermccommands.argument.Arguments; +import fr.luc.bettermccommands.platform.CommandDispatcher; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +public class CommandExecutionTest { + + private BetterMcCommands manager; + private CommandDispatcher dispatcher; + + @BeforeEach + void setUp() { + manager = new BetterMcCommands("test"); + dispatcher = manager.getDispatcher(); + } + + @Test + @DisplayName("ExĂ©cution simple d'une commande racine") + void testRootCommandExecution() { + AtomicBoolean executed = new AtomicBoolean(false); + + Command command = BetterMcCommands.builder("ping") + .executes(context -> executed.set(true)) + .build(); + + CommandSender sender = Mockito.mock(CommandSender.class); + CommandResult result = dispatcher.execute(command, sender, "ping", new String[0]); + + assertEquals(CommandResult.SUCCESS, result); + assertTrue(executed.get()); + } + + @Test + @DisplayName("ExĂ©cution d'une sous-commande avec arguments obligatoires et optionnels") + void testSubCommandWithArguments() { + AtomicReference targetPlayer = new AtomicReference<>(); + AtomicInteger amountGiven = new AtomicInteger(); + + Command command = BetterMcCommands.builder("money") + .subcommand(BetterMcCommands.subBuilder("give") + .argument(Arguments.string("player")) + .argument(Arguments.integer("amount", 1, 1000).defaultValue(50)) + .executes(context -> { + targetPlayer.set(context.getString("player")); + amountGiven.set(context.getInt("amount")); + }) + ) + .build(); + + CommandSender sender = Mockito.mock(CommandSender.class); + + // Appel avec argument optionnel omis (valeur par dĂ©faut 50) + CommandResult result1 = dispatcher.execute(command, sender, "money", new String[]{"give", "Luc"}); + assertEquals(CommandResult.SUCCESS, result1); + assertEquals("Luc", targetPlayer.get()); + assertEquals(50, amountGiven.get()); + + // Appel avec argument optionnel spĂ©cifiĂ© (200) + CommandResult result2 = dispatcher.execute(command, sender, "money", new String[]{"give", "Alex", "200"}); + assertEquals(CommandResult.SUCCESS, result2); + assertEquals("Alex", targetPlayer.get()); + assertEquals(200, amountGiven.get()); + + // Appel avec argument obligatoire manquant + CommandResult result3 = dispatcher.execute(command, sender, "money", new String[]{"give"}); + assertEquals(CommandResult.SYNTAX_ERROR, result3); + } + + @Test + @DisplayName("ContrĂŽle des permissions d'exĂ©cution") + void testPermissionCheck() { + Command command = BetterMcCommands.builder("secret") + .permission("admin.secret") + .executes(context -> {}) + .build(); + + CommandSender unauthorizedSender = Mockito.mock(CommandSender.class); + when(unauthorizedSender.hasPermission("admin.secret")).thenReturn(false); + + CommandResult result1 = dispatcher.execute(command, unauthorizedSender, "secret", new String[0]); + assertEquals(CommandResult.PERMISSION_DENIED, result1); + + CommandSender authorizedSender = Mockito.mock(CommandSender.class); + when(authorizedSender.hasPermission("admin.secret")).thenReturn(true); + + CommandResult result2 = dispatcher.execute(command, authorizedSender, "secret", new String[0]); + assertEquals(CommandResult.SUCCESS, result2); + } + + @Test + @DisplayName("Restriction par type d'Ă©metteur (Player only)") + void testPlayerOnlySenderType() { + Command command = BetterMcCommands.builder("spawn") + .playerOnly() + .executes(context -> {}) + .build(); + + ConsoleCommandSender consoleSender = Mockito.mock(ConsoleCommandSender.class); + CommandResult result1 = dispatcher.execute(command, consoleSender, "spawn", new String[0]); + assertEquals(CommandResult.PERMISSION_DENIED, result1); + + Player playerSender = Mockito.mock(Player.class); + CommandResult result2 = dispatcher.execute(command, playerSender, "spawn", new String[0]); + assertEquals(CommandResult.SUCCESS, result2); + } + + @Test + @DisplayName("Argument greedy consommant tout le reste de la ligne") + void testGreedyArgument() { + AtomicReference fullMessage = new AtomicReference<>(); + + Command command = BetterMcCommands.builder("say") + .argument(Arguments.greedyString("message")) + .executes(context -> fullMessage.set(context.getString("message"))) + .build(); + + CommandSender sender = Mockito.mock(CommandSender.class); + CommandResult result = dispatcher.execute(command, sender, "say", new String[]{"Bonjour", "tout", "le", "monde", "!"}); + + assertEquals(CommandResult.SUCCESS, result); + assertEquals("Bonjour tout le monde !", fullMessage.get()); + } +} diff --git a/src/test/java/fr/luc/bettermccommands/mock/SampleEventListener.java b/src/test/java/fr/luc/bettermccommands/mock/SampleEventListener.java new file mode 100644 index 0000000..ea07c4f --- /dev/null +++ b/src/test/java/fr/luc/bettermccommands/mock/SampleEventListener.java @@ -0,0 +1,26 @@ +package fr.luc.bettermccommands.mock; + +import fr.luc.bettermccommands.event.CommandPostExecuteEvent; +import fr.luc.bettermccommands.event.CommandPreExecuteEvent; +import fr.luc.bettermccommands.event.annotation.CommandEventHandler; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Écouteur d'exemple pour les tests. + */ +public class SampleEventListener { + + public final AtomicBoolean preFired = new AtomicBoolean(false); + public final AtomicBoolean postFired = new AtomicBoolean(false); + + @CommandEventHandler + public void onPre(CommandPreExecuteEvent event) { + preFired.set(true); + } + + @CommandEventHandler + public void onPost(CommandPostExecuteEvent event) { + postFired.set(true); + } +} diff --git a/src/test/java/fr/luc/bettermccommands/mock/SampleRank.java b/src/test/java/fr/luc/bettermccommands/mock/SampleRank.java new file mode 100644 index 0000000..b70f011 --- /dev/null +++ b/src/test/java/fr/luc/bettermccommands/mock/SampleRank.java @@ -0,0 +1,8 @@ +package fr.luc.bettermccommands.mock; + +/** + * Enum d'exemple pour les tests unitaires. + */ +public enum SampleRank { + MEMBER, ADMIN, VIP +}