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:
2026-08-20 17:26:36 +02:00
parent c3aa06ab3b
commit 637aa7b3ec
59 changed files with 5252 additions and 1 deletions
@@ -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);
}
}