Initialisation complète de la bibliothèque betterMcCommands (DSL, arguments typés, cycle de vie des événements, injection dynamique CommandMap)
This commit is contained in:
+17
@@ -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
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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 (`<gradient>`, `<green>`, 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("<red>Impossible d'exécuter cette commande en combat !</red>");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Exécution de la commande racine : /commande-demo
|
||||||
|
.executes(context -> {
|
||||||
|
context.reply("<gradient:#00d2ff:#3a7bd5><bold>=== Démo betterMcCommands ===</bold></gradient>");
|
||||||
|
context.reply("<gray>Utilisez <yellow>/demo give</yellow> pour donner des ressources.</gray>");
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sous-commande : /demo give <cible> [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 <gold>" + quantite + "</gold> items à <aqua>" + target.getName() + "</aqua> !");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sous-commande avec argument gourmand (greedy) : /demo broadcast <message...>
|
||||||
|
.subcommand(BetterMcCommands.subBuilder("broadcast")
|
||||||
|
.description("Diffuse une annonce")
|
||||||
|
.argument(Arguments.greedyString("message"))
|
||||||
|
.executes(context -> {
|
||||||
|
String msg = context.getString("message");
|
||||||
|
context.replySuccess("Diffusion : <yellow>" + msg + "</yellow>");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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("<dark_red>Accès refusé ! Permission requise : " + event.getRequiredPermission() + "</dark_red>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandEventHandler
|
||||||
|
public void onCooldown(CommandCooldownEvent event) {
|
||||||
|
event.setCustomMessage("<red>⏳ Patientez <gold>" + event.getRemainingCooldown().toSeconds() + "s</gold> avant de réutiliser cette commande.</red>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 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)
|
||||||
|
```
|
||||||
|
|||||||
@@ -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<JavaCompile> {
|
||||||
|
options.encoding = "UTF-8"
|
||||||
|
options.release.set(17)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Javadoc> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Vendored
BIN
Binary file not shown.
+9
@@ -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
|
||||||
@@ -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" "$@"
|
||||||
Vendored
+82
@@ -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%
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = "betterMcCommands"
|
||||||
@@ -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<String, Command> 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 <T> Le type de l'événement.
|
||||||
|
*/
|
||||||
|
public <T extends CommandEvent> void on(Class<T> eventType, CommandEventListener<T> 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<Command> getRegisteredCommands() {
|
||||||
|
return Collections.unmodifiableCollection(registeredCommands.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Le préfixe de namespace de cette instance.
|
||||||
|
*/
|
||||||
|
public String getPluginPrefix() {
|
||||||
|
return pluginPrefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Object> arguments;
|
||||||
|
private final Map<String, Object> 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<String, Object> 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<String, Object> 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 <T> 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> T get(String name, Class<T> 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 <T> Le type générique.
|
||||||
|
* @return Un {@link Optional} contenant la valeur si présente.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> Optional<T> getOptional(String name, Class<T> 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 <T> Le type générique.
|
||||||
|
* @return Un {@link Optional} contenant la métadonnée si présente.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> Optional<T> getMetadata(String key, Class<T> 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: "<green>Succès !</green>").
|
||||||
|
*/
|
||||||
|
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("<dark_gray>[<green>✔</green>]</dark_gray> <green>" + message + "</green>");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un message d'erreur (préfixé en rouge).
|
||||||
|
*
|
||||||
|
* @param message Le message d'erreur.
|
||||||
|
*/
|
||||||
|
public void replyError(String message) {
|
||||||
|
reply("<dark_gray>[<red>✖</red>]</dark_gray> <red>" + message + "</red>");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un message informatif (préfixé en bleu/aqua).
|
||||||
|
*
|
||||||
|
* @param message Le message d'information.
|
||||||
|
*/
|
||||||
|
public void replyInfo(String message) {
|
||||||
|
reply("<dark_gray>[<aqua>ℹ</aqua>]</dark_gray> <gray>" + message + "</gray>");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<String> aliases = new LinkedHashSet<>();
|
||||||
|
protected CommandNode parent;
|
||||||
|
protected final Map<String, CommandNode> subCommands = new LinkedHashMap<>();
|
||||||
|
protected final List<CommandArgument<?>> 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<CommandEventListener<CommandPreExecuteEvent>> preExecuteListeners = new CopyOnWriteArrayList<>();
|
||||||
|
protected final List<CommandEventListener<CommandPostExecuteEvent>> postExecuteListeners = new CopyOnWriteArrayList<>();
|
||||||
|
protected final List<CommandEventListener<CommandPermissionDeniedEvent>> permissionDeniedListeners = new CopyOnWriteArrayList<>();
|
||||||
|
protected final List<CommandEventListener<CommandSyntaxErrorEvent>> syntaxErrorListeners = new CopyOnWriteArrayList<>();
|
||||||
|
protected final List<CommandEventListener<CommandCooldownEvent>> cooldownListeners = new CopyOnWriteArrayList<>();
|
||||||
|
protected final List<CommandEventListener<CommandTabCompleteEvent>> 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<String> getAliases() {
|
||||||
|
return Collections.unmodifiableSet(aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAliases(Collection<String> 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<CommandNode> 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<CommandArgument<?>> getArguments() {
|
||||||
|
return Collections.unmodifiableList(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addArguments(Collection<CommandArgument<?>> 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 <cible> [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<CommandEventListener<CommandPreExecuteEvent>> getPreExecuteListeners() {
|
||||||
|
return preExecuteListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CommandEventListener<CommandPostExecuteEvent>> getPostExecuteListeners() {
|
||||||
|
return postExecuteListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CommandEventListener<CommandPermissionDeniedEvent>> getPermissionDeniedListeners() {
|
||||||
|
return permissionDeniedListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CommandEventListener<CommandSyntaxErrorEvent>> getSyntaxErrorListeners() {
|
||||||
|
return syntaxErrorListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CommandEventListener<CommandCooldownEvent>> getCooldownListeners() {
|
||||||
|
return cooldownListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CommandEventListener<CommandTabCompleteEvent>> getTabCompleteListeners() {
|
||||||
|
return tabCompleteListeners;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Suggestion> 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<String> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <T> Le type de données retourné par le parseur.
|
||||||
|
*/
|
||||||
|
public interface ArgumentType<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Suggestion> 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: "<joueur>", "[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 + ">";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> 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(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<String> 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> 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> 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> 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<Double> 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<Double> 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<Boolean> 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> 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> 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> 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> 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> 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 <E> Le type de l'enum.
|
||||||
|
* @return Le {@link CommandArgument} configuré.
|
||||||
|
*/
|
||||||
|
public static <E extends Enum<E>> CommandArgument<E> enumOf(String name, Class<E> 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 <T> Le type de l'objet.
|
||||||
|
* @return Le {@link CommandArgument} configuré.
|
||||||
|
*/
|
||||||
|
public static <T> CommandArgument<T> custom(String name, ArgumentType<T> type) {
|
||||||
|
return new CommandArgument<>(name, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <T> Le type de l'argument.
|
||||||
|
*/
|
||||||
|
public class CommandArgument<T> {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final ArgumentType<T> type;
|
||||||
|
private String description;
|
||||||
|
private boolean optional;
|
||||||
|
private Supplier<T> 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<T> 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<T> 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<T> 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<T> 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<T> defaultValue(Supplier<T> 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<T> 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<T> 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<T> greedy() {
|
||||||
|
this.greedy = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Le nom de l'argument.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Le type associé.
|
||||||
|
*/
|
||||||
|
public ArgumentType<T> 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<Suggestion> 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: "<joueur>" ou "[quantite]"
|
||||||
|
*/
|
||||||
|
public String getUsage() {
|
||||||
|
return type.getUsagePlaceholder(name, optional);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Boolean> {
|
||||||
|
|
||||||
|
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<Suggestion> suggest(CommandContext context, String currentInput) {
|
||||||
|
String lower = currentInput == null ? "" : currentInput.toLowerCase();
|
||||||
|
List<String> 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Double> {
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Duration> {
|
||||||
|
|
||||||
|
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<Suggestion> suggest(CommandContext context, String currentInput) {
|
||||||
|
String lower = currentInput == null ? "" : currentInput.toLowerCase();
|
||||||
|
List<String> 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <E> Le type d'enum.
|
||||||
|
*/
|
||||||
|
public class EnumArgument<E extends Enum<E>> implements ArgumentType<E> {
|
||||||
|
|
||||||
|
private final Class<E> enumClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un parseur d'enum pour la classe donnée.
|
||||||
|
*
|
||||||
|
* @param enumClass La classe d'enum.
|
||||||
|
*/
|
||||||
|
public EnumArgument(Class<E> enumClass) {
|
||||||
|
this.enumClass = Objects.requireNonNull(enumClass, "enumClass cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fabrique un nouveau type d'argument enum.
|
||||||
|
*
|
||||||
|
* @param enumClass La classe d'enum.
|
||||||
|
* @param <E> Le type de l'enum.
|
||||||
|
* @return L'instance configurée de {@link EnumArgument}.
|
||||||
|
*/
|
||||||
|
public static <E extends Enum<E>> EnumArgument<E> of(Class<E> 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<Suggestion> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Integer> {
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Location> {
|
||||||
|
|
||||||
|
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)";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<OfflinePlayer> {
|
||||||
|
|
||||||
|
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<Suggestion> 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)";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Player> {
|
||||||
|
|
||||||
|
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<Suggestion> 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<World> {
|
||||||
|
|
||||||
|
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<Suggestion> 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <B> Le type concret du Builder (pour permettre le chaînage fluide).
|
||||||
|
* @param <N> Le type concret de nœud produit (Command ou CommandNode).
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public abstract class AbstractCommandBuilder<B extends AbstractCommandBuilder<B, N>, N extends CommandNode> {
|
||||||
|
|
||||||
|
protected final String name;
|
||||||
|
protected String description;
|
||||||
|
protected String permission;
|
||||||
|
protected CommandSenderType senderType = CommandSenderType.ALL;
|
||||||
|
protected final Set<String> aliases = new LinkedHashSet<>();
|
||||||
|
protected final List<CommandArgument<?>> arguments = new ArrayList<>();
|
||||||
|
protected final List<SubCommandBuilder> subCommandBuilders = new ArrayList<>();
|
||||||
|
protected CommandExecutor executor;
|
||||||
|
protected Duration cooldown = Duration.ZERO;
|
||||||
|
protected String cooldownBypassPermission;
|
||||||
|
|
||||||
|
protected final List<CommandEventListener<CommandPreExecuteEvent>> preExecuteListeners = new ArrayList<>();
|
||||||
|
protected final List<CommandEventListener<CommandPostExecuteEvent>> postExecuteListeners = new ArrayList<>();
|
||||||
|
protected final List<CommandEventListener<CommandPermissionDeniedEvent>> permissionDeniedListeners = new ArrayList<>();
|
||||||
|
protected final List<CommandEventListener<CommandSyntaxErrorEvent>> syntaxErrorListeners = new ArrayList<>();
|
||||||
|
protected final List<CommandEventListener<CommandCooldownEvent>> cooldownListeners = new ArrayList<>();
|
||||||
|
protected final List<CommandEventListener<CommandTabCompleteEvent>> 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<CommandPreExecuteEvent> 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<CommandPostExecuteEvent> 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<CommandPermissionDeniedEvent> 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<CommandSyntaxErrorEvent> 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<CommandCooldownEvent> 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<CommandTabCompleteEvent> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<CommandBuilder, Command> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SubCommandBuilder, CommandNode> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Map<UUID, Instant>> 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<UUID, Instant> 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<UUID, Instant> 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<UUID, Instant> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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é.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<red>Action annulée par le système de sécurité !</red>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Exécution de la commande racine : /commande-demo
|
||||||
|
.executes(context -> {
|
||||||
|
context.reply("<gradient:#00d2ff:#3a7bd5><bold>=== Démonstration betterMcCommands ===</bold></gradient>");
|
||||||
|
context.reply("<gray>Bienvenue <aqua>" + context.getSender().getName() + "</aqua> !</gray>");
|
||||||
|
context.reply("<yellow>Sous-commandes disponibles :</yellow>");
|
||||||
|
context.reply(" <gold>•</gold> <yellow>/demo give <cible> [quantite]</yellow> <gray>- Donne des items</gray>");
|
||||||
|
context.reply(" <gold>•</gold> <yellow>/demo broadcast <message...></yellow> <gray>- Diffuse une annonce</gray>");
|
||||||
|
context.reply(" <gold>•</gold> <yellow>/demo tempban <joueur> <duree> <raison...></yellow> <gray>- Bannit temporairement</gray>");
|
||||||
|
context.reply(" <gold>•</gold> <yellow>/demo cooldown-test</yellow> <gray>- Teste un cooldown de 10s</gray>");
|
||||||
|
context.reply(" <gold>•</gold> <yellow>/demo admin rank set <joueur> <grade></yellow> <gray>- Sous-commandes imbriquées</gray>");
|
||||||
|
})
|
||||||
|
|
||||||
|
// 1. Sous-commande : /demo give <cible> [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 <gold>" + amount + "</gold> ressource(s) à <aqua>" + target.getName() + "</aqua> !");
|
||||||
|
target.sendMessage("§aVous avez reçu §6" + amount + " §aresource(s) de la part de §b" + context.getSender().getName() + "§a.");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. Sous-commande : /demo broadcast <message...>
|
||||||
|
.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 : <yellow>" + message + "</yellow>");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. Sous-commande : /demo tempban <joueur> <duree> <raison...>
|
||||||
|
.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 <red>" + target.getName() + "</red> banni pour <gold>" +
|
||||||
|
duration.toMinutes() + " minutes</gold>. Motif : <yellow>" + reason + "</yellow>.");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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("<green>Cooldown validé ! Vous venez d'exécuter la commande. Réessayez immédiatement pour tester le blocage.</green>");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// 5. Arborescence imbriquée : /demo admin rank set <joueur> <grade>
|
||||||
|
.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 <aqua>" + target.getName() + "</aqua> a été défini sur <gold>" + rank.name() + "</gold>.");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("<dark_red><bold>Accès Restreint</bold></dark_red> : Permission requise <gray>[" +
|
||||||
|
event.getRequiredPermission() + "]</gray>.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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("<red>⏳ Doucement ! Vous devez encore attendre <gold>" +
|
||||||
|
(seconds > 0 ? seconds + "s" : event.getRemainingCooldown().toMillis() + "ms") +
|
||||||
|
"</gold> avant de réutiliser <yellow>/" + event.getNode().getFullName() + "</yellow>.</red>");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package fr.luc.bettermccommands.event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface fonctionnelle pour écouter un événement de commande typé.
|
||||||
|
*
|
||||||
|
* @param <T> Le type d'événement écouté.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface CommandEventListener<T extends CommandEvent> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoqué lorsque l'événement de commande survient.
|
||||||
|
*
|
||||||
|
* @param event L'instance de l'événement.
|
||||||
|
*/
|
||||||
|
void onEvent(T event);
|
||||||
|
}
|
||||||
@@ -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<CommandEvent> listener, String commandFilter, int priority) {}
|
||||||
|
|
||||||
|
private final List<ListenerRegistration> 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 <T> Le type de l'événement.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T extends CommandEvent> void register(Class<T> eventType, CommandEventListener<T> 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 <T> Le type de l'événement.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T extends CommandEvent> void register(Class<T> eventType, CommandEventListener<T> 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<CommandEvent>) 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<? extends CommandEvent> eventType = (Class<? extends CommandEvent>) params[0];
|
||||||
|
method.setAccessible(true);
|
||||||
|
|
||||||
|
CommandEventListener<CommandEvent> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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).
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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).
|
||||||
|
* <p>
|
||||||
|
* 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 <joueur> [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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Suggestion> 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<Suggestion> 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<Suggestion> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<String, Object> parsedArguments = new LinkedHashMap<>();
|
||||||
|
CommandContext partialContext = new CommandContext(sender, label, args, parsedArguments);
|
||||||
|
|
||||||
|
List<CommandArgument<?>> 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<String> 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<Suggestion> 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<CommandArgument<?>> 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> T parseArgumentValue(CommandArgument<T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, PaperCommandWrapper> registeredWrappers = new ConcurrentHashMap<>();
|
||||||
|
private CommandMap commandMap;
|
||||||
|
private Map<String, org.bukkit.command.Command> 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<String, org.bukkit.command.Command>) knownCommandsField.get(simpleMap);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("[betterMcCommands] Failed to access Bukkit CommandMap via reflection: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException {
|
||||||
|
return dispatcher.tabComplete(rootCommand, sender, alias, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SampleRank> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<CommandPostExecuteEvent> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> 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<String> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package fr.luc.bettermccommands.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum d'exemple pour les tests unitaires.
|
||||||
|
*/
|
||||||
|
public enum SampleRank {
|
||||||
|
MEMBER, ADMIN, VIP
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user