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:
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user