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:
@@ -0,0 +1,185 @@
|
||||
package fr.luc.bettermccommands;
|
||||
|
||||
import fr.luc.bettermccommands.api.Command;
|
||||
import fr.luc.bettermccommands.builder.CommandBuilder;
|
||||
import fr.luc.bettermccommands.builder.SubCommandBuilder;
|
||||
import fr.luc.bettermccommands.cooldown.CooldownManager;
|
||||
import fr.luc.bettermccommands.event.CommandEvent;
|
||||
import fr.luc.bettermccommands.event.CommandEventListener;
|
||||
import fr.luc.bettermccommands.event.CommandEventManager;
|
||||
import fr.luc.bettermccommands.platform.CommandDispatcher;
|
||||
import fr.luc.bettermccommands.platform.paper.PaperCommandMapInjector;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Point d'entrée principal et gestionnaire central de la bibliothèque **betterMcCommands**.
|
||||
* Fournit l'accès aux constructeurs fluides, au bus d'événements, aux cooldowns et à l'injection dynamique.
|
||||
*/
|
||||
public class BetterMcCommands {
|
||||
|
||||
private static BetterMcCommands instance;
|
||||
|
||||
private final String pluginPrefix;
|
||||
private final CommandEventManager eventManager;
|
||||
private final CooldownManager cooldownManager;
|
||||
private final CommandDispatcher dispatcher;
|
||||
private final PaperCommandMapInjector injector;
|
||||
private final Map<String, Command> registeredCommands = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Initialise une instance de betterMcCommands avec un préfixe personnalisé.
|
||||
*
|
||||
* @param pluginPrefix Le préfixe utilisé lors de l'enregistrement (ex: nom du plugin).
|
||||
*/
|
||||
public BetterMcCommands(String pluginPrefix) {
|
||||
this.pluginPrefix = pluginPrefix != null ? pluginPrefix : "bettermc";
|
||||
this.eventManager = new CommandEventManager();
|
||||
this.cooldownManager = new CooldownManager();
|
||||
this.dispatcher = new CommandDispatcher(this);
|
||||
this.injector = new PaperCommandMapInjector();
|
||||
|
||||
if (instance == null) {
|
||||
instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise betterMcCommands pour un plugin Bukkit/Paper donné.
|
||||
*
|
||||
* @param plugin Le plugin propriétaire.
|
||||
* @return L'instance configurée.
|
||||
*/
|
||||
public static BetterMcCommands create(Plugin plugin) {
|
||||
return new BetterMcCommands(plugin != null ? plugin.getName().toLowerCase() : "bettermc");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'instance singleton par défaut de betterMcCommands.
|
||||
*/
|
||||
public static synchronized BetterMcCommands getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new BetterMcCommands("bettermc");
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un nouveau constructeur de commande racine.
|
||||
*
|
||||
* @param name Le nom de la commande (ex: "commande-demo").
|
||||
* @return Le {@link CommandBuilder} configuré.
|
||||
*/
|
||||
public static CommandBuilder builder(String name) {
|
||||
return new CommandBuilder(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un nouveau constructeur de sous-commande.
|
||||
*
|
||||
* @param name Le nom de la sous-commande (ex: "give").
|
||||
* @return Le {@link SubCommandBuilder} configuré.
|
||||
*/
|
||||
public static SubCommandBuilder subBuilder(String name) {
|
||||
return new SubCommandBuilder(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une commande construite auprès du serveur et du gestionnaire.
|
||||
*
|
||||
* @param command La commande racine.
|
||||
*/
|
||||
public void registerCommand(Command command) {
|
||||
command.setManager(this);
|
||||
registeredCommands.put(command.getName().toLowerCase(), command);
|
||||
injector.register(command, dispatcher, pluginPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Désenregistre une commande racine du serveur à chaud.
|
||||
*
|
||||
* @param command La commande à retirer.
|
||||
*/
|
||||
public void unregisterCommand(Command command) {
|
||||
registeredCommands.remove(command.getName().toLowerCase());
|
||||
injector.unregister(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Désenregistre l'ensemble des commandes gérées par cette instance.
|
||||
*/
|
||||
public void unregisterAll() {
|
||||
for (Command cmd : registeredCommands.values()) {
|
||||
injector.unregister(cmd);
|
||||
}
|
||||
registeredCommands.clear();
|
||||
cooldownManager.clearAll();
|
||||
eventManager.unregisterAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une classe d'écouteurs d'événements annotée avec {@link fr.luc.bettermccommands.event.annotation.CommandEventHandler}.
|
||||
*
|
||||
* @param listenerInstance L'instance contenant les méthodes d'écoute.
|
||||
*/
|
||||
public void registerListeners(Object listenerInstance) {
|
||||
eventManager.registerListeners(listenerInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre un écouteur fonctionnel pour un type d'événement donné.
|
||||
*
|
||||
* @param eventType Le type d'événement à écouter.
|
||||
* @param listener Le callback d'exécution.
|
||||
* @param <T> Le type de l'événement.
|
||||
*/
|
||||
public <T extends CommandEvent> void on(Class<T> eventType, CommandEventListener<T> listener) {
|
||||
eventManager.register(eventType, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le bus d'événements de commandes.
|
||||
*/
|
||||
public CommandEventManager getEventManager() {
|
||||
return eventManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le gestionnaire de temps de recharge (cooldowns).
|
||||
*/
|
||||
public CooldownManager getCooldownManager() {
|
||||
return cooldownManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le dispatcher d'exécution.
|
||||
*/
|
||||
public CommandDispatcher getDispatcher() {
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'injecteur CommandMap.
|
||||
*/
|
||||
public PaperCommandMapInjector getInjector() {
|
||||
return injector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La collection des commandes racines enregistrées.
|
||||
*/
|
||||
public Collection<Command> getRegisteredCommands() {
|
||||
return Collections.unmodifiableCollection(registeredCommands.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le préfixe de namespace de cette instance.
|
||||
*/
|
||||
public String getPluginPrefix() {
|
||||
return pluginPrefix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package fr.luc.bettermccommands.api;
|
||||
|
||||
import fr.luc.bettermccommands.BetterMcCommands;
|
||||
|
||||
/**
|
||||
* Représente une commande racine complète enregistrée auprès du serveur Minecraft.
|
||||
*/
|
||||
public class Command extends CommandNode {
|
||||
|
||||
private boolean registered = false;
|
||||
private BetterMcCommands manager;
|
||||
|
||||
/**
|
||||
* Crée une commande racine avec son nom principal.
|
||||
*
|
||||
* @param name Le nom de la commande (sans le slash initial).
|
||||
*/
|
||||
public Command(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si cette commande est actuellement enregistrée sur le serveur, sinon false.
|
||||
*/
|
||||
public boolean isRegistered() {
|
||||
return registered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit l'état d'enregistrement de la commande.
|
||||
*
|
||||
* @param registered L'état d'enregistrement.
|
||||
*/
|
||||
public void setRegistered(boolean registered) {
|
||||
this.registered = registered;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'instance du gestionnaire {@link BetterMcCommands} gérant cette commande.
|
||||
*/
|
||||
public BetterMcCommands getManager() {
|
||||
return manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associe cette commande à son gestionnaire principal.
|
||||
*
|
||||
* @param manager L'instance de {@link BetterMcCommands}.
|
||||
*/
|
||||
public void setManager(BetterMcCommands manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre cette commande auprès de la plateforme Minecraft via son gestionnaire.
|
||||
*/
|
||||
public void register() {
|
||||
if (manager != null) {
|
||||
manager.registerCommand(this);
|
||||
} else {
|
||||
BetterMcCommands.getInstance().registerCommand(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Désenregistre cette commande du serveur Minecraft à chaud.
|
||||
*/
|
||||
public void unregister() {
|
||||
if (manager != null) {
|
||||
manager.unregisterCommand(this);
|
||||
} else {
|
||||
BetterMcCommands.getInstance().unregisterCommand(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package fr.luc.bettermccommands.api;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Contexte complet d'exécution d'une commande.
|
||||
* Contient l'émetteur, les arguments analysés et typés, les métadonnées et des utilitaires de réponse.
|
||||
*/
|
||||
public class CommandContext {
|
||||
|
||||
private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage();
|
||||
|
||||
private final CommandSender sender;
|
||||
private final String label;
|
||||
private final String[] rawArgs;
|
||||
private final Map<String, Object> arguments;
|
||||
private final Map<String, Object> metadata;
|
||||
|
||||
/**
|
||||
* Construit un nouveau contexte de commande.
|
||||
*
|
||||
* @param sender L'émetteur de la commande.
|
||||
* @param label Le nom ou alias utilisé pour invoquer la commande.
|
||||
* @param rawArgs Les arguments bruts sous forme de tableau de chaînes.
|
||||
* @param arguments La table des arguments résolus et typés.
|
||||
*/
|
||||
public CommandContext(CommandSender sender, String label, String[] rawArgs, Map<String, Object> arguments) {
|
||||
this.sender = Objects.requireNonNull(sender, "sender cannot be null");
|
||||
this.label = label != null ? label : "";
|
||||
this.rawArgs = rawArgs != null ? rawArgs : new String[0];
|
||||
this.arguments = arguments != null ? new HashMap<>(arguments) : new HashMap<>();
|
||||
this.metadata = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'émetteur de la commande (Player, ConsoleCommandSender, etc.).
|
||||
*/
|
||||
public CommandSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'émetteur est un {@link Player}, sinon false.
|
||||
*/
|
||||
public boolean isPlayer() {
|
||||
return sender instanceof Player;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'émetteur est la console du serveur.
|
||||
*/
|
||||
public boolean isConsole() {
|
||||
return sender instanceof ConsoleCommandSender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le joueur émetteur de la commande.
|
||||
*
|
||||
* @return L'instance de {@link Player}.
|
||||
* @throws IllegalStateException si l'émetteur n'est pas un joueur.
|
||||
*/
|
||||
public Player getPlayer() {
|
||||
if (!isPlayer()) {
|
||||
throw new IllegalStateException("CommandSender is not a Player: " + sender.getClass().getSimpleName());
|
||||
}
|
||||
return (Player) sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la console émettrice de la commande.
|
||||
*
|
||||
* @return L'instance de {@link ConsoleCommandSender}.
|
||||
* @throws IllegalStateException si l'émetteur n'est pas la console.
|
||||
*/
|
||||
public ConsoleCommandSender getConsole() {
|
||||
if (!isConsole()) {
|
||||
throw new IllegalStateException("CommandSender is not a ConsoleCommandSender: " + sender.getClass().getSimpleName());
|
||||
}
|
||||
return (ConsoleCommandSender) sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le label ou alias utilisé lors de l'exécution.
|
||||
*/
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le tableau des arguments bruts passés lors de l'appel.
|
||||
*/
|
||||
public String[] getRawArgs() {
|
||||
return rawArgs.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Une vue non modifiable des arguments analysés.
|
||||
*/
|
||||
public Map<String, Object> getArguments() {
|
||||
return Collections.unmodifiableMap(arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un argument du nom donné a été renseigné et analysé.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return true si l'argument est présent, sinon false.
|
||||
*/
|
||||
public boolean hasArgument(String name) {
|
||||
return arguments.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un argument typé par son nom.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @param type La classe attendue pour l'argument.
|
||||
* @param <T> Le type générique.
|
||||
* @return L'instance de l'argument analysé.
|
||||
* @throws IllegalArgumentException si l'argument est manquant ou n'est pas du type attendu.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T get(String name, Class<T> type) {
|
||||
Object val = arguments.get(name);
|
||||
if (val == null) {
|
||||
throw new IllegalArgumentException("Argument '" + name + "' is missing from context.");
|
||||
}
|
||||
if (!type.isInstance(val)) {
|
||||
throw new IllegalArgumentException("Argument '" + name + "' is of type " +
|
||||
val.getClass().getSimpleName() + ", expected " + type.getSimpleName());
|
||||
}
|
||||
return (T) val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un argument de manière optionnelle.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @param type La classe attendue pour l'argument.
|
||||
* @param <T> Le type générique.
|
||||
* @return Un {@link Optional} contenant la valeur si présente.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> getOptional(String name, Class<T> type) {
|
||||
Object val = arguments.get(name);
|
||||
if (val != null && type.isInstance(val)) {
|
||||
return Optional.of((T) val);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un argument sous forme de chaîne de caractères.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return La chaîne analysée.
|
||||
*/
|
||||
public String getString(String name) {
|
||||
return get(name, String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un argument entier.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return L'entier analysé.
|
||||
*/
|
||||
public int getInt(String name) {
|
||||
return get(name, Integer.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un argument double.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le double analysé.
|
||||
*/
|
||||
public double getDouble(String name) {
|
||||
return get(name, Double.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un argument booléen.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le booléen analysé.
|
||||
*/
|
||||
public boolean getBoolean(String name) {
|
||||
return get(name, Boolean.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un joueur cible ciblé par l'argument.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le joueur cible.
|
||||
*/
|
||||
public Player getTargetPlayer(String name) {
|
||||
return get(name, Player.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un monde ciblé par l'argument.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le monde cible.
|
||||
*/
|
||||
public World getWorld(String name) {
|
||||
return get(name, World.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une durée ciblée par l'argument.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return La durée analysée.
|
||||
*/
|
||||
public Duration getDuration(String name) {
|
||||
return get(name, Duration.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit une métadonnée personnalisée dans le contexte d'exécution.
|
||||
*
|
||||
* @param key La clé de la métadonnée.
|
||||
* @param value La valeur associée.
|
||||
*/
|
||||
public void setMetadata(String key, Object value) {
|
||||
this.metadata.put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une métadonnée du contexte.
|
||||
*
|
||||
* @param key La clé.
|
||||
* @param type Le type attendu.
|
||||
* @param <T> Le type générique.
|
||||
* @return Un {@link Optional} contenant la métadonnée si présente.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> getMetadata(String key, Class<T> type) {
|
||||
Object val = metadata.get(key);
|
||||
if (val != null && type.isInstance(val)) {
|
||||
return Optional.of((T) val);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un message formaté avec MiniMessage à l'émetteur de la commande.
|
||||
*
|
||||
* @param miniMessageText Le texte formaté avec tags MiniMessage (ex: "<green>Succès !</green>").
|
||||
*/
|
||||
public void reply(String miniMessageText) {
|
||||
if (miniMessageText == null || miniMessageText.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (sender instanceof Audience) {
|
||||
((Audience) sender).sendMessage(MINI_MESSAGE.deserialize(miniMessageText));
|
||||
} else {
|
||||
sender.sendMessage(ChatColor.translateAlternateColorCodes('&', miniMessageText));
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
sender.sendMessage(miniMessageText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un composant Kyori Adventure directement à l'émetteur.
|
||||
*
|
||||
* @param component Le composant texte Adventure.
|
||||
*/
|
||||
public void reply(Component component) {
|
||||
if (component == null) {
|
||||
return;
|
||||
}
|
||||
if (sender instanceof Audience) {
|
||||
((Audience) sender).sendMessage(component);
|
||||
} else {
|
||||
sender.sendMessage(component.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un message de succès (préfixé en vert).
|
||||
*
|
||||
* @param message Le message de succès.
|
||||
*/
|
||||
public void replySuccess(String message) {
|
||||
reply("<dark_gray>[<green>✔</green>]</dark_gray> <green>" + message + "</green>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un message d'erreur (préfixé en rouge).
|
||||
*
|
||||
* @param message Le message d'erreur.
|
||||
*/
|
||||
public void replyError(String message) {
|
||||
reply("<dark_gray>[<red>✖</red>]</dark_gray> <red>" + message + "</red>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie un message informatif (préfixé en bleu/aqua).
|
||||
*
|
||||
* @param message Le message d'information.
|
||||
*/
|
||||
public void replyInfo(String message) {
|
||||
reply("<dark_gray>[<aqua>ℹ</aqua>]</dark_gray> <gray>" + message + "</gray>");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package fr.luc.bettermccommands.api;
|
||||
|
||||
/**
|
||||
* Interface fonctionnelle représentant l'action exécutée par une commande ou sous-commande.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandExecutor {
|
||||
|
||||
/**
|
||||
* Exécute la logique métier de la commande avec le contexte fourni.
|
||||
*
|
||||
* @param context Le contexte d'exécution contenant l'émetteur, les arguments et les métadonnées.
|
||||
* @throws Exception En cas d'erreur lors du traitement de la commande.
|
||||
*/
|
||||
void execute(CommandContext context) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package fr.luc.bettermccommands.api;
|
||||
|
||||
import fr.luc.bettermccommands.argument.CommandArgument;
|
||||
import fr.luc.bettermccommands.event.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Représente un nœud dans l'arborescence des commandes (commande racine ou sous-commande).
|
||||
*/
|
||||
public class CommandNode {
|
||||
|
||||
protected final String name;
|
||||
protected String description;
|
||||
protected String permission;
|
||||
protected CommandSenderType senderType = CommandSenderType.ALL;
|
||||
protected final Set<String> aliases = new LinkedHashSet<>();
|
||||
protected CommandNode parent;
|
||||
protected final Map<String, CommandNode> subCommands = new LinkedHashMap<>();
|
||||
protected final List<CommandArgument<?>> arguments = new ArrayList<>();
|
||||
protected CommandExecutor executor;
|
||||
protected Duration cooldown = Duration.ZERO;
|
||||
protected String cooldownBypassPermission;
|
||||
|
||||
// Écouteurs d'événements locaux attachés directement à ce nœud
|
||||
protected final List<CommandEventListener<CommandPreExecuteEvent>> preExecuteListeners = new CopyOnWriteArrayList<>();
|
||||
protected final List<CommandEventListener<CommandPostExecuteEvent>> postExecuteListeners = new CopyOnWriteArrayList<>();
|
||||
protected final List<CommandEventListener<CommandPermissionDeniedEvent>> permissionDeniedListeners = new CopyOnWriteArrayList<>();
|
||||
protected final List<CommandEventListener<CommandSyntaxErrorEvent>> syntaxErrorListeners = new CopyOnWriteArrayList<>();
|
||||
protected final List<CommandEventListener<CommandCooldownEvent>> cooldownListeners = new CopyOnWriteArrayList<>();
|
||||
protected final List<CommandEventListener<CommandTabCompleteEvent>> tabCompleteListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
* Crée un nouveau nœud de commande avec son nom identifiant.
|
||||
*
|
||||
* @param name Le nom du nœud.
|
||||
*/
|
||||
public CommandNode(String name) {
|
||||
this.name = Objects.requireNonNull(name, "Command node name cannot be null").toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le nom unique de ce nœud.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La description explicative du rôle de ce nœud.
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description != null ? description : "";
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La permission requise pour exécuter ce nœud (ou null).
|
||||
*/
|
||||
public String getPermission() {
|
||||
return permission;
|
||||
}
|
||||
|
||||
public void setPermission(String permission) {
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le type d'émetteur requis (Joueur, Console, Tous).
|
||||
*/
|
||||
public CommandSenderType getSenderType() {
|
||||
return senderType;
|
||||
}
|
||||
|
||||
public void setSenderType(CommandSenderType senderType) {
|
||||
this.senderType = senderType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'ensemble des alias enregistrés pour ce nœud.
|
||||
*/
|
||||
public Set<String> getAliases() {
|
||||
return Collections.unmodifiableSet(aliases);
|
||||
}
|
||||
|
||||
public void addAliases(Collection<String> aliases) {
|
||||
this.aliases.addAll(aliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le nœud parent, ou {@code null} s'il s'agit de la commande racine.
|
||||
*/
|
||||
public CommandNode getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le nœud parent.
|
||||
*
|
||||
* @param parent Le nœud parent.
|
||||
*/
|
||||
public void setParent(CommandNode parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Une vue non modifiable des sous-commandes de ce nœud.
|
||||
*/
|
||||
public Collection<CommandNode> getSubCommands() {
|
||||
return Collections.unmodifiableSet(new LinkedHashSet<>(subCommands.values()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une sous-commande et enregistre ses alias.
|
||||
*
|
||||
* @param subNode La sous-commande à associer.
|
||||
*/
|
||||
public void addSubCommand(CommandNode subNode) {
|
||||
subNode.setParent(this);
|
||||
this.subCommands.put(subNode.getName().toLowerCase(), subNode);
|
||||
for (String alias : subNode.getAliases()) {
|
||||
this.subCommands.put(alias.toLowerCase(), subNode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche une sous-commande par nom ou alias.
|
||||
*
|
||||
* @param nameOrAlias Le nom ou alias recherché.
|
||||
* @return Le {@link CommandNode} correspondant, ou {@code null}.
|
||||
*/
|
||||
public CommandNode findSubCommand(String nameOrAlias) {
|
||||
if (nameOrAlias == null) return null;
|
||||
return subCommands.get(nameOrAlias.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si ce nœud possède des sous-commandes enregistrées.
|
||||
*/
|
||||
public boolean hasSubCommands() {
|
||||
return !subCommands.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La liste ordonnée des arguments attendus pour ce nœud.
|
||||
*/
|
||||
public List<CommandArgument<?>> getArguments() {
|
||||
return Collections.unmodifiableList(arguments);
|
||||
}
|
||||
|
||||
public void addArguments(Collection<CommandArgument<?>> arguments) {
|
||||
this.arguments.addAll(arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le gestionnaire d'exécution métier associé.
|
||||
*/
|
||||
public CommandExecutor getExecutor() {
|
||||
return executor;
|
||||
}
|
||||
|
||||
public void setExecutor(CommandExecutor executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La durée du temps de recharge configuré.
|
||||
*/
|
||||
public Duration getCooldown() {
|
||||
return cooldown;
|
||||
}
|
||||
|
||||
public void setCooldown(Duration cooldown) {
|
||||
this.cooldown = cooldown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La permission permettant d'ignorer le cooldown (ou null).
|
||||
*/
|
||||
public String getCooldownBypassPermission() {
|
||||
return cooldownBypassPermission;
|
||||
}
|
||||
|
||||
public void setCooldownBypassPermission(String cooldownBypassPermission) {
|
||||
this.cooldownBypassPermission = cooldownBypassPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le chemin complet du nœud (ex: "demo admin setrank").
|
||||
*
|
||||
* @return La chaîne hiérarchique complète.
|
||||
*/
|
||||
public String getFullName() {
|
||||
if (parent == null) {
|
||||
return name;
|
||||
}
|
||||
return parent.getFullName() + " " + name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le nom de la commande racine au sommet de l'arborescence.
|
||||
*/
|
||||
public String getRootName() {
|
||||
if (parent == null) {
|
||||
return name;
|
||||
}
|
||||
return parent.getRootName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit la syntaxe d'utilisation complète (ex: "/demo give <cible> [quantite]").
|
||||
*
|
||||
* @return La chaîne d'aide formatée.
|
||||
*/
|
||||
public String getUsage() {
|
||||
StringBuilder builder = new StringBuilder("/").append(getFullName());
|
||||
for (CommandArgument<?> arg : arguments) {
|
||||
builder.append(" ").append(arg.getUsage());
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
// Gestion des écouteurs locaux
|
||||
public List<CommandEventListener<CommandPreExecuteEvent>> getPreExecuteListeners() {
|
||||
return preExecuteListeners;
|
||||
}
|
||||
|
||||
public List<CommandEventListener<CommandPostExecuteEvent>> getPostExecuteListeners() {
|
||||
return postExecuteListeners;
|
||||
}
|
||||
|
||||
public List<CommandEventListener<CommandPermissionDeniedEvent>> getPermissionDeniedListeners() {
|
||||
return permissionDeniedListeners;
|
||||
}
|
||||
|
||||
public List<CommandEventListener<CommandSyntaxErrorEvent>> getSyntaxErrorListeners() {
|
||||
return syntaxErrorListeners;
|
||||
}
|
||||
|
||||
public List<CommandEventListener<CommandCooldownEvent>> getCooldownListeners() {
|
||||
return cooldownListeners;
|
||||
}
|
||||
|
||||
public List<CommandEventListener<CommandTabCompleteEvent>> getTabCompleteListeners() {
|
||||
return tabCompleteListeners;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package fr.luc.bettermccommands.api;
|
||||
|
||||
/**
|
||||
* Représente l'état et le résultat final de l'évaluation ou de l'exécution d'une commande.
|
||||
*/
|
||||
public enum CommandResult {
|
||||
|
||||
/**
|
||||
* La commande a été exécutée avec succès.
|
||||
*/
|
||||
SUCCESS,
|
||||
|
||||
/**
|
||||
* L'exécution a échoué en raison d'une exception non gérée ou d'un échec métier.
|
||||
*/
|
||||
FAILED,
|
||||
|
||||
/**
|
||||
* L'exécution a été annulée par un écouteur d'événement (ex: CommandPreExecuteEvent).
|
||||
*/
|
||||
CANCELLED,
|
||||
|
||||
/**
|
||||
* L'émetteur n'a pas les permissions requises ou le bon type d'émetteur.
|
||||
*/
|
||||
PERMISSION_DENIED,
|
||||
|
||||
/**
|
||||
* La syntaxe est invalide (arguments manquants ou sous-commande inconnue).
|
||||
*/
|
||||
SYNTAX_ERROR,
|
||||
|
||||
/**
|
||||
* Un argument n'a pas pu être converti / parsé vers son type cible.
|
||||
*/
|
||||
ARGUMENT_PARSE_ERROR,
|
||||
|
||||
/**
|
||||
* La commande a été bloquée car le joueur est sous cooldown.
|
||||
*/
|
||||
COOLDOWN
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fr.luc.bettermccommands.api;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Définit le type d'émetteur autorisé à exécuter une commande ou sous-commande.
|
||||
*/
|
||||
public enum CommandSenderType {
|
||||
|
||||
/**
|
||||
* Tous les émetteurs sont autorisés (Joueur, Console, CommandBlock, etc.).
|
||||
*/
|
||||
ALL,
|
||||
|
||||
/**
|
||||
* Seuls les joueurs connectés sont autorisés.
|
||||
*/
|
||||
PLAYER_ONLY,
|
||||
|
||||
/**
|
||||
* Seule la console du serveur est autorisée.
|
||||
*/
|
||||
CONSOLE_ONLY;
|
||||
|
||||
/**
|
||||
* Vérifie si l'émetteur donné correspond au type requis.
|
||||
*
|
||||
* @param sender L'émetteur de la commande.
|
||||
* @return true si l'émetteur est autorisé, sinon false.
|
||||
*/
|
||||
public boolean isAllowed(CommandSender sender) {
|
||||
if (sender == null) {
|
||||
return false;
|
||||
}
|
||||
return switch (this) {
|
||||
case ALL -> true;
|
||||
case PLAYER_ONLY -> sender instanceof Player;
|
||||
case CONSOLE_ONLY -> sender instanceof ConsoleCommandSender;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package fr.luc.bettermccommands.api.suggestion;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Représente une suggestion d'auto-complétion (Tab-Complete) avec son texte et une éventuelle description.
|
||||
*/
|
||||
public class Suggestion {
|
||||
|
||||
private final String value;
|
||||
private final String tooltip;
|
||||
|
||||
/**
|
||||
* Crée une suggestion simple sans infobulle.
|
||||
*
|
||||
* @param value Le texte de la suggestion.
|
||||
*/
|
||||
public Suggestion(String value) {
|
||||
this(value, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une suggestion avec une infobulle/description explicative.
|
||||
*
|
||||
* @param value Le texte inséré lors de la complétion.
|
||||
* @param tooltip L'infobulle ou description affichée au joueur (optionnelle).
|
||||
*/
|
||||
public Suggestion(String value, String tooltip) {
|
||||
this.value = Objects.requireNonNull(value, "value cannot be null");
|
||||
this.tooltip = tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle suggestion simple.
|
||||
*
|
||||
* @param value Le texte de la suggestion.
|
||||
* @return L'instance de {@link Suggestion}.
|
||||
*/
|
||||
public static Suggestion of(String value) {
|
||||
return new Suggestion(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle suggestion avec infobulle.
|
||||
*
|
||||
* @param value Le texte de la suggestion.
|
||||
* @param tooltip L'infobulle explicative.
|
||||
* @return L'instance de {@link Suggestion}.
|
||||
*/
|
||||
public static Suggestion of(String value, String tooltip) {
|
||||
return new Suggestion(value, tooltip);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le texte de la suggestion.
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'infobulle / description de la suggestion, ou {@code null} si non définie.
|
||||
*/
|
||||
public String getTooltip() {
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Suggestion that)) return false;
|
||||
return Objects.equals(value, that.value) && Objects.equals(tooltip, that.tooltip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value, tooltip);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return tooltip == null ? value : value + " (" + tooltip + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package fr.luc.bettermccommands.api.suggestion;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Fournisseur dynamique de suggestions pour l'auto-complétion (Tab-Complete).
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SuggestionProvider {
|
||||
|
||||
/**
|
||||
* Calcule et retourne la liste des suggestions pour un argument donné dans un contexte précis.
|
||||
*
|
||||
* @param context Le contexte actuel de la commande.
|
||||
* @param currentInput La saisie en cours de frappe par l'utilisateur.
|
||||
* @return La collection de suggestions correspondantes.
|
||||
*/
|
||||
Collection<Suggestion> getSuggestions(CommandContext context, String currentInput);
|
||||
|
||||
/**
|
||||
* Crée un fournisseur à partir d'une liste statique de chaînes de caractères.
|
||||
*
|
||||
* @param values Les valeurs possibles.
|
||||
* @return Un {@link SuggestionProvider} filtrant les valeurs par préfixe insensible à la casse.
|
||||
*/
|
||||
static SuggestionProvider strings(String... values) {
|
||||
return strings(Arrays.asList(values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un fournisseur à partir d'une collection statique de chaînes.
|
||||
*
|
||||
* @param values La collection de valeurs textuelles.
|
||||
* @return Un {@link SuggestionProvider} filtrant par préfixe.
|
||||
*/
|
||||
static SuggestionProvider strings(Collection<String> values) {
|
||||
return (context, currentInput) -> {
|
||||
String lower = currentInput == null ? "" : currentInput.toLowerCase();
|
||||
return values.stream()
|
||||
.filter(v -> v.toLowerCase().startsWith(lower))
|
||||
.map(Suggestion::of)
|
||||
.collect(Collectors.toList());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un fournisseur vide ne retournant aucune suggestion.
|
||||
*
|
||||
* @return Un {@link SuggestionProvider} vide.
|
||||
*/
|
||||
static SuggestionProvider empty() {
|
||||
return (context, currentInput) -> List.of();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package fr.luc.bettermccommands.argument;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.suggestion.Suggestion;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Contrat pour convertir une chaîne de caractères en un objet Java typé et fournir des suggestions de complétion.
|
||||
*
|
||||
* @param <T> Le type de données retourné par le parseur.
|
||||
*/
|
||||
public interface ArgumentType<T> {
|
||||
|
||||
/**
|
||||
* Analyse et convertit l'entrée textuelle en objet Java du type {@link T}.
|
||||
*
|
||||
* @param argumentName Le nom de l'argument en cours de traitement.
|
||||
* @param input La valeur textuelle brute entrée par le joueur.
|
||||
* @param context Le contexte d'exécution actuel.
|
||||
* @return L'objet converti et validé.
|
||||
* @throws CommandArgumentParseException si la chaîne ne respecte pas le format attendu.
|
||||
*/
|
||||
T parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException;
|
||||
|
||||
/**
|
||||
* Fournit les suggestions d'auto-complétion (Tab-Complete) pour ce type d'argument.
|
||||
*
|
||||
* @param context Le contexte d'exécution de la commande.
|
||||
* @param currentInput La chaîne actuellement tapée par l'utilisateur.
|
||||
* @return La collection de suggestions proposées.
|
||||
*/
|
||||
default Collection<Suggestion> suggest(CommandContext context, String currentInput) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le nom lisible de ce type (ex: "Entier", "Joueur", "Texte").
|
||||
*
|
||||
* @return Le nom convivial du type.
|
||||
*/
|
||||
String getTypeName();
|
||||
|
||||
/**
|
||||
* Retourne le format d'affichage pour l'aide/usage (ex: "<joueur>", "[quantité]").
|
||||
*
|
||||
* @param argumentName Le nom de l'argument.
|
||||
* @param optional Indique si l'argument est optionnel.
|
||||
* @return La chaîne d'affichage d'usage.
|
||||
*/
|
||||
default String getUsagePlaceholder(String argumentName, boolean optional) {
|
||||
return optional ? "[" + argumentName + "]" : "<" + argumentName + ">";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package fr.luc.bettermccommands.argument;
|
||||
|
||||
import fr.luc.bettermccommands.argument.type.*;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Usine statique (Factory) simplifiant la déclaration et la création des arguments de commande.
|
||||
*/
|
||||
public final class Arguments {
|
||||
|
||||
private Arguments() {
|
||||
// Empêche l'instanciation
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument de type mot simple (délimité par un espace).
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<String> word(String name) {
|
||||
return new CommandArgument<>(name, StringArgument.word());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument de type chaîne de caractères.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<String> string(String name) {
|
||||
return new CommandArgument<>(name, StringArgument.string());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument de type chaîne gourmande (consommant tous les mots jusqu'à la fin de la commande).
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré en mode greedy.
|
||||
*/
|
||||
public static CommandArgument<String> greedyString(String name) {
|
||||
return new CommandArgument<>(name, StringArgument.string()).greedy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument de type nombre entier non restreint.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<Integer> integer(String name) {
|
||||
return new CommandArgument<>(name, IntegerArgument.integer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument de type nombre entier avec une borne minimale.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @param min La valeur minimale autorisée.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<Integer> integer(String name, int min) {
|
||||
return new CommandArgument<>(name, IntegerArgument.min(min));
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument de type nombre entier borné entre min et max.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @param min La valeur minimale.
|
||||
* @param max La valeur maximale.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<Integer> integer(String name, int min, int max) {
|
||||
return new CommandArgument<>(name, IntegerArgument.range(min, max));
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument de type nombre décimal (double).
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<Double> decimal(String name) {
|
||||
return new CommandArgument<>(name, DoubleArgument.number());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument de type nombre décimal borné.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @param min La valeur minimale.
|
||||
* @param max La valeur maximale.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<Double> decimal(String name, double min, double max) {
|
||||
return new CommandArgument<>(name, DoubleArgument.range(min, max));
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument booléen (true/false, oui/non).
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<Boolean> bool(String name) {
|
||||
return new CommandArgument<>(name, BooleanArgument.bool());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument pour cibler un joueur connecté.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<Player> player(String name) {
|
||||
return new CommandArgument<>(name, PlayerArgument.player());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument pour cibler un joueur hors-ligne ou en ligne.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<OfflinePlayer> offlinePlayer(String name) {
|
||||
return new CommandArgument<>(name, OfflinePlayerArgument.offlinePlayer());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument pour cibler un monde Bukkit.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<World> world(String name) {
|
||||
return new CommandArgument<>(name, WorldArgument.world());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument pour des coordonnées géographiques.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<Location> location(String name) {
|
||||
return new CommandArgument<>(name, LocationArgument.location());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument pour une durée temporelle (ex: 30s, 15m, 2h).
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static CommandArgument<Duration> duration(String name) {
|
||||
return new CommandArgument<>(name, DurationArgument.duration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument basé sur un enum Java arbitraire.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @param enumClass La classe de l'enum.
|
||||
* @param <E> Le type de l'enum.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static <E extends Enum<E>> CommandArgument<E> enumOf(String name, Class<E> enumClass) {
|
||||
return new CommandArgument<>(name, EnumArgument.of(enumClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument avec un type personnalisé.
|
||||
*
|
||||
* @param name Le nom de l'argument.
|
||||
* @param type L'instance de {@link ArgumentType}.
|
||||
* @param <T> Le type de l'objet.
|
||||
* @return Le {@link CommandArgument} configuré.
|
||||
*/
|
||||
public static <T> CommandArgument<T> custom(String name, ArgumentType<T> type) {
|
||||
return new CommandArgument<>(name, type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package fr.luc.bettermccommands.argument;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.suggestion.Suggestion;
|
||||
import fr.luc.bettermccommands.api.suggestion.SuggestionProvider;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Représente la définition d'un argument au sein d'une commande ou sous-commande.
|
||||
*
|
||||
* @param <T> Le type de l'argument.
|
||||
*/
|
||||
public class CommandArgument<T> {
|
||||
|
||||
private final String name;
|
||||
private final ArgumentType<T> type;
|
||||
private String description;
|
||||
private boolean optional;
|
||||
private Supplier<T> defaultValueSupplier;
|
||||
private SuggestionProvider customSuggestionProvider;
|
||||
private boolean greedy;
|
||||
|
||||
/**
|
||||
* Crée un argument obligatoire avec son nom et son type.
|
||||
*
|
||||
* @param name Le nom unique de l'argument au sein du nœud de commande.
|
||||
* @param type Le type de conversion et de validation de l'argument.
|
||||
*/
|
||||
public CommandArgument(String name, ArgumentType<T> type) {
|
||||
this.name = Objects.requireNonNull(name, "Argument name cannot be null");
|
||||
this.type = Objects.requireNonNull(type, "Argument type cannot be null");
|
||||
this.optional = false;
|
||||
this.greedy = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la description de cet argument.
|
||||
*
|
||||
* @param description Le texte descriptif.
|
||||
* @return Cette instance d'argument pour chaînage.
|
||||
*/
|
||||
public CommandArgument<T> description(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque cet argument comme optionnel (sans valeur par défaut, retourne null si omis).
|
||||
*
|
||||
* @return Cette instance d'argument.
|
||||
*/
|
||||
public CommandArgument<T> optional() {
|
||||
this.optional = true;
|
||||
this.defaultValueSupplier = () -> null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque cet argument comme optionnel avec une valeur par défaut constante.
|
||||
*
|
||||
* @param defaultValue La valeur par défaut à appliquer si l'argument est omis.
|
||||
* @return Cette instance d'argument.
|
||||
*/
|
||||
public CommandArgument<T> defaultValue(T defaultValue) {
|
||||
this.optional = true;
|
||||
this.defaultValueSupplier = () -> defaultValue;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque cet argument comme optionnel avec un fournisseur dynamique de valeur par défaut.
|
||||
*
|
||||
* @param supplier Le fournisseur de valeur par défaut.
|
||||
* @return Cette instance d'argument.
|
||||
*/
|
||||
public CommandArgument<T> defaultValue(Supplier<T> supplier) {
|
||||
this.optional = true;
|
||||
this.defaultValueSupplier = supplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit un fournisseur de suggestions personnalisé pour surcharger celui du type de base.
|
||||
*
|
||||
* @param provider Le fournisseur de suggestions.
|
||||
* @return Cette instance d'argument.
|
||||
*/
|
||||
public CommandArgument<T> suggest(SuggestionProvider provider) {
|
||||
this.customSuggestionProvider = provider;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit une liste statique de suggestions possibles pour cet argument.
|
||||
*
|
||||
* @param suggestions Les valeurs suggérées.
|
||||
* @return Cette instance d'argument.
|
||||
*/
|
||||
public CommandArgument<T> suggest(String... suggestions) {
|
||||
this.customSuggestionProvider = SuggestionProvider.strings(suggestions);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque l'argument comme "greedy" (il consomme tous les mots restants jusqu'à la fin de la commande).
|
||||
* Utile pour les messages de broadcast, raisons de bannissement, descriptions longues.
|
||||
*
|
||||
* @return Cette instance d'argument.
|
||||
*/
|
||||
public CommandArgument<T> greedy() {
|
||||
this.greedy = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le nom de l'argument.
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le type associé.
|
||||
*/
|
||||
public ArgumentType<T> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La description explicative de l'argument.
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'argument est optionnel, sinon false.
|
||||
*/
|
||||
public boolean isOptional() {
|
||||
return optional;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'argument est greedy (consomme le reste de la ligne).
|
||||
*/
|
||||
public boolean isGreedy() {
|
||||
return greedy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la valeur par défaut pour cet argument.
|
||||
*
|
||||
* @return La valeur par défaut, ou {@code null}.
|
||||
*/
|
||||
public T getDefaultValue() {
|
||||
return defaultValueSupplier != null ? defaultValueSupplier.get() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les suggestions applicables pour cet argument dans le contexte donné.
|
||||
*
|
||||
* @param context Le contexte d'exécution.
|
||||
* @param currentInput La saisie en cours.
|
||||
* @return Les suggestions calculées.
|
||||
*/
|
||||
public Collection<Suggestion> getSuggestions(CommandContext context, String currentInput) {
|
||||
if (customSuggestionProvider != null) {
|
||||
return customSuggestionProvider.getSuggestions(context, currentInput);
|
||||
}
|
||||
return type.suggest(context, currentInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la représentation textuelle de l'argument pour l'aide.
|
||||
*
|
||||
* @return Ex: "<joueur>" ou "[quantite]"
|
||||
*/
|
||||
public String getUsage() {
|
||||
return type.getUsagePlaceholder(name, optional);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package fr.luc.bettermccommands.argument;
|
||||
|
||||
/**
|
||||
* Exception levée lorsqu'un argument textuel ne peut pas être converti vers son type cible.
|
||||
*/
|
||||
public class CommandArgumentParseException extends Exception {
|
||||
|
||||
private final String argumentName;
|
||||
private final String rawInput;
|
||||
private final String expectedTypeName;
|
||||
|
||||
/**
|
||||
* Crée une nouvelle exception de parsing d'argument.
|
||||
*
|
||||
* @param argumentName Le nom de l'argument concerné.
|
||||
* @param rawInput La chaîne brute fournie par l'utilisateur.
|
||||
* @param expectedTypeName Le nom convivial du type attendu.
|
||||
* @param message Le message d'erreur explicatif.
|
||||
*/
|
||||
public CommandArgumentParseException(String argumentName, String rawInput, String expectedTypeName, String message) {
|
||||
super(message);
|
||||
this.argumentName = argumentName;
|
||||
this.rawInput = rawInput;
|
||||
this.expectedTypeName = expectedTypeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le nom de l'argument.
|
||||
*/
|
||||
public String getArgumentName() {
|
||||
return argumentName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La saisie brute entrée par le joueur.
|
||||
*/
|
||||
public String getRawInput() {
|
||||
return rawInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le nom du type cible attendu.
|
||||
*/
|
||||
public String getExpectedTypeName() {
|
||||
return expectedTypeName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package fr.luc.bettermccommands.argument.type;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.suggestion.Suggestion;
|
||||
import fr.luc.bettermccommands.argument.ArgumentType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Type d'argument pour les valeurs booléennes (true/false, oui/non, on/off, 1/0).
|
||||
*/
|
||||
public class BooleanArgument implements ArgumentType<Boolean> {
|
||||
|
||||
private static final BooleanArgument INSTANCE = new BooleanArgument();
|
||||
|
||||
/**
|
||||
* @return L'instance singleton de {@link BooleanArgument}.
|
||||
*/
|
||||
public static BooleanArgument bool() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
String lower = input.trim().toLowerCase();
|
||||
if (lower.equals("true") || lower.equals("oui") || lower.equals("on") || lower.equals("1") || lower.equals("yes")) {
|
||||
return true;
|
||||
}
|
||||
if (lower.equals("false") || lower.equals("non") || lower.equals("off") || lower.equals("0") || lower.equals("no")) {
|
||||
return false;
|
||||
}
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"'" + input + "' n'est pas un booléen valide (attendu: true/false, oui/non, on/off).");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Suggestion> suggest(CommandContext context, String currentInput) {
|
||||
String lower = currentInput == null ? "" : currentInput.toLowerCase();
|
||||
List<String> options = List.of("true", "false", "oui", "non");
|
||||
return options.stream()
|
||||
.filter(opt -> opt.startsWith(lower))
|
||||
.map(Suggestion::of)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Booléen";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package fr.luc.bettermccommands.argument.type;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.argument.ArgumentType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
|
||||
/**
|
||||
* Type d'argument pour les nombres décimaux (double) avec bornes optionnelles.
|
||||
*/
|
||||
public class DoubleArgument implements ArgumentType<Double> {
|
||||
|
||||
private final Double min;
|
||||
private final Double max;
|
||||
|
||||
/**
|
||||
* Crée un argument décimal sans bornes.
|
||||
*/
|
||||
public DoubleArgument() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument décimal avec bornes.
|
||||
*
|
||||
* @param min La valeur minimale autorisée.
|
||||
* @param max La valeur maximale autorisée.
|
||||
*/
|
||||
public DoubleArgument(Double min, Double max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un type d'argument décimal standard.
|
||||
*
|
||||
* @return L'instance configurée.
|
||||
*/
|
||||
public static DoubleArgument number() {
|
||||
return new DoubleArgument();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un type d'argument décimal avec une valeur minimale.
|
||||
*
|
||||
* @param min Le minimum.
|
||||
* @return L'instance configurée.
|
||||
*/
|
||||
public static DoubleArgument min(double min) {
|
||||
return new DoubleArgument(min, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un type d'argument décimal avec un intervalle fermé [min, max].
|
||||
*
|
||||
* @param min Le minimum.
|
||||
* @param max Le maximum.
|
||||
* @return L'instance configurée.
|
||||
*/
|
||||
public static DoubleArgument range(double min, double max) {
|
||||
return new DoubleArgument(min, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
double val;
|
||||
try {
|
||||
val = Double.parseDouble(input.replace(',', '.'));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"'" + input + "' n'est pas un nombre décimal valide.");
|
||||
}
|
||||
|
||||
if (min != null && val < min) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Le nombre doit être supérieur ou égal à " + min + " (valeur reçue: " + val + ").");
|
||||
}
|
||||
if (max != null && val > max) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Le nombre doit être inférieur ou égal à " + max + " (valeur reçue: " + val + ").");
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
if (min != null && max != null) {
|
||||
return "Décimal [" + min + ".." + max + "]";
|
||||
} else if (min != null) {
|
||||
return "Décimal (>=" + min + ")";
|
||||
} else if (max != null) {
|
||||
return "Décimal (<=" + max + ")";
|
||||
}
|
||||
return "Décimal";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package fr.luc.bettermccommands.argument.type;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.suggestion.Suggestion;
|
||||
import fr.luc.bettermccommands.argument.ArgumentType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Type d'argument pour analyser des durées temporelles textuelles (ex: "30s", "10m", "2h", "1d", "7d").
|
||||
*/
|
||||
public class DurationArgument implements ArgumentType<Duration> {
|
||||
|
||||
private static final DurationArgument INSTANCE = new DurationArgument();
|
||||
private static final Pattern DURATION_PATTERN = Pattern.compile("(\\d+)\\s*([smhdw]|sec|min|heure|jour|semaine|seconds?|minutes?|hours?|days?|weeks?)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* @return L'instance singleton de {@link DurationArgument}.
|
||||
*/
|
||||
public static DurationArgument duration() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
String clean = input.trim();
|
||||
if (clean.matches("^\\d+$")) {
|
||||
// Nombre seul = secondes par défaut
|
||||
long seconds = Long.parseLong(clean);
|
||||
return Duration.ofSeconds(seconds);
|
||||
}
|
||||
|
||||
Matcher matcher = DURATION_PATTERN.matcher(clean);
|
||||
long totalSeconds = 0;
|
||||
boolean found = false;
|
||||
|
||||
while (matcher.find()) {
|
||||
found = true;
|
||||
long amount = Long.parseLong(matcher.group(1));
|
||||
String unit = matcher.group(2).toLowerCase();
|
||||
|
||||
switch (unit) {
|
||||
case "s", "sec", "second", "seconds" -> totalSeconds += amount;
|
||||
case "m", "min", "minute", "minutes" -> totalSeconds += amount * 60;
|
||||
case "h", "heure", "heures", "hour", "hours" -> totalSeconds += amount * 3600;
|
||||
case "d", "j", "jour", "jours", "day", "days" -> totalSeconds += amount * 86400;
|
||||
case "w", "semaine", "semaines", "week", "weeks" -> totalSeconds += amount * 604800;
|
||||
default -> throw new CommandArgumentParseException(argumentName, input, getTypeName(), "Unité de durée inconnue: " + unit);
|
||||
}
|
||||
}
|
||||
|
||||
if (!found || totalSeconds <= 0) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Format de durée invalide '" + input + "' (exemples valides: 30s, 15m, 2h, 1d).");
|
||||
}
|
||||
|
||||
return Duration.ofSeconds(totalSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Suggestion> suggest(CommandContext context, String currentInput) {
|
||||
String lower = currentInput == null ? "" : currentInput.toLowerCase();
|
||||
List<String> common = List.of("30s", "1m", "5m", "15m", "30m", "1h", "12h", "1d", "7d");
|
||||
return common.stream()
|
||||
.filter(s -> s.startsWith(lower))
|
||||
.map(s -> Suggestion.of(s, "Durée " + s))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Durée";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package fr.luc.bettermccommands.argument.type;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.suggestion.Suggestion;
|
||||
import fr.luc.bettermccommands.argument.ArgumentType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Type d'argument générique permettant de parser n'importe quel {@link Enum} Java.
|
||||
*
|
||||
* @param <E> Le type d'enum.
|
||||
*/
|
||||
public class EnumArgument<E extends Enum<E>> implements ArgumentType<E> {
|
||||
|
||||
private final Class<E> enumClass;
|
||||
|
||||
/**
|
||||
* Crée un parseur d'enum pour la classe donnée.
|
||||
*
|
||||
* @param enumClass La classe d'enum.
|
||||
*/
|
||||
public EnumArgument(Class<E> enumClass) {
|
||||
this.enumClass = Objects.requireNonNull(enumClass, "enumClass cannot be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Fabrique un nouveau type d'argument enum.
|
||||
*
|
||||
* @param enumClass La classe d'enum.
|
||||
* @param <E> Le type de l'enum.
|
||||
* @return L'instance configurée de {@link EnumArgument}.
|
||||
*/
|
||||
public static <E extends Enum<E>> EnumArgument<E> of(Class<E> enumClass) {
|
||||
return new EnumArgument<>(enumClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public E parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
for (E constant : enumClass.getEnumConstants()) {
|
||||
if (constant.name().equalsIgnoreCase(input)) {
|
||||
return constant;
|
||||
}
|
||||
}
|
||||
String validValues = Arrays.stream(enumClass.getEnumConstants())
|
||||
.map(e -> e.name().toLowerCase())
|
||||
.collect(Collectors.joining(", "));
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Valeur invalide '" + input + "'. Valeurs attendues : [" + validValues + "].");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Suggestion> suggest(CommandContext context, String currentInput) {
|
||||
String lower = currentInput == null ? "" : currentInput.toLowerCase();
|
||||
return Arrays.stream(enumClass.getEnumConstants())
|
||||
.map(e -> e.name().toLowerCase())
|
||||
.filter(name -> name.startsWith(lower))
|
||||
.map(Suggestion::of)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return enumClass.getSimpleName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package fr.luc.bettermccommands.argument.type;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.argument.ArgumentType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
|
||||
/**
|
||||
* Type d'argument pour les nombres entiers avec bornes minimale et maximale optionnelles.
|
||||
*/
|
||||
public class IntegerArgument implements ArgumentType<Integer> {
|
||||
|
||||
private final Integer min;
|
||||
private final Integer max;
|
||||
|
||||
/**
|
||||
* Crée un argument entier sans restriction de bornes.
|
||||
*/
|
||||
public IntegerArgument() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un argument entier avec bornes optionnelles.
|
||||
*
|
||||
* @param min La valeur minimale autorisée (ou null).
|
||||
* @param max La valeur maximale autorisée (ou null).
|
||||
*/
|
||||
public IntegerArgument(Integer min, Integer max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un type d'argument entier non borné.
|
||||
*
|
||||
* @return L'instance de {@link IntegerArgument}.
|
||||
*/
|
||||
public static IntegerArgument integer() {
|
||||
return new IntegerArgument();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un type d'argument entier avec un minimum.
|
||||
*
|
||||
* @param min La valeur minimale.
|
||||
* @return L'instance configurée.
|
||||
*/
|
||||
public static IntegerArgument min(int min) {
|
||||
return new IntegerArgument(min, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un type d'argument entier avec un intervalle fermé [min, max].
|
||||
*
|
||||
* @param min La valeur minimale.
|
||||
* @param max La valeur maximale.
|
||||
* @return L'instance configurée.
|
||||
*/
|
||||
public static IntegerArgument range(int min, int max) {
|
||||
return new IntegerArgument(min, max);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
int val;
|
||||
try {
|
||||
val = Integer.parseInt(input);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"'" + input + "' n'est pas un nombre entier valide.");
|
||||
}
|
||||
|
||||
if (min != null && val < min) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Le nombre doit être supérieur ou égal à " + min + " (valeur reçue: " + val + ").");
|
||||
}
|
||||
if (max != null && val > max) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Le nombre doit être inférieur ou égal à " + max + " (valeur reçue: " + val + ").");
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
if (min != null && max != null) {
|
||||
return "Entier [" + min + ".." + max + "]";
|
||||
} else if (min != null) {
|
||||
return "Entier (>=" + min + ")";
|
||||
} else if (max != null) {
|
||||
return "Entier (<=" + max + ")";
|
||||
}
|
||||
return "Entier";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package fr.luc.bettermccommands.argument.type;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.argument.ArgumentType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
/**
|
||||
* Type d'argument pour parser des coordonnées géographiques (format "x,y,z" ou "x,y,z,monde" avec support des coordonnées relatives '~').
|
||||
*/
|
||||
public class LocationArgument implements ArgumentType<Location> {
|
||||
|
||||
private static final LocationArgument INSTANCE = new LocationArgument();
|
||||
|
||||
/**
|
||||
* @return L'instance singleton de {@link LocationArgument}.
|
||||
*/
|
||||
public static LocationArgument location() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Location parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
String[] parts = input.split(",");
|
||||
if (parts.length < 3) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Les coordonnées doivent être sous la forme 'x,y,z' ou 'x,y,z,monde'.");
|
||||
}
|
||||
|
||||
World world = null;
|
||||
Location reference = null;
|
||||
if (context.isPlayer()) {
|
||||
Player p = context.getPlayer();
|
||||
reference = p.getLocation();
|
||||
world = reference.getWorld();
|
||||
}
|
||||
|
||||
if (parts.length >= 4) {
|
||||
world = org.bukkit.Bukkit.getWorld(parts[3].trim());
|
||||
if (world == null) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Monde introuvable: '" + parts[3].trim() + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
if (world == null) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Impossible de déterminer le monde (émetteur non-joueur et aucun monde spécifié).");
|
||||
}
|
||||
|
||||
try {
|
||||
double x = parseCoord(parts[0].trim(), reference != null ? reference.getX() : 0);
|
||||
double y = parseCoord(parts[1].trim(), reference != null ? reference.getY() : 0);
|
||||
double z = parseCoord(parts[2].trim(), reference != null ? reference.getZ() : 0);
|
||||
return new Location(world, x, y, z);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Coordonnées invalides dans '" + input + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
private double parseCoord(String val, double origin) {
|
||||
if (val.startsWith("~")) {
|
||||
if (val.length() == 1) {
|
||||
return origin;
|
||||
}
|
||||
return origin + Double.parseDouble(val.substring(1));
|
||||
}
|
||||
return Double.parseDouble(val);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Coordonnées (x,y,z)";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package fr.luc.bettermccommands.argument.type;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.suggestion.Suggestion;
|
||||
import fr.luc.bettermccommands.argument.ArgumentType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Type d'argument pour cibler un joueur hors-ligne ou en ligne (par nom ou UUID).
|
||||
*/
|
||||
public class OfflinePlayerArgument implements ArgumentType<OfflinePlayer> {
|
||||
|
||||
private static final OfflinePlayerArgument INSTANCE = new OfflinePlayerArgument();
|
||||
|
||||
/**
|
||||
* @return L'instance singleton de {@link OfflinePlayerArgument}.
|
||||
*/
|
||||
public static OfflinePlayerArgument offlinePlayer() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public OfflinePlayer parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
// Test si entrée au format UUID
|
||||
try {
|
||||
UUID uuid = UUID.fromString(input);
|
||||
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
|
||||
if (offlinePlayer.hasPlayedBefore() || offlinePlayer.isOnline()) {
|
||||
return offlinePlayer;
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Pas un UUID, recherche par nom
|
||||
}
|
||||
|
||||
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(input);
|
||||
if (!offlinePlayer.hasPlayedBefore() && !offlinePlayer.isOnline()) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Le joueur '" + input + "' n'a jamais joué sur ce serveur.");
|
||||
}
|
||||
|
||||
return offlinePlayer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Suggestion> suggest(CommandContext context, String currentInput) {
|
||||
String lower = currentInput == null ? "" : currentInput.toLowerCase();
|
||||
try {
|
||||
return Arrays.stream(Bukkit.getOfflinePlayers())
|
||||
.map(OfflinePlayer::getName)
|
||||
.filter(name -> name != null && name.toLowerCase().startsWith(lower))
|
||||
.map(Suggestion::of)
|
||||
.toList();
|
||||
} catch (Throwable t) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Joueur (Hors-Ligne)";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.luc.bettermccommands.argument.type;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.suggestion.Suggestion;
|
||||
import fr.luc.bettermccommands.argument.ArgumentType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Type d'argument pour cibler un joueur connecté en ligne.
|
||||
*/
|
||||
public class PlayerArgument implements ArgumentType<Player> {
|
||||
|
||||
private static final PlayerArgument INSTANCE = new PlayerArgument();
|
||||
|
||||
/**
|
||||
* @return L'instance singleton de {@link PlayerArgument}.
|
||||
*/
|
||||
public static PlayerArgument player() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Player parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
Player player = Bukkit.getPlayerExact(input);
|
||||
if (player == null) {
|
||||
// Recherche par début de pseudo si exact échoue
|
||||
player = Bukkit.getPlayer(input);
|
||||
}
|
||||
|
||||
if (player == null || !player.isOnline()) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Le joueur '" + input + "' est introuvable ou hors-ligne.");
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Suggestion> suggest(CommandContext context, String currentInput) {
|
||||
String lower = currentInput == null ? "" : currentInput.toLowerCase();
|
||||
try {
|
||||
return Bukkit.getOnlinePlayers().stream()
|
||||
.map(Player::getName)
|
||||
.filter(name -> name.toLowerCase().startsWith(lower))
|
||||
.map(Suggestion::of)
|
||||
.toList();
|
||||
} catch (Throwable t) {
|
||||
// Utile lors des tests unitaires si Bukkit.getOnlinePlayers() n'est pas initialisé
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Joueur";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package fr.luc.bettermccommands.argument.type;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.argument.ArgumentType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
|
||||
/**
|
||||
* Type d'argument pour les chaînes de caractères (mot unique ou chaîne libre).
|
||||
*/
|
||||
public class StringArgument implements ArgumentType<String> {
|
||||
|
||||
private final String typeName;
|
||||
|
||||
/**
|
||||
* Crée un type d'argument pour une chaîne de caractères simple.
|
||||
*/
|
||||
public StringArgument() {
|
||||
this("Texte");
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un type d'argument texte avec un nom explicite.
|
||||
*
|
||||
* @param typeName Le nom lisible.
|
||||
*/
|
||||
public StringArgument(String typeName) {
|
||||
this.typeName = typeName != null ? typeName : "Texte";
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une instance standard pour un mot unique.
|
||||
*
|
||||
* @return L'instance de {@link StringArgument}.
|
||||
*/
|
||||
public static StringArgument word() {
|
||||
return new StringArgument("Mot");
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une instance standard pour une chaîne de caractères libre.
|
||||
*
|
||||
* @return L'instance de {@link StringArgument}.
|
||||
*/
|
||||
public static StringArgument string() {
|
||||
return new StringArgument("Texte");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
if (input == null || input.isEmpty()) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(), "Le texte ne peut pas être vide.");
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return typeName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package fr.luc.bettermccommands.argument.type;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.suggestion.Suggestion;
|
||||
import fr.luc.bettermccommands.argument.ArgumentType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Type d'argument pour cibler un monde Bukkit chargé.
|
||||
*/
|
||||
public class WorldArgument implements ArgumentType<World> {
|
||||
|
||||
private static final WorldArgument INSTANCE = new WorldArgument();
|
||||
|
||||
/**
|
||||
* @return L'instance singleton de {@link WorldArgument}.
|
||||
*/
|
||||
public static WorldArgument world() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public World parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
World world = Bukkit.getWorld(input);
|
||||
if (world == null) {
|
||||
throw new CommandArgumentParseException(argumentName, input, getTypeName(),
|
||||
"Le monde '" + input + "' n'existe pas ou n'est pas chargé.");
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Suggestion> suggest(CommandContext context, String currentInput) {
|
||||
String lower = currentInput == null ? "" : currentInput.toLowerCase();
|
||||
try {
|
||||
return Bukkit.getWorlds().stream()
|
||||
.map(World::getName)
|
||||
.filter(name -> name.toLowerCase().startsWith(lower))
|
||||
.map(Suggestion::of)
|
||||
.toList();
|
||||
} catch (Throwable t) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Monde";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package fr.luc.bettermccommands.builder;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.CommandExecutor;
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
import fr.luc.bettermccommands.api.CommandSenderType;
|
||||
import fr.luc.bettermccommands.argument.CommandArgument;
|
||||
import fr.luc.bettermccommands.event.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Classe abstraite de base fournissant le DSL fluide pour configurer un nœud de commande.
|
||||
*
|
||||
* @param <B> Le type concret du Builder (pour permettre le chaînage fluide).
|
||||
* @param <N> Le type concret de nœud produit (Command ou CommandNode).
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public abstract class AbstractCommandBuilder<B extends AbstractCommandBuilder<B, N>, N extends CommandNode> {
|
||||
|
||||
protected final String name;
|
||||
protected String description;
|
||||
protected String permission;
|
||||
protected CommandSenderType senderType = CommandSenderType.ALL;
|
||||
protected final Set<String> aliases = new LinkedHashSet<>();
|
||||
protected final List<CommandArgument<?>> arguments = new ArrayList<>();
|
||||
protected final List<SubCommandBuilder> subCommandBuilders = new ArrayList<>();
|
||||
protected CommandExecutor executor;
|
||||
protected Duration cooldown = Duration.ZERO;
|
||||
protected String cooldownBypassPermission;
|
||||
|
||||
protected final List<CommandEventListener<CommandPreExecuteEvent>> preExecuteListeners = new ArrayList<>();
|
||||
protected final List<CommandEventListener<CommandPostExecuteEvent>> postExecuteListeners = new ArrayList<>();
|
||||
protected final List<CommandEventListener<CommandPermissionDeniedEvent>> permissionDeniedListeners = new ArrayList<>();
|
||||
protected final List<CommandEventListener<CommandSyntaxErrorEvent>> syntaxErrorListeners = new ArrayList<>();
|
||||
protected final List<CommandEventListener<CommandCooldownEvent>> cooldownListeners = new ArrayList<>();
|
||||
protected final List<CommandEventListener<CommandTabCompleteEvent>> tabCompleteListeners = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Initialise le constructeur avec le nom du nœud.
|
||||
*
|
||||
* @param name Le nom de la commande ou sous-commande.
|
||||
*/
|
||||
public AbstractCommandBuilder(String name) {
|
||||
this.name = Objects.requireNonNull(name, "Name cannot be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la description de la commande.
|
||||
*
|
||||
* @param description Le texte descriptif.
|
||||
* @return Cette instance du builder pour chaînage.
|
||||
*/
|
||||
public B description(String description) {
|
||||
this.description = description;
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit la permission Bukkit requise pour exécuter cette commande.
|
||||
*
|
||||
* @param permission La chaîne de permission (ex: "bettermc.demo").
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B permission(String permission) {
|
||||
this.permission = permission;
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restreint l'exécution de la commande à un type d'émetteur spécifique.
|
||||
*
|
||||
* @param senderType Le type d'émetteur requis.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B senderType(CommandSenderType senderType) {
|
||||
this.senderType = Objects.requireNonNull(senderType, "senderType cannot be null");
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restreint l'exécution de la commande exclusivement aux joueurs connectés.
|
||||
*
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B playerOnly() {
|
||||
this.senderType = CommandSenderType.PLAYER_ONLY;
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restreint l'exécution de la commande exclusivement à la console du serveur.
|
||||
*
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B consoleOnly() {
|
||||
this.senderType = CommandSenderType.CONSOLE_ONLY;
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute un ou plusieurs alias pour cette commande.
|
||||
*
|
||||
* @param aliases Les alias supplémentaires.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B aliases(String... aliases) {
|
||||
if (aliases != null) {
|
||||
for (String alias : aliases) {
|
||||
if (alias != null && !alias.trim().isEmpty()) {
|
||||
this.aliases.add(alias.trim().toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute un argument typé à la commande.
|
||||
*
|
||||
* @param argument L'argument à ajouter.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B argument(CommandArgument<?> argument) {
|
||||
this.arguments.add(Objects.requireNonNull(argument, "Argument cannot be null"));
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une sous-commande via son builder.
|
||||
*
|
||||
* @param subCommandBuilder Le builder de la sous-commande.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B subcommand(SubCommandBuilder subCommandBuilder) {
|
||||
this.subCommandBuilders.add(Objects.requireNonNull(subCommandBuilder, "SubCommandBuilder cannot be null"));
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le gestionnaire d'exécution métier appelé lors de l'invocation.
|
||||
*
|
||||
* @param executor La fonction d'exécution de la commande.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B executes(CommandExecutor executor) {
|
||||
this.executor = executor;
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit un temps de recharge (Cooldown) entre deux exécutions par un même joueur.
|
||||
*
|
||||
* @param cooldown La durée du cooldown.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B cooldown(Duration cooldown) {
|
||||
this.cooldown = cooldown != null ? cooldown : Duration.ZERO;
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit un temps de recharge en secondes.
|
||||
*
|
||||
* @param seconds Le nombre de secondes.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B cooldown(long seconds) {
|
||||
return cooldown(Duration.ofSeconds(seconds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Spécifie la permission permettant d'ignorer le cooldown de cette commande.
|
||||
*
|
||||
* @param bypassPermission La permission de bypass.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B cooldownBypass(String bypassPermission) {
|
||||
this.cooldownBypassPermission = bypassPermission;
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
// --- Écouteurs d'événements (Event Hooks) ---
|
||||
|
||||
/**
|
||||
* Attache un écouteur déclenché juste avant l'exécution de cette commande.
|
||||
*
|
||||
* @param listener L'écouteur.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B onPreExecute(CommandEventListener<CommandPreExecuteEvent> listener) {
|
||||
this.preExecuteListeners.add(listener);
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attache un écouteur déclenché après l'exécution de cette commande.
|
||||
*
|
||||
* @param listener L'écouteur.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B onPostExecute(CommandEventListener<CommandPostExecuteEvent> listener) {
|
||||
this.postExecuteListeners.add(listener);
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attache un écouteur déclenché en cas de refus de permission ou mauvais émetteur.
|
||||
*
|
||||
* @param listener L'écouteur.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B onPermissionDenied(CommandEventListener<CommandPermissionDeniedEvent> listener) {
|
||||
this.permissionDeniedListeners.add(listener);
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attache un écouteur déclenché en cas d'erreur de syntaxe ou argument manquant.
|
||||
*
|
||||
* @param listener L'écouteur.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B onSyntaxError(CommandEventListener<CommandSyntaxErrorEvent> listener) {
|
||||
this.syntaxErrorListeners.add(listener);
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attache un écouteur déclenché lorsqu'un joueur tente d'exécuter la commande sous cooldown.
|
||||
*
|
||||
* @param listener L'écouteur.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B onCooldown(CommandEventListener<CommandCooldownEvent> listener) {
|
||||
this.cooldownListeners.add(listener);
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attache un écouteur déclenché lors du calcul des suggestions d'auto-complétion.
|
||||
*
|
||||
* @param listener L'écouteur.
|
||||
* @return Cette instance du builder.
|
||||
*/
|
||||
public B onTabComplete(CommandEventListener<CommandTabCompleteEvent> listener) {
|
||||
this.tabCompleteListeners.add(listener);
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit et configure l'instance du nœud de commande.
|
||||
*
|
||||
* @return Le nœud construit.
|
||||
*/
|
||||
public abstract N build();
|
||||
|
||||
/**
|
||||
* Applique les propriétés configurées par le builder sur le nœud cible.
|
||||
*
|
||||
* @param node Le nœud à initialiser.
|
||||
*/
|
||||
protected void applyProperties(CommandNode node) {
|
||||
node.setDescription(this.description);
|
||||
node.setPermission(this.permission);
|
||||
node.setSenderType(this.senderType);
|
||||
node.addAliases(this.aliases);
|
||||
node.addArguments(this.arguments);
|
||||
node.setExecutor(this.executor);
|
||||
node.setCooldown(this.cooldown);
|
||||
node.setCooldownBypassPermission(this.cooldownBypassPermission);
|
||||
|
||||
node.getPreExecuteListeners().addAll(this.preExecuteListeners);
|
||||
node.getPostExecuteListeners().addAll(this.postExecuteListeners);
|
||||
node.getPermissionDeniedListeners().addAll(this.permissionDeniedListeners);
|
||||
node.getSyntaxErrorListeners().addAll(this.syntaxErrorListeners);
|
||||
node.getCooldownListeners().addAll(this.cooldownListeners);
|
||||
node.getTabCompleteListeners().addAll(this.tabCompleteListeners);
|
||||
|
||||
for (SubCommandBuilder subBuilder : subCommandBuilders) {
|
||||
CommandNode subNode = subBuilder.build();
|
||||
node.addSubCommand(subNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package fr.luc.bettermccommands.builder;
|
||||
|
||||
import fr.luc.bettermccommands.BetterMcCommands;
|
||||
import fr.luc.bettermccommands.api.Command;
|
||||
|
||||
/**
|
||||
* Constructeur fluide (Builder) pour déclarer et enregistrer une commande racine Minecraft.
|
||||
*/
|
||||
public class CommandBuilder extends AbstractCommandBuilder<CommandBuilder, Command> {
|
||||
|
||||
/**
|
||||
* Crée un nouveau constructeur de commande racine.
|
||||
*
|
||||
* @param name Le nom principal de la commande.
|
||||
*/
|
||||
public CommandBuilder(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Command build() {
|
||||
Command command = new Command(this.name);
|
||||
applyProperties(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit la commande et l'enregistre immédiatement auprès du gestionnaire {@link BetterMcCommands}.
|
||||
*
|
||||
* @return L'instance de {@link Command} enregistrée.
|
||||
*/
|
||||
public Command register() {
|
||||
Command command = build();
|
||||
BetterMcCommands.getInstance().registerCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit la commande et l'enregistre auprès d'une instance spécifique de {@link BetterMcCommands}.
|
||||
*
|
||||
* @param manager L'instance du gestionnaire.
|
||||
* @return L'instance de {@link Command} enregistrée.
|
||||
*/
|
||||
public Command register(BetterMcCommands manager) {
|
||||
Command command = build();
|
||||
manager.registerCommand(command);
|
||||
return command;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package fr.luc.bettermccommands.builder;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
|
||||
/**
|
||||
* Constructeur fluide (Builder) pour déclarer des sous-commandes imbriquées.
|
||||
*/
|
||||
public class SubCommandBuilder extends AbstractCommandBuilder<SubCommandBuilder, CommandNode> {
|
||||
|
||||
/**
|
||||
* Crée un nouveau constructeur de sous-commande.
|
||||
*
|
||||
* @param name Le nom de la sous-commande.
|
||||
*/
|
||||
public SubCommandBuilder(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandNode build() {
|
||||
CommandNode node = new CommandNode(this.name);
|
||||
applyProperties(node);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package fr.luc.bettermccommands.cooldown;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Gère les temps de recharge (Cooldowns) par commande et par joueur.
|
||||
*/
|
||||
public class CooldownManager {
|
||||
|
||||
private final Map<String, Map<UUID, Instant>> cooldowns = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Vérifie si l'émetteur est actuellement soumis à un temps de recharge pour le nœud de commande donné.
|
||||
*
|
||||
* @param node Le nœud de commande.
|
||||
* @param sender L'émetteur testé.
|
||||
* @return true si l'émetteur est en cooldown, sinon false.
|
||||
*/
|
||||
public boolean isOnCooldown(CommandNode node, CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
return false; // La console n'a pas de cooldown
|
||||
}
|
||||
if (node.getCooldown() == null || node.getCooldown().isZero() || node.getCooldown().isNegative()) {
|
||||
return false;
|
||||
}
|
||||
if (node.getCooldownBypassPermission() != null && player.hasPermission(node.getCooldownBypassPermission())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<UUID, Instant> nodeMap = cooldowns.get(node.getFullName());
|
||||
if (nodeMap == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Instant expiration = nodeMap.get(player.getUniqueId());
|
||||
if (expiration == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Instant.now().isAfter(expiration)) {
|
||||
nodeMap.remove(player.getUniqueId());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le temps restant avant expiration du cooldown pour un émetteur.
|
||||
*
|
||||
* @param node Le nœud de commande.
|
||||
* @param sender L'émetteur testé.
|
||||
* @return La durée restante, ou {@link Duration#ZERO} si aucun cooldown n'est actif.
|
||||
*/
|
||||
public Duration getRemainingCooldown(CommandNode node, CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
|
||||
Map<UUID, Instant> nodeMap = cooldowns.get(node.getFullName());
|
||||
if (nodeMap == null) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
|
||||
Instant expiration = nodeMap.get(player.getUniqueId());
|
||||
if (expiration == null) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
|
||||
Instant now = Instant.now();
|
||||
if (now.isAfter(expiration)) {
|
||||
nodeMap.remove(player.getUniqueId());
|
||||
return Duration.ZERO;
|
||||
}
|
||||
|
||||
return Duration.between(now, expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique le temps de recharge configuré sur le nœud à l'émetteur.
|
||||
*
|
||||
* @param node Le nœud de commande.
|
||||
* @param sender L'émetteur.
|
||||
*/
|
||||
public void applyCooldown(CommandNode node, CommandSender sender) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
Duration duration = node.getCooldown();
|
||||
if (duration == null || duration.isZero() || duration.isNegative()) {
|
||||
return;
|
||||
}
|
||||
|
||||
cooldowns.computeIfAbsent(node.getFullName(), k -> new ConcurrentHashMap<>())
|
||||
.put(player.getUniqueId(), Instant.now().plus(duration));
|
||||
}
|
||||
|
||||
/**
|
||||
* Réinitialise / supprime le cooldown d'un joueur pour une commande donnée.
|
||||
*
|
||||
* @param node Le nœud de commande.
|
||||
* @param player Le joueur.
|
||||
*/
|
||||
public void resetCooldown(CommandNode node, Player player) {
|
||||
Map<UUID, Instant> nodeMap = cooldowns.get(node.getFullName());
|
||||
if (nodeMap != null) {
|
||||
nodeMap.remove(player.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie l'ensemble des temps de recharge enregistrés.
|
||||
*/
|
||||
public void clearAll() {
|
||||
cooldowns.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package fr.luc.bettermccommands.demo;
|
||||
|
||||
import fr.luc.bettermccommands.BetterMcCommands;
|
||||
import fr.luc.bettermccommands.demo.commands.DemoBaseCommand;
|
||||
import fr.luc.bettermccommands.demo.commands.DemoListener;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
/**
|
||||
* Exemple de plugin Bukkit/Paper intégrant la bibliothèque betterMcCommands.
|
||||
*/
|
||||
public class DemoPlugin extends JavaPlugin {
|
||||
|
||||
private BetterMcCommands commandsManager;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
getLogger().info("Initialisation de betterMcCommands Demo...");
|
||||
|
||||
// 1. Initialisation du gestionnaire pour ce plugin
|
||||
this.commandsManager = BetterMcCommands.create(this);
|
||||
|
||||
// 2. Enregistrement des écouteurs d'événements de commandes
|
||||
this.commandsManager.registerListeners(new DemoListener());
|
||||
|
||||
// 3. Enregistrement de la commande de démonstration /commande-demo
|
||||
DemoBaseCommand.create().register();
|
||||
|
||||
getLogger().info("betterMcCommands Demo activé avec succès !");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
getLogger().info("Désactivation de betterMcCommands Demo...");
|
||||
|
||||
// Nettoyage et désenregistrement à chaud de toutes les commandes
|
||||
if (commandsManager != null) {
|
||||
commandsManager.unregisterAll();
|
||||
}
|
||||
|
||||
getLogger().info("betterMcCommands Demo désactivé.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package fr.luc.bettermccommands.demo.commands;
|
||||
|
||||
import fr.luc.bettermccommands.BetterMcCommands;
|
||||
import fr.luc.bettermccommands.api.Command;
|
||||
import fr.luc.bettermccommands.argument.Arguments;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Commande de démonstration illustrant la puissance et la flexibilité de betterMcCommands :
|
||||
* - Commande racine avec alias et restrictions
|
||||
* - Sous-commandes simples et imbriquées
|
||||
* - Arguments typés obligatoires et optionnels (Player, Integer, Duration, Greedy String, Enum)
|
||||
* - Cooldowns natifs
|
||||
* - Événements de cycle de vie (onPreExecute, onPermissionDenied, etc.)
|
||||
*/
|
||||
public class DemoBaseCommand {
|
||||
|
||||
public enum Rank {
|
||||
JOUEUR, VIP, MODERATEUR, ADMINISTRATEUR
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit et retourne la commande de démonstration /commande-demo.
|
||||
*
|
||||
* @return La commande prête à être enregistrée.
|
||||
*/
|
||||
public static Command create() {
|
||||
return BetterMcCommands.builder("commande-demo")
|
||||
.description("Commande de démonstration pour betterMcCommands")
|
||||
.aliases("demo", "cdemo")
|
||||
.permission("bettermc.demo")
|
||||
|
||||
// Événement local déclenché avant l'exécution de la commande
|
||||
.onPreExecute(event -> {
|
||||
if (event.isPlayer()) {
|
||||
Player player = event.getPlayer();
|
||||
// Exemple de vérification : simuler un check de combat ou d'état
|
||||
if (event.getContext() != null && event.getContext().hasArgument("bloque")) {
|
||||
event.setCancelled(true);
|
||||
event.reply("<red>Action annulée par le système de sécurité !</red>");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Exécution de la commande racine : /commande-demo
|
||||
.executes(context -> {
|
||||
context.reply("<gradient:#00d2ff:#3a7bd5><bold>=== Démonstration betterMcCommands ===</bold></gradient>");
|
||||
context.reply("<gray>Bienvenue <aqua>" + context.getSender().getName() + "</aqua> !</gray>");
|
||||
context.reply("<yellow>Sous-commandes disponibles :</yellow>");
|
||||
context.reply(" <gold>•</gold> <yellow>/demo give <cible> [quantite]</yellow> <gray>- Donne des items</gray>");
|
||||
context.reply(" <gold>•</gold> <yellow>/demo broadcast <message...></yellow> <gray>- Diffuse une annonce</gray>");
|
||||
context.reply(" <gold>•</gold> <yellow>/demo tempban <joueur> <duree> <raison...></yellow> <gray>- Bannit temporairement</gray>");
|
||||
context.reply(" <gold>•</gold> <yellow>/demo cooldown-test</yellow> <gray>- Teste un cooldown de 10s</gray>");
|
||||
context.reply(" <gold>•</gold> <yellow>/demo admin rank set <joueur> <grade></yellow> <gray>- Sous-commandes imbriquées</gray>");
|
||||
})
|
||||
|
||||
// 1. Sous-commande : /demo give <cible> [quantite]
|
||||
.subcommand(BetterMcCommands.subBuilder("give")
|
||||
.description("Donne des items à un joueur cible")
|
||||
.permission("bettermc.demo.give")
|
||||
.argument(Arguments.player("cible").description("Le joueur qui recevra les items"))
|
||||
.argument(Arguments.integer("quantite", 1, 64).defaultValue(1).description("Nombre d'items (1 à 64)"))
|
||||
.executes(context -> {
|
||||
Player target = context.getTargetPlayer("cible");
|
||||
int amount = context.getInt("quantite");
|
||||
|
||||
context.replySuccess("Attribution de <gold>" + amount + "</gold> ressource(s) à <aqua>" + target.getName() + "</aqua> !");
|
||||
target.sendMessage("§aVous avez reçu §6" + amount + " §aresource(s) de la part de §b" + context.getSender().getName() + "§a.");
|
||||
})
|
||||
)
|
||||
|
||||
// 2. Sous-commande : /demo broadcast <message...>
|
||||
.subcommand(BetterMcCommands.subBuilder("broadcast")
|
||||
.description("Diffuse un message global à tous les joueurs du serveur")
|
||||
.aliases("bc", "annonce")
|
||||
.permission("bettermc.demo.broadcast")
|
||||
.argument(Arguments.greedyString("message").description("Le message complet à diffuser"))
|
||||
.executes(context -> {
|
||||
String message = context.getString("message");
|
||||
context.replySuccess("Diffusion de l'annonce : <yellow>" + message + "</yellow>");
|
||||
})
|
||||
)
|
||||
|
||||
// 3. Sous-commande : /demo tempban <joueur> <duree> <raison...>
|
||||
.subcommand(BetterMcCommands.subBuilder("tempban")
|
||||
.description("Sanctionne temporairement un joueur")
|
||||
.permission("bettermc.demo.tempban")
|
||||
.argument(Arguments.offlinePlayer("joueur").description("Le joueur à sanctionner"))
|
||||
.argument(Arguments.duration("duree").description("Durée de la sanction (ex: 30m, 2h, 7d)"))
|
||||
.argument(Arguments.greedyString("raison").defaultValue("Infraction aux règles").description("Motif du bannissement"))
|
||||
.executes(context -> {
|
||||
var target = context.get("joueur", org.bukkit.OfflinePlayer.class);
|
||||
Duration duration = context.getDuration("duree");
|
||||
String reason = context.getString("raison");
|
||||
|
||||
context.replySuccess("Joueur <red>" + target.getName() + "</red> banni pour <gold>" +
|
||||
duration.toMinutes() + " minutes</gold>. Motif : <yellow>" + reason + "</yellow>.");
|
||||
})
|
||||
)
|
||||
|
||||
// 4. Sous-commande avec Cooldown : /demo cooldown-test
|
||||
.subcommand(BetterMcCommands.subBuilder("cooldown-test")
|
||||
.description("Teste le système de cooldown (10 secondes)")
|
||||
.cooldown(Duration.ofSeconds(10))
|
||||
.cooldownBypass("bettermc.bypass.cooldown")
|
||||
.playerOnly()
|
||||
.executes(context -> {
|
||||
context.reply("<green>Cooldown validé ! Vous venez d'exécuter la commande. Réessayez immédiatement pour tester le blocage.</green>");
|
||||
})
|
||||
)
|
||||
|
||||
// 5. Arborescence imbriquée : /demo admin rank set <joueur> <grade>
|
||||
.subcommand(BetterMcCommands.subBuilder("admin")
|
||||
.description("Commandes administratives")
|
||||
.permission("bettermc.admin")
|
||||
.subcommand(BetterMcCommands.subBuilder("rank")
|
||||
.description("Gestion des rangs des joueurs")
|
||||
.subcommand(BetterMcCommands.subBuilder("set")
|
||||
.description("Définit le rang d'un joueur")
|
||||
.argument(Arguments.player("joueur"))
|
||||
.argument(Arguments.enumOf("grade", Rank.class))
|
||||
.executes(context -> {
|
||||
Player target = context.getTargetPlayer("joueur");
|
||||
Rank rank = context.get("grade", Rank.class);
|
||||
|
||||
context.replySuccess("Le rang de <aqua>" + target.getName() + "</aqua> a été défini sur <gold>" + rank.name() + "</gold>.");
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.luc.bettermccommands.demo.commands;
|
||||
|
||||
import fr.luc.bettermccommands.event.*;
|
||||
import fr.luc.bettermccommands.event.annotation.CommandEventHandler;
|
||||
|
||||
/**
|
||||
* Exemple de classe d'écouteurs d'événements de commandes utilisant l'annotation {@link CommandEventHandler}.
|
||||
*/
|
||||
public class DemoListener {
|
||||
|
||||
/**
|
||||
* Intercepte toutes les commandes avant leur exécution.
|
||||
*
|
||||
* @param event L'événement de pré-exécution.
|
||||
*/
|
||||
@CommandEventHandler(priority = 10)
|
||||
public void onAnyCommandPreExecute(CommandPreExecuteEvent event) {
|
||||
System.out.println("[betterMcCommands Log] Exécution demandée pour : /" + event.getNode().getFullName() +
|
||||
" par " + event.getSender().getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepte spécifiquement les événements sur la commande 'commande-demo'.
|
||||
*
|
||||
* @param event L'événement de post-exécution.
|
||||
*/
|
||||
@CommandEventHandler(command = "commande-demo")
|
||||
public void onDemoPostExecute(CommandPostExecuteEvent event) {
|
||||
if (event.isSuccessful()) {
|
||||
System.out.println("[betterMcCommands Metric] /" + event.getNode().getFullName() +
|
||||
" exécutée en " + event.getExecutionDuration().toMillis() + "ms.");
|
||||
} else {
|
||||
System.err.println("[betterMcCommands Alert] Échec d'exécution sur /" + event.getNode().getFullName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepte les refus de permission pour personnaliser le message ou jouer un son.
|
||||
*
|
||||
* @param event L'événement de permission refusée.
|
||||
*/
|
||||
@CommandEventHandler
|
||||
public void onPermissionDenied(CommandPermissionDeniedEvent event) {
|
||||
if (event.getRequiredPermission() != null) {
|
||||
event.setCustomErrorMessage("<dark_red><bold>Accès Restreint</bold></dark_red> : Permission requise <gray>[" +
|
||||
event.getRequiredPermission() + "]</gray>.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepte les tentatives d'exécution sous cooldown.
|
||||
*
|
||||
* @param event L'événement de cooldown.
|
||||
*/
|
||||
@CommandEventHandler
|
||||
public void onCooldown(CommandCooldownEvent event) {
|
||||
long seconds = event.getRemainingCooldown().toSeconds();
|
||||
event.setCustomMessage("<red>⏳ Doucement ! Vous devez encore attendre <gold>" +
|
||||
(seconds > 0 ? seconds + "s" : event.getRemainingCooldown().toMillis() + "ms") +
|
||||
"</gold> avant de réutiliser <yellow>/" + event.getNode().getFullName() + "</yellow>.</red>");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package fr.luc.bettermccommands.event;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.event.Cancellable;
|
||||
|
||||
/**
|
||||
* Classe de base pour les événements de commande pouvant être annulés par les écouteurs.
|
||||
*/
|
||||
public abstract class CancellableCommandEvent extends CommandEvent implements Cancellable {
|
||||
|
||||
private boolean cancelled = false;
|
||||
|
||||
/**
|
||||
* Crée un événement annulable.
|
||||
*
|
||||
* @param node Le nœud de commande.
|
||||
* @param sender L'émetteur.
|
||||
* @param context Le contexte.
|
||||
*/
|
||||
public CancellableCommandEvent(CommandNode node, CommandSender sender, CommandContext context) {
|
||||
super(node, sender, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCancelled(boolean cancel) {
|
||||
this.cancelled = cancel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package fr.luc.bettermccommands.event;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Événement déclenché lorsqu'un joueur tente d'exécuter une commande soumise à un temps de recharge (Cooldown) actif.
|
||||
* <p>
|
||||
* Si l'événement est annulé ({@code setCancelled(true)}), l'exécution de la commande est autorisée exceptionnellement.
|
||||
*/
|
||||
public class CommandCooldownEvent extends CancellableCommandEvent {
|
||||
|
||||
private final Duration remainingCooldown;
|
||||
private String customMessage;
|
||||
|
||||
/**
|
||||
* Crée un événement de cooldown de commande.
|
||||
*
|
||||
* @param node Le nœud de commande.
|
||||
* @param sender L'émetteur sous cooldown.
|
||||
* @param context Le contexte de la commande.
|
||||
* @param remainingCooldown La durée restante avant expiration.
|
||||
*/
|
||||
public CommandCooldownEvent(CommandNode node, CommandSender sender, CommandContext context, Duration remainingCooldown) {
|
||||
super(node, sender, context);
|
||||
this.remainingCooldown = remainingCooldown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La durée restante avant la fin du cooldown.
|
||||
*/
|
||||
public Duration getRemainingCooldown() {
|
||||
return remainingCooldown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le message personnalisé éventuel, ou {@code null}.
|
||||
*/
|
||||
public String getCustomMessage() {
|
||||
return customMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit un message d'avertissement de cooldown sur-mesure.
|
||||
*
|
||||
* @param customMessage Le message au format MiniMessage ou couleurs Minecraft.
|
||||
*/
|
||||
public void setCustomMessage(String customMessage) {
|
||||
this.customMessage = customMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package fr.luc.bettermccommands.event;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Classe de base pour tous les événements du cycle de vie d'une commande.
|
||||
*/
|
||||
public abstract class CommandEvent {
|
||||
|
||||
private final CommandNode node;
|
||||
private final CommandSender sender;
|
||||
private final CommandContext context;
|
||||
|
||||
/**
|
||||
* Crée un nouvel événement de commande.
|
||||
*
|
||||
* @param node Le nœud de commande ciblé.
|
||||
* @param sender L'émetteur de la commande.
|
||||
* @param context Le contexte d'exécution (peut être partiel ou null).
|
||||
*/
|
||||
public CommandEvent(CommandNode node, CommandSender sender, CommandContext context) {
|
||||
this.node = Objects.requireNonNull(node, "node cannot be null");
|
||||
this.sender = Objects.requireNonNull(sender, "sender cannot be null");
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le nœud de commande concerné par cet événement.
|
||||
*/
|
||||
public CommandNode getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'émetteur ayant invoqué la commande.
|
||||
*/
|
||||
public CommandSender getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si l'émetteur est un joueur.
|
||||
*/
|
||||
public boolean isPlayer() {
|
||||
return sender instanceof Player;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le joueur émetteur.
|
||||
* @throws IllegalStateException si l'émetteur n'est pas un joueur.
|
||||
*/
|
||||
public Player getPlayer() {
|
||||
if (!isPlayer()) {
|
||||
throw new IllegalStateException("CommandSender is not a player: " + sender.getClass().getSimpleName());
|
||||
}
|
||||
return (Player) sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le contexte d'exécution s'il est déjà instancié, sinon {@code null}.
|
||||
*/
|
||||
public CommandContext getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une réponse textuelle formatée à l'émetteur de la commande.
|
||||
*
|
||||
* @param miniMessageText Le message au format MiniMessage ou couleurs Minecraft.
|
||||
*/
|
||||
public void reply(String miniMessageText) {
|
||||
if (context != null) {
|
||||
context.reply(miniMessageText);
|
||||
} else {
|
||||
sender.sendMessage(miniMessageText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package fr.luc.bettermccommands.event;
|
||||
|
||||
/**
|
||||
* Interface fonctionnelle pour écouter un événement de commande typé.
|
||||
*
|
||||
* @param <T> Le type d'événement écouté.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandEventListener<T extends CommandEvent> {
|
||||
|
||||
/**
|
||||
* Invoqué lorsque l'événement de commande survient.
|
||||
*
|
||||
* @param event L'instance de l'événement.
|
||||
*/
|
||||
void onEvent(T event);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package fr.luc.bettermccommands.event;
|
||||
|
||||
import fr.luc.bettermccommands.event.annotation.CommandEventHandler;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* Gestionnaire et bus central des événements de commande.
|
||||
* Supporte à la fois l'enregistrement fonctionnel (lambdas) et déclaratif (classes annotées avec {@link CommandEventHandler}).
|
||||
*/
|
||||
public class CommandEventManager {
|
||||
|
||||
private record ListenerRegistration(Class<?> eventType, CommandEventListener<CommandEvent> listener, String commandFilter, int priority) {}
|
||||
|
||||
private final List<ListenerRegistration> registrations = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
* Enregistre un écouteur fonctionnel pour un type d'événement donné.
|
||||
*
|
||||
* @param eventType Le type d'événement à écouter.
|
||||
* @param listener Le consommateur de l'événement.
|
||||
* @param <T> Le type de l'événement.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends CommandEvent> void register(Class<T> eventType, CommandEventListener<T> listener) {
|
||||
register(eventType, listener, null, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre un écouteur fonctionnel avec filtre sur le nom de commande et priorité.
|
||||
*
|
||||
* @param eventType Le type d'événement.
|
||||
* @param listener Le callback.
|
||||
* @param commandFilter Le filtre sur le nom de la commande (ou null).
|
||||
* @param priority La priorité d'exécution.
|
||||
* @param <T> Le type de l'événement.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends CommandEvent> void register(Class<T> eventType, CommandEventListener<T> listener, String commandFilter, int priority) {
|
||||
Objects.requireNonNull(eventType, "eventType cannot be null");
|
||||
Objects.requireNonNull(listener, "listener cannot be null");
|
||||
|
||||
registrations.add(new ListenerRegistration(
|
||||
eventType,
|
||||
(CommandEventListener<CommandEvent>) listener,
|
||||
commandFilter != null && !commandFilter.isEmpty() ? commandFilter.toLowerCase() : null,
|
||||
priority
|
||||
));
|
||||
sortRegistrations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse une instance d'écouteur et enregistre toutes ses méthodes annotées avec {@link CommandEventHandler}.
|
||||
*
|
||||
* @param listenerInstance L'instance de la classe contenant des méthodes annotées.
|
||||
*/
|
||||
public void registerListeners(Object listenerInstance) {
|
||||
Objects.requireNonNull(listenerInstance, "listenerInstance cannot be null");
|
||||
|
||||
for (Method method : listenerInstance.getClass().getDeclaredMethods()) {
|
||||
if (!method.isAnnotationPresent(CommandEventHandler.class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CommandEventHandler annotation = method.getAnnotation(CommandEventHandler.class);
|
||||
Class<?>[] params = method.getParameterTypes();
|
||||
if (params.length != 1 || !CommandEvent.class.isAssignableFrom(params[0])) {
|
||||
throw new IllegalArgumentException("Method " + method.getName() + " in " +
|
||||
listenerInstance.getClass().getName() + " must have exactly 1 parameter extending CommandEvent.");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends CommandEvent> eventType = (Class<? extends CommandEvent>) params[0];
|
||||
method.setAccessible(true);
|
||||
|
||||
CommandEventListener<CommandEvent> listener = event -> {
|
||||
try {
|
||||
method.invoke(listenerInstance, event);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[betterMcCommands] Error dispatching event " + event.getClass().getSimpleName() +
|
||||
" to listener " + listenerInstance.getClass().getSimpleName() + "#" + method.getName());
|
||||
e.printStackTrace();
|
||||
}
|
||||
};
|
||||
|
||||
registrations.add(new ListenerRegistration(
|
||||
eventType,
|
||||
listener,
|
||||
annotation.command().isEmpty() ? null : annotation.command().toLowerCase(),
|
||||
annotation.priority()
|
||||
));
|
||||
}
|
||||
sortRegistrations();
|
||||
}
|
||||
|
||||
/**
|
||||
* Déclenche un événement et notifie tous les écouteurs enregistrés correspondants.
|
||||
*
|
||||
* @param event L'événement à diffuser.
|
||||
*/
|
||||
public void dispatch(CommandEvent event) {
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String cmdName = event.getNode().getRootName().toLowerCase();
|
||||
|
||||
for (ListenerRegistration reg : registrations) {
|
||||
if (!reg.eventType().isInstance(event)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reg.commandFilter() != null && !reg.commandFilter().equalsIgnoreCase(cmdName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
reg.listener().onEvent(event);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[betterMcCommands] Exception in event listener for " + event.getClass().getSimpleName() + ": " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime tous les écouteurs enregistrés.
|
||||
*/
|
||||
public void unregisterAll() {
|
||||
registrations.clear();
|
||||
}
|
||||
|
||||
private void sortRegistrations() {
|
||||
registrations.sort(Comparator.comparingInt(ListenerRegistration::priority));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package fr.luc.bettermccommands.event;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
import fr.luc.bettermccommands.api.CommandSenderType;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
/**
|
||||
* Événement déclenché lorsqu'un émetteur tente d'exécuter une commande sans disposer de la permission requise
|
||||
* ou sans avoir le type d'émetteur attendu (ex: console voulant exécuter une commande réservée aux joueurs).
|
||||
* <p>
|
||||
* Si l'événement est annulé ({@code setCancelled(true)}), le message d'erreur par défaut n'est pas envoyé,
|
||||
* ce qui permet d'afficher un message ou de jouer un son personnalisé.
|
||||
*/
|
||||
public class CommandPermissionDeniedEvent extends CancellableCommandEvent {
|
||||
|
||||
private final String requiredPermission;
|
||||
private final CommandSenderType requiredSenderType;
|
||||
private String customErrorMessage;
|
||||
|
||||
/**
|
||||
* Crée l'événement de refus de permission / restriction d'émetteur.
|
||||
*
|
||||
* @param node Le nœud ciblé.
|
||||
* @param sender L'émetteur bloqué.
|
||||
* @param context Le contexte d'exécution (peut être null).
|
||||
* @param requiredPermission La permission manquante (ou null).
|
||||
* @param requiredSenderType Le type d'émetteur requis.
|
||||
*/
|
||||
public CommandPermissionDeniedEvent(CommandNode node, CommandSender sender, CommandContext context,
|
||||
String requiredPermission, CommandSenderType requiredSenderType) {
|
||||
super(node, sender, context);
|
||||
this.requiredPermission = requiredPermission;
|
||||
this.requiredSenderType = requiredSenderType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La permission manquante, ou {@code null} s'il s'agit d'une restriction de type d'émetteur.
|
||||
*/
|
||||
public String getRequiredPermission() {
|
||||
return requiredPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le type d'émetteur requis par la commande.
|
||||
*/
|
||||
public CommandSenderType getRequiredSenderType() {
|
||||
return requiredSenderType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le message d'erreur personnalisé à envoyer, ou {@code null} pour le message par défaut.
|
||||
*/
|
||||
public String getCustomErrorMessage() {
|
||||
return customErrorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit un message d'erreur sur-mesure à envoyer au joueur.
|
||||
*
|
||||
* @param customErrorMessage Le message formaté.
|
||||
*/
|
||||
public void setCustomErrorMessage(String customErrorMessage) {
|
||||
this.customErrorMessage = customErrorMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package fr.luc.bettermccommands.event;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
import fr.luc.bettermccommands.api.CommandResult;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Événement déclenché après l'exécution d'une commande (avec succès ou échec).
|
||||
* Utile pour les métriques, les journaux d'audit (logging) et les statistiques d'utilisation.
|
||||
*/
|
||||
public class CommandPostExecuteEvent extends CommandEvent {
|
||||
|
||||
private final CommandResult result;
|
||||
private final Duration executionDuration;
|
||||
private final Throwable exception;
|
||||
|
||||
/**
|
||||
* Crée un événement de post-exécution.
|
||||
*
|
||||
* @param node Le nœud exécuté.
|
||||
* @param sender L'émetteur.
|
||||
* @param context Le contexte de la commande.
|
||||
* @param result Le résultat final.
|
||||
* @param executionDuration La durée totale d'exécution.
|
||||
* @param exception L'exception éventuelle survenue lors de l'exécution (ou null).
|
||||
*/
|
||||
public CommandPostExecuteEvent(CommandNode node, CommandSender sender, CommandContext context,
|
||||
CommandResult result, Duration executionDuration, Throwable exception) {
|
||||
super(node, sender, context);
|
||||
this.result = result;
|
||||
this.executionDuration = executionDuration;
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le résultat d'exécution de la commande.
|
||||
*/
|
||||
public CommandResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La durée nécessaire au traitement de la commande.
|
||||
*/
|
||||
public Duration getExecutionDuration() {
|
||||
return executionDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'exception levée lors de l'exécution, ou {@code null} en cas de succès.
|
||||
*/
|
||||
public Throwable getException() {
|
||||
return exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true si la commande s'est exécutée avec succès sans lever d'exception.
|
||||
*/
|
||||
public boolean isSuccessful() {
|
||||
return result == CommandResult.SUCCESS && exception == null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.luc.bettermccommands.event;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
/**
|
||||
* Événement déclenché juste avant l'exécution du gestionnaire métier de la commande.
|
||||
* Permet d'annuler la commande, de vérifier des pré-conditions (état de combat, économie, inventaire) ou d'injecter des métadonnées.
|
||||
*/
|
||||
public class CommandPreExecuteEvent extends CancellableCommandEvent {
|
||||
|
||||
/**
|
||||
* Construit l'événement de pré-exécution.
|
||||
*
|
||||
* @param node Le nœud de commande sur le point d'être exécuté.
|
||||
* @param sender L'émetteur.
|
||||
* @param context Le contexte complet avec les arguments résolus.
|
||||
*/
|
||||
public CommandPreExecuteEvent(CommandNode node, CommandSender sender, CommandContext context) {
|
||||
super(node, sender, context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package fr.luc.bettermccommands.event;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
/**
|
||||
* Événement déclenché lorsqu'une commande est appelée avec une syntaxe incorrecte
|
||||
* (arguments obligatoires manquants, sous-commande inexistante ou erreur de conversion d'argument).
|
||||
* <p>
|
||||
* Si l'événement est annulé ({@code setCancelled(true)}), le message d'aide automatique n'est pas envoyé.
|
||||
*/
|
||||
public class CommandSyntaxErrorEvent extends CancellableCommandEvent {
|
||||
|
||||
private final String errorReason;
|
||||
private final String usage;
|
||||
private String customMessage;
|
||||
|
||||
/**
|
||||
* Crée un événement d'erreur de syntaxe.
|
||||
*
|
||||
* @param node Le nœud de commande concerné.
|
||||
* @param sender L'émetteur.
|
||||
* @param context Le contexte de commande (peut être null ou partiel).
|
||||
* @param errorReason L'explication de l'erreur.
|
||||
* @param usage La syntaxe attendue (ex: "/demo give <joueur> [quantite]").
|
||||
*/
|
||||
public CommandSyntaxErrorEvent(CommandNode node, CommandSender sender, CommandContext context,
|
||||
String errorReason, String usage) {
|
||||
super(node, sender, context);
|
||||
this.errorReason = errorReason;
|
||||
this.usage = usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La raison détaillée de l'erreur de syntaxe.
|
||||
*/
|
||||
public String getErrorReason() {
|
||||
return errorReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La ligne d'usage correcte pour cette commande.
|
||||
*/
|
||||
public String getUsage() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le message d'erreur personnalisé à envoyer, ou {@code null}.
|
||||
*/
|
||||
public String getCustomMessage() {
|
||||
return customMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit un message personnalisé à la place de l'aide par défaut.
|
||||
*
|
||||
* @param customMessage Le message personnalisé.
|
||||
*/
|
||||
public void setCustomMessage(String customMessage) {
|
||||
this.customMessage = customMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package fr.luc.bettermccommands.event;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.api.CommandNode;
|
||||
import fr.luc.bettermccommands.api.suggestion.Suggestion;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Événement déclenché lors du calcul des suggestions d'auto-complétion (Tab-Complete).
|
||||
* Permet d'ajouter, modifier, filtrer ou réordonner les suggestions renvoyées au client.
|
||||
*/
|
||||
public class CommandTabCompleteEvent extends CancellableCommandEvent {
|
||||
|
||||
private final List<Suggestion> suggestions;
|
||||
private final String currentInput;
|
||||
private final int argumentIndex;
|
||||
|
||||
/**
|
||||
* Crée l'événement de complétion.
|
||||
*
|
||||
* @param node Le nœud de commande concerné.
|
||||
* @param sender L'émetteur recevant les suggestions.
|
||||
* @param context Le contexte partiel.
|
||||
* @param suggestions La liste modifiable des suggestions calculées.
|
||||
* @param currentInput La chaîne tapée en cours.
|
||||
* @param argumentIndex L'index de l'argument complété.
|
||||
*/
|
||||
public CommandTabCompleteEvent(CommandNode node, CommandSender sender, CommandContext context,
|
||||
List<Suggestion> suggestions, String currentInput, int argumentIndex) {
|
||||
super(node, sender, context);
|
||||
this.suggestions = new ArrayList<>(suggestions);
|
||||
this.currentInput = currentInput != null ? currentInput : "";
|
||||
this.argumentIndex = argumentIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La liste modifiable des suggestions qui seront retournées au client.
|
||||
*/
|
||||
public List<Suggestion> getSuggestions() {
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une suggestion simple à la liste.
|
||||
*
|
||||
* @param value La chaîne suggérée.
|
||||
*/
|
||||
public void addSuggestion(String value) {
|
||||
this.suggestions.add(Suggestion.of(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute une suggestion avec infobulle à la liste.
|
||||
*
|
||||
* @param value Le texte inséré.
|
||||
* @param tooltip L'infobulle affichée.
|
||||
*/
|
||||
public void addSuggestion(String value, String tooltip) {
|
||||
this.suggestions.add(Suggestion.of(value, tooltip));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Le mot en cours de saisie par le joueur.
|
||||
*/
|
||||
public String getCurrentInput() {
|
||||
return currentInput;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return L'index de l'argument en cours de frappe.
|
||||
*/
|
||||
public int getArgumentIndex() {
|
||||
return argumentIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package fr.luc.bettermccommands.event.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annote une méthode d'une classe d'écoute pour recevoir les événements de commande betterMcCommands.
|
||||
* La méthode doit accepter un unique paramètre héritant de {@link fr.luc.bettermccommands.event.CommandEvent}.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CommandEventHandler {
|
||||
|
||||
/**
|
||||
* Filtre optionnel sur le nom racine de la commande (ex: "commande-demo").
|
||||
* Si laissé vide (""), la méthode recevra les événements de toutes les commandes.
|
||||
*
|
||||
* @return Le nom de commande filtré ou vide pour tous.
|
||||
*/
|
||||
String command() default "";
|
||||
|
||||
/**
|
||||
* Priorité d'exécution de l'écouteur (les valeurs plus petites sont exécutées en premier).
|
||||
*
|
||||
* @return La priorité numérique (défaut: 0).
|
||||
*/
|
||||
int priority() default 0;
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
package fr.luc.bettermccommands.platform;
|
||||
|
||||
import fr.luc.bettermccommands.BetterMcCommands;
|
||||
import fr.luc.bettermccommands.api.*;
|
||||
import fr.luc.bettermccommands.api.suggestion.Suggestion;
|
||||
import fr.luc.bettermccommands.argument.CommandArgument;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
import fr.luc.bettermccommands.event.*;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Moteur central de routage, de validation, de parsing et d'exécution des commandes.
|
||||
*/
|
||||
public class CommandDispatcher {
|
||||
|
||||
private final BetterMcCommands manager;
|
||||
|
||||
/**
|
||||
* Crée un nouveau dispatcher associé à un gestionnaire {@link BetterMcCommands}.
|
||||
*
|
||||
* @param manager L'instance du gestionnaire.
|
||||
*/
|
||||
public CommandDispatcher(BetterMcCommands manager) {
|
||||
this.manager = Objects.requireNonNull(manager, "manager cannot be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute une commande racine avec les arguments bruts fournis.
|
||||
*
|
||||
* @param rootCommand La commande racine.
|
||||
* @param sender L'émetteur de la commande.
|
||||
* @param label Le label ou alias utilisé.
|
||||
* @param args Les arguments bruts.
|
||||
* @return Le résultat d'exécution {@link CommandResult}.
|
||||
*/
|
||||
public CommandResult execute(Command rootCommand, CommandSender sender, String label, String[] args) {
|
||||
Instant startTime = Instant.now();
|
||||
|
||||
// 1. Résolution de la sous-commande ciblée dans l'arborescence
|
||||
CommandNode targetNode = rootCommand;
|
||||
int argIndex = 0;
|
||||
|
||||
while (argIndex < args.length && targetNode.hasSubCommands()) {
|
||||
String candidate = args[argIndex];
|
||||
CommandNode sub = targetNode.findSubCommand(candidate);
|
||||
if (sub != null) {
|
||||
targetNode = sub;
|
||||
argIndex++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Arguments restants destinés aux paramètres de la commande ciblée
|
||||
String[] remainingArgs = Arrays.copyOfRange(args, argIndex, args.length);
|
||||
|
||||
// 2. Vérification du type d'émetteur (Console vs Player)
|
||||
if (!targetNode.getSenderType().isAllowed(sender)) {
|
||||
CommandPermissionDeniedEvent event = new CommandPermissionDeniedEvent(
|
||||
targetNode, sender, null, null, targetNode.getSenderType());
|
||||
dispatchEvent(targetNode, event);
|
||||
|
||||
if (!event.isCancelled()) {
|
||||
if (event.getCustomErrorMessage() != null) {
|
||||
event.reply(event.getCustomErrorMessage());
|
||||
} else if (targetNode.getSenderType() == CommandSenderType.PLAYER_ONLY) {
|
||||
sender.sendMessage("§cCette commande est réservée aux joueurs en jeu.");
|
||||
} else if (targetNode.getSenderType() == CommandSenderType.CONSOLE_ONLY) {
|
||||
sender.sendMessage("§cCette commande ne peut être exécutée que depuis la console.");
|
||||
}
|
||||
}
|
||||
return CommandResult.PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
// 3. Vérification des permissions
|
||||
if (targetNode.getPermission() != null && !targetNode.getPermission().isEmpty()) {
|
||||
if (!sender.hasPermission(targetNode.getPermission())) {
|
||||
CommandPermissionDeniedEvent event = new CommandPermissionDeniedEvent(
|
||||
targetNode, sender, null, targetNode.getPermission(), targetNode.getSenderType());
|
||||
dispatchEvent(targetNode, event);
|
||||
|
||||
if (!event.isCancelled()) {
|
||||
if (event.getCustomErrorMessage() != null) {
|
||||
event.reply(event.getCustomErrorMessage());
|
||||
} else {
|
||||
sender.sendMessage("§cVous n'avez pas la permission requise (§7" + targetNode.getPermission() + "§c).");
|
||||
}
|
||||
}
|
||||
return CommandResult.PERMISSION_DENIED;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Vérification du temps de recharge (Cooldown)
|
||||
if (manager.getCooldownManager().isOnCooldown(targetNode, sender)) {
|
||||
Duration remaining = manager.getCooldownManager().getRemainingCooldown(targetNode, sender);
|
||||
CommandCooldownEvent event = new CommandCooldownEvent(targetNode, sender, null, remaining);
|
||||
dispatchEvent(targetNode, event);
|
||||
|
||||
if (!event.isCancelled()) {
|
||||
if (event.getCustomMessage() != null) {
|
||||
event.reply(event.getCustomMessage());
|
||||
} else {
|
||||
long seconds = remaining.toSeconds();
|
||||
String timeStr = seconds > 0 ? seconds + "s" : remaining.toMillis() + "ms";
|
||||
sender.sendMessage("§cVeuillez patienter §6" + timeStr + " §cavant de réutiliser cette commande.");
|
||||
}
|
||||
return CommandResult.COOLDOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Analyse et parsing des arguments
|
||||
Map<String, Object> parsedArguments = new LinkedHashMap<>();
|
||||
CommandContext partialContext = new CommandContext(sender, label, args, parsedArguments);
|
||||
|
||||
List<CommandArgument<?>> expectedArgs = targetNode.getArguments();
|
||||
int expectedIndex = 0;
|
||||
int remainingIndex = 0;
|
||||
|
||||
while (expectedIndex < expectedArgs.size()) {
|
||||
CommandArgument<?> arg = expectedArgs.get(expectedIndex);
|
||||
|
||||
if (arg.isGreedy()) {
|
||||
if (remainingIndex >= remainingArgs.length) {
|
||||
if (!arg.isOptional()) {
|
||||
return handleSyntaxError(targetNode, sender, partialContext,
|
||||
"Argument gourmand manquant : " + arg.getName(), targetNode.getUsage());
|
||||
} else {
|
||||
parsedArguments.put(arg.getName(), arg.getDefaultValue());
|
||||
}
|
||||
} else {
|
||||
String greedyInput = String.join(" ", Arrays.copyOfRange(remainingArgs, remainingIndex, remainingArgs.length));
|
||||
try {
|
||||
Object parsed = parseArgumentValue(arg, greedyInput, partialContext);
|
||||
parsedArguments.put(arg.getName(), parsed);
|
||||
} catch (CommandArgumentParseException e) {
|
||||
return handleSyntaxError(targetNode, sender, partialContext, e.getMessage(), targetNode.getUsage());
|
||||
}
|
||||
remainingIndex = remainingArgs.length; // Tous les arguments sont consommés
|
||||
}
|
||||
expectedIndex++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (remainingIndex >= remainingArgs.length) {
|
||||
if (arg.isOptional()) {
|
||||
parsedArguments.put(arg.getName(), arg.getDefaultValue());
|
||||
} else {
|
||||
return handleSyntaxError(targetNode, sender, partialContext,
|
||||
"Argument obligatoire manquant : <" + arg.getName() + ">", targetNode.getUsage());
|
||||
}
|
||||
} else {
|
||||
String input = remainingArgs[remainingIndex];
|
||||
try {
|
||||
Object parsed = parseArgumentValue(arg, input, partialContext);
|
||||
parsedArguments.put(arg.getName(), parsed);
|
||||
} catch (CommandArgumentParseException e) {
|
||||
return handleSyntaxError(targetNode, sender, partialContext, e.getMessage(), targetNode.getUsage());
|
||||
}
|
||||
remainingIndex++;
|
||||
}
|
||||
expectedIndex++;
|
||||
}
|
||||
|
||||
// Si des arguments surnuméraires ont été passés alors qu'aucun argument greedy n'est défini
|
||||
if (remainingIndex < remainingArgs.length && targetNode.getExecutor() != null && !expectedArgs.isEmpty()
|
||||
&& !expectedArgs.get(expectedArgs.size() - 1).isGreedy()) {
|
||||
// Trop d'arguments fournis
|
||||
return handleSyntaxError(targetNode, sender, partialContext,
|
||||
"Trop d'arguments fournis pour cette commande.", targetNode.getUsage());
|
||||
}
|
||||
|
||||
// Si le nœud n'a pas d'exécuteur mais a des sous-commandes, afficher l'aide des sous-commandes
|
||||
if (targetNode.getExecutor() == null) {
|
||||
sendSubcommandsHelp(targetNode, sender);
|
||||
return CommandResult.SUCCESS;
|
||||
}
|
||||
|
||||
// 6. Contexte final
|
||||
CommandContext fullContext = new CommandContext(sender, label, args, parsedArguments);
|
||||
|
||||
// 7. Événement PreExecute
|
||||
CommandPreExecuteEvent preEvent = new CommandPreExecuteEvent(targetNode, sender, fullContext);
|
||||
dispatchEvent(targetNode, preEvent);
|
||||
|
||||
if (preEvent.isCancelled()) {
|
||||
return CommandResult.CANCELLED;
|
||||
}
|
||||
|
||||
// 8. Exécution du gestionnaire métier
|
||||
Throwable executionError = null;
|
||||
CommandResult result;
|
||||
|
||||
try {
|
||||
targetNode.getExecutor().execute(fullContext);
|
||||
result = CommandResult.SUCCESS;
|
||||
manager.getCooldownManager().applyCooldown(targetNode, sender);
|
||||
} catch (Throwable t) {
|
||||
executionError = t;
|
||||
result = CommandResult.FAILED;
|
||||
sender.sendMessage("§cUne erreur interne est survenue lors de l'exécution de la commande.");
|
||||
t.printStackTrace();
|
||||
}
|
||||
|
||||
// 9. Événement PostExecute
|
||||
Duration duration = Duration.between(startTime, Instant.now());
|
||||
CommandPostExecuteEvent postEvent = new CommandPostExecuteEvent(
|
||||
targetNode, sender, fullContext, result, duration, executionError);
|
||||
dispatchEvent(targetNode, postEvent);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la liste de suggestions pour l'auto-complétion (Tab-Complete).
|
||||
*
|
||||
* @param rootCommand La commande racine.
|
||||
* @param sender L'émetteur.
|
||||
* @param alias L'alias utilisé.
|
||||
* @param args Les arguments en cours de frappe.
|
||||
* @return La liste des chaînes de suggestion pour le client.
|
||||
*/
|
||||
public List<String> tabComplete(Command rootCommand, CommandSender sender, String alias, String[] args) {
|
||||
if (args.length == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
CommandNode targetNode = rootCommand;
|
||||
int argIndex = 0;
|
||||
|
||||
// Navigation dans les sous-commandes
|
||||
while (argIndex < args.length - 1 && targetNode.hasSubCommands()) {
|
||||
String candidate = args[argIndex];
|
||||
CommandNode sub = targetNode.findSubCommand(candidate);
|
||||
if (sub != null) {
|
||||
targetNode = sub;
|
||||
argIndex++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
String currentInput = args[args.length - 1].toLowerCase();
|
||||
int relativeArgIndex = args.length - 1 - argIndex;
|
||||
|
||||
List<Suggestion> rawSuggestions = new ArrayList<>();
|
||||
|
||||
// Si le nœud a des sous-commandes et qu'on est sur le premier mot après le nœud
|
||||
if (relativeArgIndex == 0 && targetNode.hasSubCommands()) {
|
||||
for (CommandNode sub : targetNode.getSubCommands()) {
|
||||
if (sub.getPermission() == null || sender.hasPermission(sub.getPermission())) {
|
||||
if (sub.getName().toLowerCase().startsWith(currentInput)) {
|
||||
rawSuggestions.add(Suggestion.of(sub.getName(), sub.getDescription()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si le nœud a des arguments définis à cet index
|
||||
List<CommandArgument<?>> nodeArgs = targetNode.getArguments();
|
||||
if (relativeArgIndex >= 0 && relativeArgIndex < nodeArgs.size()) {
|
||||
CommandArgument<?> argument = nodeArgs.get(relativeArgIndex);
|
||||
CommandContext partialContext = new CommandContext(sender, alias, args, Collections.emptyMap());
|
||||
rawSuggestions.addAll(argument.getSuggestions(partialContext, currentInput));
|
||||
}
|
||||
|
||||
// Déclenchement de l'événement TabComplete
|
||||
CommandTabCompleteEvent tabEvent = new CommandTabCompleteEvent(
|
||||
targetNode, sender, null, rawSuggestions, currentInput, relativeArgIndex);
|
||||
dispatchEvent(targetNode, tabEvent);
|
||||
|
||||
if (tabEvent.isCancelled()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return tabEvent.getSuggestions().stream()
|
||||
.map(Suggestion::getValue)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T parseArgumentValue(CommandArgument<T> arg, String input, CommandContext context) throws CommandArgumentParseException {
|
||||
return arg.getType().parse(arg.getName(), input, context);
|
||||
}
|
||||
|
||||
private CommandResult handleSyntaxError(CommandNode node, CommandSender sender, CommandContext context,
|
||||
String reason, String usage) {
|
||||
CommandSyntaxErrorEvent event = new CommandSyntaxErrorEvent(node, sender, context, reason, usage);
|
||||
dispatchEvent(node, event);
|
||||
|
||||
if (!event.isCancelled()) {
|
||||
if (event.getCustomMessage() != null) {
|
||||
event.reply(event.getCustomMessage());
|
||||
} else {
|
||||
sender.sendMessage("§cSyntaxe incorrecte : " + reason);
|
||||
sender.sendMessage("§7Utilisation : §e" + usage);
|
||||
}
|
||||
}
|
||||
return CommandResult.SYNTAX_ERROR;
|
||||
}
|
||||
|
||||
private void sendSubcommandsHelp(CommandNode node, CommandSender sender) {
|
||||
sender.sendMessage("§6=== Aide : §e/" + node.getFullName() + " §6===");
|
||||
for (CommandNode sub : node.getSubCommands()) {
|
||||
if (sub.getPermission() == null || sender.hasPermission(sub.getPermission())) {
|
||||
String desc = sub.getDescription().isEmpty() ? "" : " §7- " + sub.getDescription();
|
||||
sender.sendMessage("§e" + sub.getUsage() + desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchEvent(CommandNode node, CommandEvent event) {
|
||||
// 1. Écouteurs locaux sur le nœud
|
||||
if (event instanceof CommandPreExecuteEvent pre) {
|
||||
node.getPreExecuteListeners().forEach(l -> l.onEvent(pre));
|
||||
} else if (event instanceof CommandPostExecuteEvent post) {
|
||||
node.getPostExecuteListeners().forEach(l -> l.onEvent(post));
|
||||
} else if (event instanceof CommandPermissionDeniedEvent perm) {
|
||||
node.getPermissionDeniedListeners().forEach(l -> l.onEvent(perm));
|
||||
} else if (event instanceof CommandSyntaxErrorEvent syn) {
|
||||
node.getSyntaxErrorListeners().forEach(l -> l.onEvent(syn));
|
||||
} else if (event instanceof CommandCooldownEvent cool) {
|
||||
node.getCooldownListeners().forEach(l -> l.onEvent(cool));
|
||||
} else if (event instanceof CommandTabCompleteEvent tab) {
|
||||
node.getTabCompleteListeners().forEach(l -> l.onEvent(tab));
|
||||
}
|
||||
|
||||
// 2. Écouteurs globaux dans le bus d'événements
|
||||
manager.getEventManager().dispatch(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package fr.luc.bettermccommands.platform.paper;
|
||||
|
||||
import fr.luc.bettermccommands.BetterMcCommands;
|
||||
import fr.luc.bettermccommands.api.Command;
|
||||
import fr.luc.bettermccommands.platform.CommandDispatcher;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.command.CommandMap;
|
||||
import org.bukkit.command.SimpleCommandMap;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Gestionnaire d'injection et de retrait dynamique des commandes dans la {@link CommandMap} de Paper / Bukkit.
|
||||
* Permet l'enregistrement à chaud sans nécessiter de redémarrage ni de déclaration dans {@code plugin.yml}.
|
||||
*/
|
||||
public class PaperCommandMapInjector {
|
||||
|
||||
private final Map<String, PaperCommandWrapper> registeredWrappers = new ConcurrentHashMap<>();
|
||||
private CommandMap commandMap;
|
||||
private Map<String, org.bukkit.command.Command> knownCommandsMap;
|
||||
|
||||
/**
|
||||
* Initialise l'injecteur et résout la {@link CommandMap} du serveur.
|
||||
*/
|
||||
public PaperCommandMapInjector() {
|
||||
resolveCommandMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre une commande betterMcCommands auprès du serveur Bukkit/Paper.
|
||||
*
|
||||
* @param command La commande racine à enregistrer.
|
||||
* @param dispatcher Le dispatcher d'exécution.
|
||||
* @param prefix Le préfixe d'enregistrement (ex: nom du plugin ou "bettermc").
|
||||
*/
|
||||
public synchronized void register(Command command, CommandDispatcher dispatcher, String prefix) {
|
||||
if (commandMap == null) {
|
||||
resolveCommandMap();
|
||||
}
|
||||
|
||||
if (commandMap == null) {
|
||||
throw new IllegalStateException("Unable to resolve Bukkit CommandMap for dynamic registration.");
|
||||
}
|
||||
|
||||
PaperCommandWrapper wrapper = new PaperCommandWrapper(command, dispatcher);
|
||||
commandMap.register(prefix != null ? prefix : "bettermc", wrapper);
|
||||
registeredWrappers.put(command.getName().toLowerCase(), wrapper);
|
||||
command.setRegistered(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Désenregistre une commande à chaud du serveur Bukkit/Paper.
|
||||
*
|
||||
* @param command La commande à retirer.
|
||||
*/
|
||||
public synchronized void unregister(Command command) {
|
||||
PaperCommandWrapper wrapper = registeredWrappers.remove(command.getName().toLowerCase());
|
||||
if (wrapper == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrapper.unregister(commandMap);
|
||||
|
||||
if (knownCommandsMap != null) {
|
||||
knownCommandsMap.remove(wrapper.getName().toLowerCase());
|
||||
for (String alias : wrapper.getAliases()) {
|
||||
knownCommandsMap.remove(alias.toLowerCase());
|
||||
}
|
||||
if (wrapper.getLabel() != null) {
|
||||
knownCommandsMap.remove(wrapper.getLabel().toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
command.setRegistered(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Désenregistre l'ensemble des commandes injectées.
|
||||
*/
|
||||
public synchronized void unregisterAll() {
|
||||
for (PaperCommandWrapper wrapper : registeredWrappers.values()) {
|
||||
wrapper.getRootCommand().unregister();
|
||||
}
|
||||
registeredWrappers.clear();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void resolveCommandMap() {
|
||||
Server server = Bukkit.getServer();
|
||||
if (server == null) {
|
||||
return; // Environnement de test unitaire où le serveur Bukkit n'est pas instancié
|
||||
}
|
||||
|
||||
try {
|
||||
// Tentative directe via la méthode getCommandMap() moderne
|
||||
try {
|
||||
Method getCommandMapMethod = server.getClass().getMethod("getCommandMap");
|
||||
this.commandMap = (CommandMap) getCommandMapMethod.invoke(server);
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
// Fallback réflexion sur le champ commandMap de CraftServer
|
||||
Field commandMapField = server.getClass().getDeclaredField("commandMap");
|
||||
commandMapField.setAccessible(true);
|
||||
this.commandMap = (CommandMap) commandMapField.get(server);
|
||||
}
|
||||
|
||||
if (this.commandMap instanceof SimpleCommandMap simpleMap) {
|
||||
Field knownCommandsField = SimpleCommandMap.class.getDeclaredField("knownCommands");
|
||||
knownCommandsField.setAccessible(true);
|
||||
this.knownCommandsMap = (Map<String, org.bukkit.command.Command>) knownCommandsField.get(simpleMap);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("[betterMcCommands] Failed to access Bukkit CommandMap via reflection: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package fr.luc.bettermccommands.platform.paper;
|
||||
|
||||
import fr.luc.bettermccommands.api.Command;
|
||||
import fr.luc.bettermccommands.platform.CommandDispatcher;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Enveloppe Bukkit permettant d'enregistrer une commande {@link Command} betterMcCommands dans le système natif Bukkit/Paper.
|
||||
*/
|
||||
public class PaperCommandWrapper extends org.bukkit.command.Command {
|
||||
|
||||
private final Command rootCommand;
|
||||
private final CommandDispatcher dispatcher;
|
||||
|
||||
/**
|
||||
* Crée l'enveloppe de commande Bukkit.
|
||||
*
|
||||
* @param rootCommand La commande racine betterMcCommands.
|
||||
* @param dispatcher Le dispatcher responsable de l'exécution.
|
||||
*/
|
||||
public PaperCommandWrapper(Command rootCommand, CommandDispatcher dispatcher) {
|
||||
super(rootCommand.getName(), rootCommand.getDescription(), rootCommand.getUsage(), new ArrayList<>(rootCommand.getAliases()));
|
||||
this.rootCommand = rootCommand;
|
||||
this.dispatcher = dispatcher;
|
||||
if (rootCommand.getPermission() != null) {
|
||||
setPermission(rootCommand.getPermission());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return La commande racine betterMcCommands associée.
|
||||
*/
|
||||
public Command getRootCommand() {
|
||||
return rootCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean execute(CommandSender sender, String commandLabel, String[] args) {
|
||||
dispatcher.execute(rootCommand, sender, commandLabel, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException {
|
||||
return dispatcher.tabComplete(rootCommand, sender, alias, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package fr.luc.bettermccommands;
|
||||
|
||||
import fr.luc.bettermccommands.api.CommandContext;
|
||||
import fr.luc.bettermccommands.argument.CommandArgumentParseException;
|
||||
import fr.luc.bettermccommands.argument.type.*;
|
||||
import fr.luc.bettermccommands.mock.SampleRank;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ArgumentParsingTest {
|
||||
|
||||
private CommandContext dummyContext;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
CommandSender sender = Mockito.mock(CommandSender.class);
|
||||
dummyContext = new CommandContext(sender, "test", new String[0], Collections.emptyMap());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("IntegerArgument doit parser les entiers valides et respecter les bornes")
|
||||
void testIntegerArgument() throws Exception {
|
||||
IntegerArgument arg = IntegerArgument.range(1, 10);
|
||||
|
||||
assertEquals(5, arg.parse("count", "5", dummyContext));
|
||||
assertEquals(1, arg.parse("count", "1", dummyContext));
|
||||
assertEquals(10, arg.parse("count", "10", dummyContext));
|
||||
|
||||
assertThrows(CommandArgumentParseException.class, () -> arg.parse("count", "0", dummyContext));
|
||||
assertThrows(CommandArgumentParseException.class, () -> arg.parse("count", "11", dummyContext));
|
||||
assertThrows(CommandArgumentParseException.class, () -> arg.parse("count", "notANumber", dummyContext));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DoubleArgument doit supporter les formats avec point et virgule")
|
||||
void testDoubleArgument() throws Exception {
|
||||
DoubleArgument arg = DoubleArgument.min(0.5);
|
||||
|
||||
assertEquals(1.5, arg.parse("amount", "1.5", dummyContext), 0.001);
|
||||
assertEquals(2.5, arg.parse("amount", "2,5", dummyContext), 0.001);
|
||||
|
||||
assertThrows(CommandArgumentParseException.class, () -> arg.parse("amount", "0.2", dummyContext));
|
||||
assertThrows(CommandArgumentParseException.class, () -> arg.parse("amount", "abc", dummyContext));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("BooleanArgument doit convertir les formats true/false, oui/non, 1/0")
|
||||
void testBooleanArgument() throws Exception {
|
||||
BooleanArgument arg = BooleanArgument.bool();
|
||||
|
||||
assertTrue(arg.parse("flag", "true", dummyContext));
|
||||
assertTrue(arg.parse("flag", "oui", dummyContext));
|
||||
assertTrue(arg.parse("flag", "1", dummyContext));
|
||||
assertTrue(arg.parse("flag", "yes", dummyContext));
|
||||
|
||||
assertFalse(arg.parse("flag", "false", dummyContext));
|
||||
assertFalse(arg.parse("flag", "non", dummyContext));
|
||||
assertFalse(arg.parse("flag", "0", dummyContext));
|
||||
assertFalse(arg.parse("flag", "no", dummyContext));
|
||||
|
||||
assertThrows(CommandArgumentParseException.class, () -> arg.parse("flag", "invalid", dummyContext));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("EnumArgument doit convertir les constantes enum insensiblement à la casse")
|
||||
void testEnumArgument() throws Exception {
|
||||
EnumArgument<SampleRank> arg = EnumArgument.of(SampleRank.class);
|
||||
|
||||
assertEquals(SampleRank.ADMIN, arg.parse("rank", "admin", dummyContext));
|
||||
assertEquals(SampleRank.MEMBER, arg.parse("rank", "MEMBER", dummyContext));
|
||||
assertEquals(SampleRank.VIP, arg.parse("rank", "Vip", dummyContext));
|
||||
|
||||
assertThrows(CommandArgumentParseException.class, () -> arg.parse("rank", "OWNER", dummyContext));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DurationArgument doit parser les chaînes temporelles avec unités variées")
|
||||
void testDurationArgument() throws Exception {
|
||||
DurationArgument arg = DurationArgument.duration();
|
||||
|
||||
assertEquals(Duration.ofSeconds(30), arg.parse("time", "30s", dummyContext));
|
||||
assertEquals(Duration.ofMinutes(15), arg.parse("time", "15m", dummyContext));
|
||||
assertEquals(Duration.ofHours(2), arg.parse("time", "2h", dummyContext));
|
||||
assertEquals(Duration.ofDays(1), arg.parse("time", "1d", dummyContext));
|
||||
assertEquals(Duration.ofSeconds(100), arg.parse("time", "1m 40s", dummyContext));
|
||||
|
||||
assertThrows(CommandArgumentParseException.class, () -> arg.parse("time", "invalid", dummyContext));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package fr.luc.bettermccommands;
|
||||
|
||||
import fr.luc.bettermccommands.api.Command;
|
||||
import fr.luc.bettermccommands.api.CommandResult;
|
||||
import fr.luc.bettermccommands.event.*;
|
||||
import fr.luc.bettermccommands.mock.SampleEventListener;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CommandEventLifecycleTest {
|
||||
|
||||
private BetterMcCommands manager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
manager = new BetterMcCommands("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("L'annulation de PreExecuteEvent doit bloquer l'exécution")
|
||||
void testPreExecuteCancellation() {
|
||||
AtomicBoolean executed = new AtomicBoolean(false);
|
||||
|
||||
Command command = BetterMcCommands.builder("tpall")
|
||||
.onPreExecute(event -> event.setCancelled(true))
|
||||
.executes(context -> executed.set(true))
|
||||
.build();
|
||||
|
||||
Player player = Mockito.mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
|
||||
CommandResult result = manager.getDispatcher().execute(command, player, "tpall", new String[0]);
|
||||
|
||||
assertEquals(CommandResult.CANCELLED, result);
|
||||
assertFalse(executed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PostExecuteEvent doit capturer le succès et la durée")
|
||||
void testPostExecuteEvent() {
|
||||
AtomicReference<CommandPostExecuteEvent> capturedEvent = new AtomicReference<>();
|
||||
|
||||
manager.on(CommandPostExecuteEvent.class, capturedEvent::set);
|
||||
|
||||
Command command = BetterMcCommands.builder("heal")
|
||||
.executes(context -> {
|
||||
// Simule un petit traitement
|
||||
Thread.sleep(10);
|
||||
})
|
||||
.build();
|
||||
|
||||
Player player = Mockito.mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
|
||||
CommandResult result = manager.getDispatcher().execute(command, player, "heal", new String[0]);
|
||||
|
||||
assertEquals(CommandResult.SUCCESS, result);
|
||||
assertNotNull(capturedEvent.get());
|
||||
assertTrue(capturedEvent.get().isSuccessful());
|
||||
assertTrue(capturedEvent.get().getExecutionDuration().toMillis() >= 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Le système de Cooldown doit bloquer les exécutions successives d'un joueur")
|
||||
void testCommandCooldown() {
|
||||
Command command = BetterMcCommands.builder("kit")
|
||||
.cooldown(Duration.ofSeconds(60))
|
||||
.executes(context -> {})
|
||||
.build();
|
||||
|
||||
Player player = Mockito.mock(Player.class);
|
||||
UUID uuid = UUID.randomUUID();
|
||||
when(player.getUniqueId()).thenReturn(uuid);
|
||||
|
||||
// 1ère exécution -> Réussie
|
||||
CommandResult result1 = manager.getDispatcher().execute(command, player, "kit", new String[0]);
|
||||
assertEquals(CommandResult.SUCCESS, result1);
|
||||
|
||||
// 2ème exécution immédiate -> Bloquée par Cooldown
|
||||
CommandResult result2 = manager.getDispatcher().execute(command, player, "kit", new String[0]);
|
||||
assertEquals(CommandResult.COOLDOWN, result2);
|
||||
|
||||
// Reset cooldown
|
||||
manager.getCooldownManager().resetCooldown(command, player);
|
||||
|
||||
// 3ème exécution après reset -> Réussie
|
||||
CommandResult result3 = manager.getDispatcher().execute(command, player, "kit", new String[0]);
|
||||
assertEquals(CommandResult.SUCCESS, result3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Les écouteurs annotés @CommandEventHandler doivent être appelés correctement")
|
||||
void testAnnotatedEventListeners() {
|
||||
SampleEventListener listener = new SampleEventListener();
|
||||
manager.registerListeners(listener);
|
||||
|
||||
Command command = BetterMcCommands.builder("custom")
|
||||
.executes(context -> {})
|
||||
.build();
|
||||
|
||||
Player player = Mockito.mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||
|
||||
manager.getDispatcher().execute(command, player, "custom", new String[0]);
|
||||
|
||||
assertTrue(listener.preFired.get());
|
||||
assertTrue(listener.postFired.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package fr.luc.bettermccommands;
|
||||
|
||||
import fr.luc.bettermccommands.api.Command;
|
||||
import fr.luc.bettermccommands.api.CommandResult;
|
||||
import fr.luc.bettermccommands.argument.Arguments;
|
||||
import fr.luc.bettermccommands.platform.CommandDispatcher;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.ConsoleCommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class CommandExecutionTest {
|
||||
|
||||
private BetterMcCommands manager;
|
||||
private CommandDispatcher dispatcher;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
manager = new BetterMcCommands("test");
|
||||
dispatcher = manager.getDispatcher();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Exécution simple d'une commande racine")
|
||||
void testRootCommandExecution() {
|
||||
AtomicBoolean executed = new AtomicBoolean(false);
|
||||
|
||||
Command command = BetterMcCommands.builder("ping")
|
||||
.executes(context -> executed.set(true))
|
||||
.build();
|
||||
|
||||
CommandSender sender = Mockito.mock(CommandSender.class);
|
||||
CommandResult result = dispatcher.execute(command, sender, "ping", new String[0]);
|
||||
|
||||
assertEquals(CommandResult.SUCCESS, result);
|
||||
assertTrue(executed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Exécution d'une sous-commande avec arguments obligatoires et optionnels")
|
||||
void testSubCommandWithArguments() {
|
||||
AtomicReference<String> targetPlayer = new AtomicReference<>();
|
||||
AtomicInteger amountGiven = new AtomicInteger();
|
||||
|
||||
Command command = BetterMcCommands.builder("money")
|
||||
.subcommand(BetterMcCommands.subBuilder("give")
|
||||
.argument(Arguments.string("player"))
|
||||
.argument(Arguments.integer("amount", 1, 1000).defaultValue(50))
|
||||
.executes(context -> {
|
||||
targetPlayer.set(context.getString("player"));
|
||||
amountGiven.set(context.getInt("amount"));
|
||||
})
|
||||
)
|
||||
.build();
|
||||
|
||||
CommandSender sender = Mockito.mock(CommandSender.class);
|
||||
|
||||
// Appel avec argument optionnel omis (valeur par défaut 50)
|
||||
CommandResult result1 = dispatcher.execute(command, sender, "money", new String[]{"give", "Luc"});
|
||||
assertEquals(CommandResult.SUCCESS, result1);
|
||||
assertEquals("Luc", targetPlayer.get());
|
||||
assertEquals(50, amountGiven.get());
|
||||
|
||||
// Appel avec argument optionnel spécifié (200)
|
||||
CommandResult result2 = dispatcher.execute(command, sender, "money", new String[]{"give", "Alex", "200"});
|
||||
assertEquals(CommandResult.SUCCESS, result2);
|
||||
assertEquals("Alex", targetPlayer.get());
|
||||
assertEquals(200, amountGiven.get());
|
||||
|
||||
// Appel avec argument obligatoire manquant
|
||||
CommandResult result3 = dispatcher.execute(command, sender, "money", new String[]{"give"});
|
||||
assertEquals(CommandResult.SYNTAX_ERROR, result3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Contrôle des permissions d'exécution")
|
||||
void testPermissionCheck() {
|
||||
Command command = BetterMcCommands.builder("secret")
|
||||
.permission("admin.secret")
|
||||
.executes(context -> {})
|
||||
.build();
|
||||
|
||||
CommandSender unauthorizedSender = Mockito.mock(CommandSender.class);
|
||||
when(unauthorizedSender.hasPermission("admin.secret")).thenReturn(false);
|
||||
|
||||
CommandResult result1 = dispatcher.execute(command, unauthorizedSender, "secret", new String[0]);
|
||||
assertEquals(CommandResult.PERMISSION_DENIED, result1);
|
||||
|
||||
CommandSender authorizedSender = Mockito.mock(CommandSender.class);
|
||||
when(authorizedSender.hasPermission("admin.secret")).thenReturn(true);
|
||||
|
||||
CommandResult result2 = dispatcher.execute(command, authorizedSender, "secret", new String[0]);
|
||||
assertEquals(CommandResult.SUCCESS, result2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Restriction par type d'émetteur (Player only)")
|
||||
void testPlayerOnlySenderType() {
|
||||
Command command = BetterMcCommands.builder("spawn")
|
||||
.playerOnly()
|
||||
.executes(context -> {})
|
||||
.build();
|
||||
|
||||
ConsoleCommandSender consoleSender = Mockito.mock(ConsoleCommandSender.class);
|
||||
CommandResult result1 = dispatcher.execute(command, consoleSender, "spawn", new String[0]);
|
||||
assertEquals(CommandResult.PERMISSION_DENIED, result1);
|
||||
|
||||
Player playerSender = Mockito.mock(Player.class);
|
||||
CommandResult result2 = dispatcher.execute(command, playerSender, "spawn", new String[0]);
|
||||
assertEquals(CommandResult.SUCCESS, result2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Argument greedy consommant tout le reste de la ligne")
|
||||
void testGreedyArgument() {
|
||||
AtomicReference<String> fullMessage = new AtomicReference<>();
|
||||
|
||||
Command command = BetterMcCommands.builder("say")
|
||||
.argument(Arguments.greedyString("message"))
|
||||
.executes(context -> fullMessage.set(context.getString("message")))
|
||||
.build();
|
||||
|
||||
CommandSender sender = Mockito.mock(CommandSender.class);
|
||||
CommandResult result = dispatcher.execute(command, sender, "say", new String[]{"Bonjour", "tout", "le", "monde", "!"});
|
||||
|
||||
assertEquals(CommandResult.SUCCESS, result);
|
||||
assertEquals("Bonjour tout le monde !", fullMessage.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.luc.bettermccommands.mock;
|
||||
|
||||
import fr.luc.bettermccommands.event.CommandPostExecuteEvent;
|
||||
import fr.luc.bettermccommands.event.CommandPreExecuteEvent;
|
||||
import fr.luc.bettermccommands.event.annotation.CommandEventHandler;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Écouteur d'exemple pour les tests.
|
||||
*/
|
||||
public class SampleEventListener {
|
||||
|
||||
public final AtomicBoolean preFired = new AtomicBoolean(false);
|
||||
public final AtomicBoolean postFired = new AtomicBoolean(false);
|
||||
|
||||
@CommandEventHandler
|
||||
public void onPre(CommandPreExecuteEvent event) {
|
||||
preFired.set(true);
|
||||
}
|
||||
|
||||
@CommandEventHandler
|
||||
public void onPost(CommandPostExecuteEvent event) {
|
||||
postFired.set(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package fr.luc.bettermccommands.mock;
|
||||
|
||||
/**
|
||||
* Enum d'exemple pour les tests unitaires.
|
||||
*/
|
||||
public enum SampleRank {
|
||||
MEMBER, ADMIN, VIP
|
||||
}
|
||||
Reference in New Issue
Block a user