Ajout des fonctionnalités de validation de commandes/arguments, confirmation préalable (double validation) et répétition cadencée (CommandRepeater)

This commit is contained in:
2026-08-20 17:58:15 +02:00
parent ac2937709f
commit a2ccf0eb8c
16 changed files with 918 additions and 6 deletions
+99 -5
View File
@@ -18,7 +18,10 @@ Une bibliothèque Java moderne, fluide et événementielle permettant de créer,
- [1. Commande de Démonstration Complète (`/commande-demo`)](#1-commande-de-démonstration-complète-commande-demo)
- [2. Sous-Commandes Imbriquées](#2-sous-commandes-imbriquées)
- [3. Arguments Gourmands (Greedy String)](#3-arguments-gourmands-greedy-string)
- [4. Temps de Recharge (Cooldowns)](#4-temps-de-recharge-cooldowns)
- [4. Système de Validation Métier (Pré-requis)](#4-système-de-validation-métier-pré-requis)
- [5. Confirmation Préalable & Double Validation (`.requireConfirmation`)](#5-confirmation-préalable--double-validation-requireconfirmation)
- [6. Répétition & Cadence de Commandes (`CommandRepeater`)](#6-répétition--cadence-de-commandes-commandrepeater)
- [7. Temps de Recharge (Cooldowns)](#7-temps-de-recharge-cooldowns)
- [🧩 Catalogue des Arguments Typés](#-catalogue-des-arguments-typés)
- [🎯 Système d'Événements & Cycle de Vie](#-système-dévénements--cycle-de-vie)
- [Liste des Événements Disponibles](#liste-des-événements-disponibles)
@@ -31,6 +34,11 @@ Une bibliothèque Java moderne, fluide et événementielle permettant de créer,
- **DSL Fluide & Déclaratif** : Déclaration intuitive basée sur le pattern Builder chainable (`BetterMcCommands.builder("ma-commande")`).
- **Zéro `plugin.yml`** : Injection directe par réflexion dans la `CommandMap` du serveur Minecraft avec support du dé-enregistrement à chaud.
- **Validation Métier Directe & Extensible** :
- Validateurs au niveau commande (`.validate(ctx -> condition, "Erreur...")`)
- Validateurs au niveau argument (`argument.validate(val -> condition, "Erreur...")`)
- **Confirmation Préalable (Double validation)** : Sécurisation des commandes critiques (`/disband`, `/delete-data`) avec expiration (`.requireConfirmation(Duration.ofSeconds(15))`).
- **Répétition & Tâches Cadencées (`CommandRepeater`)** : Répétition automatique d'une action à intervalles réguliers avec suivi de progression et annulation.
- **Système d'Arguments Typés & Auto-complétion** :
- Conversion et validation automatiques avec messages d'erreurs clairs.
- Auto-complétion dynamique (Tab-Complete) intelligente selon le type de chaque argument.
@@ -58,10 +66,16 @@ flowchart TD
PermCheck -- Valide --> CoolCheck{"Vérification Cooldown"}
CoolCheck -- Actif --> EvtCool["Dispatch CommandCooldownEvent"]
CoolCheck -- Inactif --> ArgParse{"Parsing des Arguments Typés"}
ArgParse -- Erreur format / manquant --> EvtSyntax["Dispatch CommandSyntaxErrorEvent"]
CoolCheck -- Inactif --> ArgParse{"Parsing & Validation des Arguments"}
ArgParse -- Erreur format / invalide --> EvtSyntax["Dispatch CommandSyntaxErrorEvent"]
ArgParse -- Succès --> EvtPre{"Dispatch CommandPreExecuteEvent"}
ArgParse -- Succès --> ConfCheck{"Confirmation Requise ?"}
ConfCheck -- Non confirmée --> EvtConf["Dispatch ConfirmationRequiredEvent & Invite à retaper"]
ConfCheck -- Confirmée ou Non requise --> ValCheck{"Validateurs (CommandValidator)"}
ValCheck -- Invalide --> ValErr["Envoi Message d'Erreur & Arrêt"]
ValCheck -- Valide --> EvtPre{"Dispatch CommandPreExecuteEvent"}
EvtPre -- Annulé (setCancelled) --> Cancelled["Arrêt de l'exécution"]
EvtPre -- Non annulé --> Exec["Exécution du CommandExecutor métier"]
@@ -220,7 +234,75 @@ BetterMcCommands.builder("broadcast")
---
### 4. Temps de Recharge (Cooldowns)
### 4. Système de Validation Métier (Pré-requis)
Vous pouvez attacher des validateurs à vos commandes et arguments :
```java
// Validation au niveau de la commande
BetterMcCommands.builder("level-reward")
.playerOnly()
.validate(ctx -> ctx.getPlayer().getLevel() >= 10, "Vous devez être niveau 10 minimum !")
.executes(ctx -> {
ctx.replySuccess("Récompense de niveau réclamée !");
})
.register();
// Validation au niveau d'un argument
BetterMcCommands.builder("setprice")
.argument(Arguments.decimal("prix").validate(prix -> prix > 0, "Le prix doit être strictement positif !"))
.executes(ctx -> {
double prix = ctx.getDouble("prix");
ctx.replySuccess("Prix configuré à " + prix + " €.");
})
.register();
```
---
### 5. Confirmation Préalable & Double Validation (`.requireConfirmation`)
Sécurisez les commandes destructrices ou sensibles. Le joueur doit ré-exécuter la commande dans le temps imparti pour confirmer l'action :
```java
BetterMcCommands.builder("disband")
.description("Dissout votre guilde")
.playerOnly()
.requireConfirmation(Duration.ofSeconds(15), "<red><bold>ATTENTION</bold> : Cette action est irréversible ! Retapez <yellow>/disband</yellow> dans les 15s pour confirmer.</red>")
.executes(context -> {
context.replySuccess("Votre guilde a été dissoute.");
})
.register();
```
---
### 6. Répétition & Cadence de Commandes (`CommandRepeater`)
Permet de répéter une action à intervalle régulier (ex: compte à rebours, diffusion répétée, téléportation avec pulsation) :
```java
BetterMcCommands.builder("countdown")
.playerOnly()
.executes(context -> {
// Répète 5 fois toutes les 1 seconde
CommandRepeater.repeat(monPlugin, context, 5, Duration.ofSeconds(1),
progress -> {
int restants = progress.getTotalRuns() - progress.getCurrentRun() + 1;
context.reply("<gold>Lancement dans <yellow>" + restants + "</yellow>...</gold>");
},
() -> {
// Action finale
context.replySuccess("<bold>PARTEZ !</bold>");
}
);
})
.register();
```
---
### 7. Temps de Recharge (Cooldowns)
```java
BetterMcCommands.builder("kit")
@@ -264,6 +346,7 @@ BetterMcCommands.builder("kit")
|---|---|:---:|
| `CommandPreExecuteEvent` | Déclenché juste avant l'exécution métier. Permet d'annuler ou modifier l'action. | ✅ Oui |
| `CommandPostExecuteEvent` | Déclenché après l'exécution. Contient la durée d'exécution et le statut final. | ❌ Non |
| `CommandConfirmationRequiredEvent` | Déclenché lors d'une demande de confirmation préalable pour personnaliser l'invite. | ✅ Oui |
| `CommandPermissionDeniedEvent` | Déclenché si la permission est manquante ou si l'émetteur n'est pas autorisé. | ✅ Oui |
| `CommandSyntaxErrorEvent` | Déclenché lors d'une erreur de syntaxe ou argument manquant. | ✅ Oui |
| `CommandCooldownEvent` | Déclenché lorsqu'un joueur tente d'exécuter une commande sous cooldown actif. | ✅ Oui |
@@ -333,11 +416,22 @@ fr.luc.bettermccommands/
│ ├── ArgumentType.java
│ ├── Arguments.java (Factory d'arguments)
│ └── type/ (String, Integer, Player, Duration, Enum, etc.)
├── validation/ (Moteur de validation de commande et d'argument)
│ ├── CommandValidator.java
│ ├── ArgumentValidator.java
│ └── ValidationResult.java
├── confirmation/ (Système de confirmation / Double validation)
│ ├── ConfirmationManager.java
│ └── PendingConfirmation.java
├── repeat/ (Tâches répétées & cadence de commandes)
│ ├── CommandRepeater.java
│ └── CommandRepeatProgress.java
├── event/ (Bus d'événements & Lifecycle)
│ ├── CommandEvent.java
│ ├── CancellableCommandEvent.java
│ ├── CommandPreExecuteEvent.java
│ ├── CommandPostExecuteEvent.java
│ ├── CommandConfirmationRequiredEvent.java
│ ├── CommandPermissionDeniedEvent.java
│ ├── CommandSyntaxErrorEvent.java
│ ├── CommandCooldownEvent.java
@@ -27,6 +27,7 @@ public class BetterMcCommands {
private final String pluginPrefix;
private final CommandEventManager eventManager;
private final CooldownManager cooldownManager;
private final fr.luc.bettermccommands.confirmation.ConfirmationManager confirmationManager;
private final CommandDispatcher dispatcher;
private final PaperCommandMapInjector injector;
private final Map<String, Command> registeredCommands = new ConcurrentHashMap<>();
@@ -40,6 +41,7 @@ public class BetterMcCommands {
this.pluginPrefix = pluginPrefix != null ? pluginPrefix : "bettermc";
this.eventManager = new CommandEventManager();
this.cooldownManager = new CooldownManager();
this.confirmationManager = new fr.luc.bettermccommands.confirmation.ConfirmationManager();
this.dispatcher = new CommandDispatcher(this);
this.injector = new PaperCommandMapInjector();
@@ -155,6 +157,13 @@ public class BetterMcCommands {
return cooldownManager;
}
/**
* @return Le gestionnaire des confirmations d'exécution.
*/
public fr.luc.bettermccommands.confirmation.ConfirmationManager getConfirmationManager() {
return confirmationManager;
}
/**
* @return Le dispatcher d'exécution.
*/
@@ -24,6 +24,12 @@ public class CommandNode {
protected Duration cooldown = Duration.ZERO;
protected String cooldownBypassPermission;
// Validation et confirmation
protected final List<fr.luc.bettermccommands.validation.CommandValidator> validators = new ArrayList<>();
protected boolean confirmationRequired = false;
protected Duration confirmationTimeout = Duration.ofSeconds(15);
protected String confirmationPrompt;
// É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<>();
@@ -225,6 +231,38 @@ public class CommandNode {
return builder.toString();
}
public void addValidator(fr.luc.bettermccommands.validation.CommandValidator validator) {
this.validators.add(Objects.requireNonNull(validator, "Validator cannot be null"));
}
public List<fr.luc.bettermccommands.validation.CommandValidator> getValidators() {
return Collections.unmodifiableList(validators);
}
public boolean isConfirmationRequired() {
return confirmationRequired;
}
public void setConfirmationRequired(boolean confirmationRequired) {
this.confirmationRequired = confirmationRequired;
}
public Duration getConfirmationTimeout() {
return confirmationTimeout;
}
public void setConfirmationTimeout(Duration confirmationTimeout) {
this.confirmationTimeout = confirmationTimeout;
}
public String getConfirmationPrompt() {
return confirmationPrompt;
}
public void setConfirmationPrompt(String confirmationPrompt) {
this.confirmationPrompt = confirmationPrompt;
}
// Gestion des écouteurs locaux
public List<CommandEventListener<CommandPreExecuteEvent>> getPreExecuteListeners() {
return preExecuteListeners;
@@ -5,6 +5,7 @@ import fr.luc.bettermccommands.api.suggestion.Suggestion;
import fr.luc.bettermccommands.api.suggestion.SuggestionProvider;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.function.Supplier;
@@ -22,6 +23,7 @@ public class CommandArgument<T> {
private Supplier<T> defaultValueSupplier;
private SuggestionProvider customSuggestionProvider;
private boolean greedy;
private final java.util.List<fr.luc.bettermccommands.validation.ArgumentValidator<T>> validators = new java.util.ArrayList<>();
/**
* Crée un argument obligatoire avec son nom et son type.
@@ -173,6 +175,35 @@ public class CommandArgument<T> {
return type.suggest(context, currentInput);
}
/**
* Ajoute un validateur personnalisé pour cet argument.
*
* @param validator Le validateur d'argument.
* @return Cette instance d'argument.
*/
public CommandArgument<T> validate(fr.luc.bettermccommands.validation.ArgumentValidator<T> validator) {
this.validators.add(Objects.requireNonNull(validator, "Validator cannot be null"));
return this;
}
/**
* Ajoute une règle de validation simple basée sur un prédicat.
*
* @param predicate Le prédicat retournant true si la valeur est valide.
* @param errorMessage Le message d'erreur en cas d'échec.
* @return Cette instance d'argument.
*/
public CommandArgument<T> validate(java.util.function.Predicate<T> predicate, String errorMessage) {
return validate(fr.luc.bettermccommands.validation.ArgumentValidator.of(predicate, errorMessage));
}
/**
* @return La liste des validateurs associés à cet argument.
*/
public java.util.List<fr.luc.bettermccommands.validation.ArgumentValidator<T>> getValidators() {
return Collections.unmodifiableList(validators);
}
/**
* Retourne la représentation textuelle de l'argument pour l'aide.
*
@@ -30,6 +30,11 @@ public abstract class AbstractCommandBuilder<B extends AbstractCommandBuilder<B,
protected Duration cooldown = Duration.ZERO;
protected String cooldownBypassPermission;
protected final List<fr.luc.bettermccommands.validation.CommandValidator> validators = new ArrayList<>();
protected boolean confirmationRequired = false;
protected Duration confirmationTimeout = Duration.ofSeconds(15);
protected String confirmationPrompt;
protected final List<CommandEventListener<CommandPreExecuteEvent>> preExecuteListeners = new ArrayList<>();
protected final List<CommandEventListener<CommandPostExecuteEvent>> postExecuteListeners = new ArrayList<>();
protected final List<CommandEventListener<CommandPermissionDeniedEvent>> permissionDeniedListeners = new ArrayList<>();
@@ -249,6 +254,61 @@ public abstract class AbstractCommandBuilder<B extends AbstractCommandBuilder<B,
return (B) this;
}
/**
* Ajoute une règle de validation personnalisée à cette commande.
*
* @param validator Le validateur.
* @return Cette instance du builder.
*/
public B validate(fr.luc.bettermccommands.validation.CommandValidator validator) {
this.validators.add(Objects.requireNonNull(validator, "Validator cannot be null"));
return (B) this;
}
/**
* Ajoute une règle de validation simple basée sur un prédicat.
*
* @param predicate Le test de validation (retourne true si valide).
* @param errorMessage Le message envoyé si le test échoue.
* @return Cette instance du builder.
*/
public B validate(java.util.function.Predicate<CommandContext> predicate, String errorMessage) {
return validate(fr.luc.bettermccommands.validation.CommandValidator.of(predicate, errorMessage));
}
/**
* Exige une confirmation préalable du joueur avant d'exécuter l'action (délai par défaut: 15s).
*
* @return Cette instance du builder.
*/
public B requireConfirmation() {
return requireConfirmation(Duration.ofSeconds(15), null);
}
/**
* Exige une confirmation préalable du joueur avec un délai personnalisé.
*
* @param timeout La durée maximale pour confirmer.
* @return Cette instance du builder.
*/
public B requireConfirmation(Duration timeout) {
return requireConfirmation(timeout, null);
}
/**
* Exige une confirmation préalable du joueur avec un délai et message personnalisés.
*
* @param timeout La durée maximale pour confirmer.
* @param promptMessage Le message de demande de confirmation.
* @return Cette instance du builder.
*/
public B requireConfirmation(Duration timeout, String promptMessage) {
this.confirmationRequired = true;
this.confirmationTimeout = timeout != null ? timeout : Duration.ofSeconds(15);
this.confirmationPrompt = promptMessage;
return (B) this;
}
/**
* Construit et configure l'instance du nœud de commande.
*
@@ -271,6 +331,13 @@ public abstract class AbstractCommandBuilder<B extends AbstractCommandBuilder<B,
node.setCooldown(this.cooldown);
node.setCooldownBypassPermission(this.cooldownBypassPermission);
node.setConfirmationRequired(this.confirmationRequired);
node.setConfirmationTimeout(this.confirmationTimeout);
node.setConfirmationPrompt(this.confirmationPrompt);
for (fr.luc.bettermccommands.validation.CommandValidator validator : this.validators) {
node.addValidator(validator);
}
node.getPreExecuteListeners().addAll(this.preExecuteListeners);
node.getPostExecuteListeners().addAll(this.postExecuteListeners);
node.getPermissionDeniedListeners().addAll(this.permissionDeniedListeners);
@@ -0,0 +1,95 @@
package fr.luc.bettermccommands.confirmation;
import fr.luc.bettermccommands.api.CommandContext;
import fr.luc.bettermccommands.api.CommandNode;
import org.bukkit.entity.Player;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Gestionnaire des confirmations et validations préalables à l'exécution de commandes sensibles.
*/
public class ConfirmationManager {
private final Map<UUID, PendingConfirmation> pendingConfirmations = new ConcurrentHashMap<>();
/**
* Enregistre une demande de confirmation pour un joueur.
*
* @param player Le joueur devant confirmer.
* @param node Le nœud de commande concerné.
* @param context Le contexte de la commande.
* @param timeout Le délai maximum accordé pour confirmer.
*/
public void requestConfirmation(Player player, CommandNode node, CommandContext context, Duration timeout) {
if (player == null || node == null) return;
Instant expiresAt = Instant.now().plus(timeout != null ? timeout : Duration.ofSeconds(15));
pendingConfirmations.put(player.getUniqueId(), new PendingConfirmation(node, context, expiresAt));
}
/**
* Récupère la confirmation en attente pour un joueur s'il y en a une valide et non expirée.
*
* @param player Le joueur.
* @return Un {@link Optional} contenant la confirmation valide.
*/
public Optional<PendingConfirmation> getPendingConfirmation(Player player) {
if (player == null) return Optional.empty();
PendingConfirmation pending = pendingConfirmations.get(player.getUniqueId());
if (pending == null) {
return Optional.empty();
}
if (pending.isExpired()) {
pendingConfirmations.remove(player.getUniqueId());
return Optional.empty();
}
return Optional.of(pending);
}
/**
* Valide et consomme la confirmation en attente pour un joueur.
*
* @param player Le joueur confirmant l'action.
* @return La {@link PendingConfirmation} validée, ou {@link Optional#empty()} si aucune confirmation active.
*/
public Optional<PendingConfirmation> consumeConfirmation(Player player) {
if (player == null) return Optional.empty();
PendingConfirmation pending = pendingConfirmations.remove(player.getUniqueId());
if (pending == null || pending.isExpired()) {
return Optional.empty();
}
return Optional.of(pending);
}
/**
* Annule la confirmation en attente pour un joueur.
*
* @param player Le joueur.
*/
public void cancelConfirmation(Player player) {
if (player != null) {
pendingConfirmations.remove(player.getUniqueId());
}
}
/**
* Nettoie toutes les confirmations expirées en mémoire.
*/
public void cleanupExpired() {
pendingConfirmations.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
/**
* Supprime toutes les confirmations en mémoire.
*/
public void clearAll() {
pendingConfirmations.clear();
}
}
@@ -0,0 +1,70 @@
package fr.luc.bettermccommands.confirmation;
import fr.luc.bettermccommands.api.CommandContext;
import fr.luc.bettermccommands.api.CommandNode;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
/**
* Représente une demande de confirmation en attente d'approbation par un joueur.
*/
public class PendingConfirmation {
private final CommandNode node;
private final CommandContext context;
private final Instant expiresAt;
/**
* Crée une nouvelle confirmation en attente.
*
* @param node Le nœud de commande en attente.
* @param context Le contexte de commande capturé.
* @param expiresAt La date/heure d'expiration.
*/
public PendingConfirmation(CommandNode node, CommandContext context, Instant expiresAt) {
this.node = Objects.requireNonNull(node, "node cannot be null");
this.context = Objects.requireNonNull(context, "context cannot be null");
this.expiresAt = Objects.requireNonNull(expiresAt, "expiresAt cannot be null");
}
/**
* @return Le nœud de commande concerné.
*/
public CommandNode getNode() {
return node;
}
/**
* @return Le contexte d'exécution capturé lors de l'appel initial.
*/
public CommandContext getContext() {
return context;
}
/**
* @return La date/heure d'expiration de la confirmation.
*/
public Instant getExpiresAt() {
return expiresAt;
}
/**
* @return true si le délai de confirmation est dépassé, sinon false.
*/
public boolean isExpired() {
return Instant.now().isAfter(expiresAt);
}
/**
* @return La durée restante avant expiration.
*/
public Duration getRemaining() {
Instant now = Instant.now();
if (now.isAfter(expiresAt)) {
return Duration.ZERO;
}
return Duration.between(now, expiresAt);
}
}
@@ -131,6 +131,27 @@ public class DemoBaseCommand {
)
)
// 6. Sous-commande avec confirmation préalable (Double validation) : /demo delete-data
.subcommand(BetterMcCommands.subBuilder("delete-data")
.description("Action sensible nécessitant une confirmation")
.permission("bettermc.demo.admin")
.playerOnly()
.requireConfirmation(Duration.ofSeconds(15), "<red><bold>ATTENTION</bold> : Cette action est irréversible ! Retapez <yellow>/demo delete-data</yellow> dans les 15s pour confirmer la suppression.</red>")
.executes(context -> {
context.replySuccess("Données supprimées avec succès après confirmation !");
})
)
// 7. Sous-commande avec validateur de pré-requis : /demo level-reward
.subcommand(BetterMcCommands.subBuilder("level-reward")
.description("Récompense réservée aux joueurs de niveau 5 ou plus")
.playerOnly()
.validate(ctx -> ctx.getPlayer().getLevel() >= 5, "Vous devez posséder au minimum le niveau d'expérience 5 pour réclamer cette récompense !")
.executes(context -> {
context.replySuccess("Félicitations ! Vous avez réclamé votre récompense de niveau.");
})
)
.build();
}
}
@@ -0,0 +1,53 @@
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'une commande nécessite une confirmation avant d'être exécutée.
* Permet de personnaliser le message de confirmation ou d'annuler la demande de confirmation.
*/
public class CommandConfirmationRequiredEvent extends CancellableCommandEvent {
private final Duration timeout;
private String customPromptMessage;
/**
* Crée l'événement de demande de confirmation.
*
* @param node Le nœud de commande concerné.
* @param sender L'émetteur (joueur).
* @param context Le contexte de commande capturé.
* @param timeout Le délai imparti pour confirmer.
*/
public CommandConfirmationRequiredEvent(CommandNode node, CommandSender sender, CommandContext context, Duration timeout) {
super(node, sender, context);
this.timeout = timeout;
}
/**
* @return La durée impartie pour confirmer l'action.
*/
public Duration getTimeout() {
return timeout;
}
/**
* @return Le message personnalisé de confirmation, ou {@code null} pour le message par défaut.
*/
public String getCustomPromptMessage() {
return customPromptMessage;
}
/**
* Définit un message de confirmation personnalisé.
*
* @param customPromptMessage Le message au format MiniMessage ou couleurs Minecraft.
*/
public void setCustomPromptMessage(String customPromptMessage) {
this.customPromptMessage = customPromptMessage;
}
}
@@ -184,6 +184,44 @@ public class CommandDispatcher {
// 6. Contexte final
CommandContext fullContext = new CommandContext(sender, label, args, parsedArguments);
// 6.5. Vérification de la confirmation requise (Double validation)
if (targetNode.isConfirmationRequired() && sender instanceof Player player) {
Optional<fr.luc.bettermccommands.confirmation.PendingConfirmation> pendingOpt = manager.getConfirmationManager().getPendingConfirmation(player);
if (pendingOpt.isPresent() && pendingOpt.get().getNode().getFullName().equalsIgnoreCase(targetNode.getFullName())) {
// Confirmation reçue et validée !
manager.getConfirmationManager().consumeConfirmation(player);
} else {
// Enregistrement de la confirmation en attente
manager.getConfirmationManager().requestConfirmation(player, targetNode, fullContext, targetNode.getConfirmationTimeout());
CommandConfirmationRequiredEvent confEvent = new CommandConfirmationRequiredEvent(
targetNode, player, fullContext, targetNode.getConfirmationTimeout());
dispatchEvent(targetNode, confEvent);
if (!confEvent.isCancelled()) {
if (confEvent.getCustomPromptMessage() != null) {
confEvent.reply(confEvent.getCustomPromptMessage());
} else if (targetNode.getConfirmationPrompt() != null) {
fullContext.reply(targetNode.getConfirmationPrompt());
} else {
long secs = targetNode.getConfirmationTimeout().toSeconds();
fullContext.reply("<gold>⚠ <bold>Confirmation requise</bold> : </gold>" +
"<yellow>Retapez la commande <aqua>/" + label + (args.length > 0 ? " " + String.join(" ", args) : "") +
"</aqua> dans les <gold>" + secs + "s</gold> pour valider l'exécution.</yellow>");
}
}
return CommandResult.CANCELLED;
}
}
// 6.6. Exécution des validateurs de commande (CommandValidator)
for (fr.luc.bettermccommands.validation.CommandValidator validator : targetNode.getValidators()) {
fr.luc.bettermccommands.validation.ValidationResult valRes = validator.validate(fullContext);
if (!valRes.isValid()) {
fullContext.replyError(valRes.getErrorMessage());
return CommandResult.FAILED;
}
}
// 7. Événement PreExecute
CommandPreExecuteEvent preEvent = new CommandPreExecuteEvent(targetNode, sender, fullContext);
dispatchEvent(targetNode, preEvent);
@@ -286,7 +324,14 @@ public class CommandDispatcher {
@SuppressWarnings("unchecked")
private <T> T parseArgumentValue(CommandArgument<T> arg, String input, CommandContext context) throws CommandArgumentParseException {
return arg.getType().parse(arg.getName(), input, context);
T parsed = arg.getType().parse(arg.getName(), input, context);
for (fr.luc.bettermccommands.validation.ArgumentValidator<T> validator : arg.getValidators()) {
fr.luc.bettermccommands.validation.ValidationResult result = validator.validate(parsed, context);
if (!result.isValid()) {
throw new CommandArgumentParseException(arg.getName(), input, arg.getType().getTypeName(), result.getErrorMessage());
}
}
return parsed;
}
private CommandResult handleSyntaxError(CommandNode node, CommandSender sender, CommandContext context,
@@ -0,0 +1,69 @@
package fr.luc.bettermccommands.repeat;
import fr.luc.bettermccommands.api.CommandContext;
/**
* Informations de progression d'une exécution de commande répétée.
*/
public class CommandRepeatProgress {
private final CommandContext context;
private final int currentRun;
private final int totalRuns;
private boolean cancelled = false;
/**
* Crée un objet de suivi d'exécution de commande répétée.
*
* @param context Le contexte de la commande.
* @param currentRun Le numéro de l'itération actuelle (1-indexed).
* @param totalRuns Le nombre total d'itérations prévues.
*/
public CommandRepeatProgress(CommandContext context, int currentRun, int totalRuns) {
this.context = context;
this.currentRun = currentRun;
this.totalRuns = totalRuns;
}
/**
* @return Le contexte d'exécution de la commande.
*/
public CommandContext getContext() {
return context;
}
/**
* @return Le numéro de l'itération en cours (ex: 1 pour la première).
*/
public int getCurrentRun() {
return currentRun;
}
/**
* @return Le nombre total d'exécutions programmées.
*/
public int getTotalRuns() {
return totalRuns;
}
/**
* @return true s'il s'agit de la dernière itération de la boucle.
*/
public boolean isLastRun() {
return currentRun >= totalRuns;
}
/**
* @return true si la répétition a été interrompue.
*/
public boolean isCancelled() {
return cancelled;
}
/**
* Interrompt immédiatement les itérations restantes de la boucle.
*/
public void cancel() {
this.cancelled = true;
}
}
@@ -0,0 +1,83 @@
package fr.luc.bettermccommands.repeat;
import fr.luc.bettermccommands.api.CommandContext;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
/**
* Utilitaire permettant d'exécuter une commande ou une action associée de manière répétée / cadencée dans le temps.
*/
public class CommandRepeater {
/**
* Répète une action liée au contexte de commande à intervalle régulier.
*
* @param plugin Le plugin Bukkit gérant les tâches.
* @param context Le contexte de la commande.
* @param totalRuns Le nombre total de répétitions.
* @param interval L'intervalle de temps entre chaque exécution.
* @param onRun L'action à exécuter à chaque itération.
* @return L'instance de {@link BukkitTask} créée.
*/
public static BukkitTask repeat(Plugin plugin, CommandContext context, int totalRuns, Duration interval,
Consumer<CommandRepeatProgress> onRun) {
return repeat(plugin, context, totalRuns, interval, onRun, null);
}
/**
* Répète une action avec rappel final à l'issue de toutes les répétitions.
*
* @param plugin Le plugin Bukkit gérant les tâches.
* @param context Le contexte de la commande.
* @param totalRuns Le nombre total d'exécutions (ex: 5).
* @param interval La durée entre deux itérations.
* @param onRun L'action exécutée à chaque itération.
* @param onComplete L'action finale appelée lorsque toutes les répétitions sont terminées (ou null).
* @return L'instance de {@link BukkitTask} correspondante.
*/
public static BukkitTask repeat(Plugin plugin, CommandContext context, int totalRuns, Duration interval,
Consumer<CommandRepeatProgress> onRun, Runnable onComplete) {
Objects.requireNonNull(plugin, "plugin cannot be null");
Objects.requireNonNull(context, "context cannot be null");
Objects.requireNonNull(onRun, "onRun cannot be null");
long ticks = Math.max(1, (interval != null ? interval.toMillis() : 1000) / 50);
AtomicInteger runCount = new AtomicInteger(0);
final BukkitTask[] taskHolder = new BukkitTask[1];
taskHolder[0] = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
int current = runCount.incrementAndGet();
CommandRepeatProgress progress = new CommandRepeatProgress(context, current, totalRuns);
try {
onRun.accept(progress);
} catch (Exception e) {
context.replyError("Erreur lors de l'exécution répétée : " + e.getMessage());
e.printStackTrace();
progress.cancel();
}
if (progress.isCancelled() || current >= totalRuns) {
if (taskHolder[0] != null) {
taskHolder[0].cancel();
}
if (!progress.isCancelled() && onComplete != null) {
try {
onComplete.run();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}, 0L, ticks);
return taskHolder[0];
}
}
@@ -0,0 +1,40 @@
package fr.luc.bettermccommands.validation;
import fr.luc.bettermccommands.api.CommandContext;
import java.util.function.Predicate;
/**
* Validateur spécifique pour la valeur d'un argument typé.
*
* @param <T> Le type de l'argument.
*/
@FunctionalInterface
public interface ArgumentValidator<T> {
/**
* Valide la valeur de l'argument analysé.
*
* @param value La valeur convertie de l'argument.
* @param context Le contexte d'exécution.
* @return Le résultat de validation.
*/
ValidationResult validate(T value, CommandContext context);
/**
* Crée un validateur d'argument simple à partir d'un prédicat.
*
* @param predicate Le prédicat testant la valeur.
* @param errorMessage Le message d'erreur si le test échoue.
* @param <T> Le type d'argument.
* @return Le validateur configuré.
*/
static <T> ArgumentValidator<T> of(Predicate<T> predicate, String errorMessage) {
return (value, context) -> {
if (predicate.test(value)) {
return ValidationResult.valid();
}
return ValidationResult.invalid(errorMessage);
};
}
}
@@ -0,0 +1,36 @@
package fr.luc.bettermccommands.validation;
import fr.luc.bettermccommands.api.CommandContext;
import java.util.function.Predicate;
/**
* Validateur fonctionnel permettant de vérifier des conditions métier avant l'exécution d'une commande.
*/
@FunctionalInterface
public interface CommandValidator {
/**
* Valide le contexte d'exécution de la commande.
*
* @param context Le contexte complet de la commande.
* @return Le {@link ValidationResult} indiquant le succès ou l'échec avec message.
*/
ValidationResult validate(CommandContext context);
/**
* Crée un validateur à partir d'un prédicat simple et d'un message d'erreur.
*
* @param predicate Le prédicat retournant true si la condition est remplie.
* @param errorMessage Le message d'erreur envoyé en cas d'échec.
* @return L'instance de {@link CommandValidator}.
*/
static CommandValidator of(Predicate<CommandContext> predicate, String errorMessage) {
return context -> {
if (predicate.test(context)) {
return ValidationResult.valid();
}
return ValidationResult.invalid(errorMessage);
};
}
}
@@ -0,0 +1,50 @@
package fr.luc.bettermccommands.validation;
import java.util.Objects;
/**
* Représente le résultat d'une validation de commande ou d'argument.
*/
public class ValidationResult {
private static final ValidationResult VALID = new ValidationResult(true, null);
private final boolean valid;
private final String errorMessage;
private ValidationResult(boolean valid, String errorMessage) {
this.valid = valid;
this.errorMessage = errorMessage;
}
/**
* @return Un résultat indiquant que la validation a réussi.
*/
public static ValidationResult valid() {
return VALID;
}
/**
* Crée un résultat d'échec de validation avec un message explicatif.
*
* @param errorMessage Le message d'erreur formaté (MiniMessage ou texte).
* @return Le résultat d'échec.
*/
public static ValidationResult invalid(String errorMessage) {
return new ValidationResult(false, Objects.requireNonNull(errorMessage, "errorMessage cannot be null"));
}
/**
* @return true si la validation est passée avec succès, sinon false.
*/
public boolean isValid() {
return valid;
}
/**
* @return Le message d'erreur en cas d'échec, ou {@code null} si valide.
*/
public String getErrorMessage() {
return errorMessage;
}
}
@@ -0,0 +1,111 @@
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.validation.ValidationResult;
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 static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
public class CommandValidationAndConfirmationTest {
private BetterMcCommands manager;
@BeforeEach
void setUp() {
manager = new BetterMcCommands("test");
}
@Test
@DisplayName("La validation de commande (CommandValidator) doit bloquer l'exécution si invalide")
void testCommandValidation() {
AtomicBoolean executed = new AtomicBoolean(false);
AtomicBoolean allowExecution = new AtomicBoolean(false);
Command command = BetterMcCommands.builder("trade")
.validate(ctx -> {
if (!allowExecution.get()) {
return ValidationResult.invalid("Échange temporairement indisponible.");
}
return ValidationResult.valid();
})
.executes(ctx -> executed.set(true))
.build();
Player player = Mockito.mock(Player.class);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
// 1. Validation échoue
CommandResult res1 = manager.getDispatcher().execute(command, player, "trade", new String[0]);
assertEquals(CommandResult.FAILED, res1);
assertFalse(executed.get());
// 2. Validation réussit
allowExecution.set(true);
CommandResult res2 = manager.getDispatcher().execute(command, player, "trade", new String[0]);
assertEquals(CommandResult.SUCCESS, res2);
assertTrue(executed.get());
}
@Test
@DisplayName("La validation d'argument (ArgumentValidator) doit rejeter les entrées non conformes")
void testArgumentValidation() {
AtomicBoolean executed = new AtomicBoolean(false);
Command command = BetterMcCommands.builder("rename")
.argument(Arguments.string("nom")
.validate(nom -> !nom.equalsIgnoreCase("root"), "Le nom 'root' est interdit."))
.executes(ctx -> executed.set(true))
.build();
Player player = Mockito.mock(Player.class);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
// Nom interdit "root" -> Erreur
CommandResult res1 = manager.getDispatcher().execute(command, player, "rename", new String[]{"root"});
assertEquals(CommandResult.SYNTAX_ERROR, res1);
assertFalse(executed.get());
// Nom valide "Guerrier" -> Succès
CommandResult res2 = manager.getDispatcher().execute(command, player, "rename", new String[]{"Guerrier"});
assertEquals(CommandResult.SUCCESS, res2);
assertTrue(executed.get());
}
@Test
@DisplayName("Le système de confirmation doit demander confirmation puis exécuter à la seconde tentative")
void testConfirmationWorkflow() {
AtomicBoolean executed = new AtomicBoolean(false);
Command command = BetterMcCommands.builder("disband")
.requireConfirmation(Duration.ofSeconds(10))
.executes(ctx -> executed.set(true))
.build();
Player player = Mockito.mock(Player.class);
UUID uuid = UUID.randomUUID();
when(player.getUniqueId()).thenReturn(uuid);
// 1ère tentative : Enregistre la confirmation et bloque l'exécution
CommandResult res1 = manager.getDispatcher().execute(command, player, "disband", new String[0]);
assertEquals(CommandResult.CANCELLED, res1);
assertFalse(executed.get());
assertTrue(manager.getConfirmationManager().getPendingConfirmation(player).isPresent());
// 2ème tentative immédiate : Valide et exécute
CommandResult res2 = manager.getDispatcher().execute(command, player, "disband", new String[0]);
assertEquals(CommandResult.SUCCESS, res2);
assertTrue(executed.get());
assertFalse(manager.getConfirmationManager().getPendingConfirmation(player).isPresent());
}
}