diff --git a/README.md b/README.md index 5c75881..99a045e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Une bibliothèque Java moderne, fluide et événementielle permettant de créer, --- ## 📑 Sommaire +- [📚 Guides Détaillés (Dossier `/docs`)](#-guides-détaillés-dossier-docs) - [🌟 Fonctionnalités](#-fonctionnalités) - [🏗️ Architecture & Flux d'Exécution](#️-architecture--flux-dexécution) - [📦 Installation & Configuration](#-installation--configuration) @@ -30,6 +31,19 @@ Une bibliothèque Java moderne, fluide et événementielle permettant de créer, --- +## 📚 Guides Détaillés (Dossier `/docs`) + +Pour aller plus loin, consultez la documentation modulaire : +* 📘 [**01. Guide de Démarrage Rapide**](docs/01_GETTING_STARTED.md) : Installation, structure de plugin et initialisation. +* 🏗️ [**02. Fluent Builder DSL**](docs/02_COMMAND_BUILDER_DSL.md) : Déclaration de commandes racines, sous-commandes imbriquées, restrictions et alias. +* 🧩 [**03. Arguments Typés & Auto-complétion**](docs/03_TYPED_ARGUMENTS.md) : Catalogue exhaustif des arguments, parsers personnalisés et Tab-Complete. +* 🎯 [**04. Système d'Événements & Cycle de Vie**](docs/04_LIFECYCLE_EVENTS.md) : Hooks d'exécution, écouteurs `@CommandEventHandler` et gestion des erreurs. +* 🛡️ [**05. Validation & Double Confirmation**](docs/05_VALIDATION_AND_CONFIRMATION.md) : Validateurs de pré-requis et confirmations interactives. +* ⏳ [**06. Cooldowns & Répétition de Commandes**](docs/06_COOLDOWNS_AND_REPEAT.md) : Temps de recharge, bypass et exécution répétée (`CommandRepeater`). +* ⚙️ [**07. Plateforme & Injection CommandMap**](docs/07_PLATFORM_AND_INJECTION.md) : Fonctionnement sous le capot, réflexion et hot-reload. + +--- + ## 🌟 Fonctionnalités - **DSL Fluide & Déclaratif** : Déclaration intuitive basée sur le pattern Builder chainable (`BetterMcCommands.builder("ma-commande")`). diff --git a/docs/01_GETTING_STARTED.md b/docs/01_GETTING_STARTED.md new file mode 100644 index 0000000..a29f0c1 --- /dev/null +++ b/docs/01_GETTING_STARTED.md @@ -0,0 +1,127 @@ +# 🚀 Guide de Démarrage Rapide - betterMcCommands + +Ce guide vous accompagne dans l'installation, la configuration et la création de vos premières commandes avec la bibliothèque **betterMcCommands**. + +--- + +## 📦 1. Installation + +### Gradle (Kotlin DSL) + +Ajoutez les dépôts et la dépendance dans votre fichier `build.gradle.kts` : + +```kotlin +repositories { + mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") +} + +dependencies { + // Intégration de betterMcCommands + implementation("fr.luc:betterMcCommands:1.0.0-SNAPSHOT") +} +``` + +### Gradle (Groovy DSL) + +```groovy +repositories { + mavenCentral() + maven { url 'https://repo.papermc.io/repository/maven-public/' } +} + +dependencies { + implementation 'fr.luc:betterMcCommands:1.0.0-SNAPSHOT' +} +``` + +--- + +## ⚙️ 2. Initialisation dans votre Plugin Bukkit/Paper + +Créez une instance de `BetterMcCommands` dans la méthode `onEnable()` de votre plugin et nettoyez-la proprement dans `onDisable()` : + +```java +package fr.luc.monplugin; + +import fr.luc.bettermccommands.BetterMcCommands; +import org.bukkit.plugin.java.JavaPlugin; + +public class MonPlugin extends JavaPlugin { + + private BetterMcCommands commandsManager; + + @Override + public void onEnable() { + // 1. Initialisation du gestionnaire avec namespace dédié + this.commandsManager = BetterMcCommands.create(this); + + // 2. Enregistrement d'écouteurs d'événements de commandes globaux (optionnel) + this.commandsManager.registerListeners(new MonEcouteurDeCommandes()); + + // 3. Déclaration et enregistrement des commandes + enregistrerCommandes(); + + getLogger().info("MonPlugin et betterMcCommands sont prêts !"); + } + + private void enregistrerCommandes() { + // Exemple d'une commande simple : /ping + BetterMcCommands.builder("ping") + .description("Renvoie pong au joueur") + .aliases("p") + .executes(context -> { + context.replySuccess("Pong !"); + }) + .register(); + } + + @Override + public void onDisable() { + // 4. Désenregistrement à chaud de toutes les commandes injectées + if (commandsManager != null) { + commandsManager.unregisterAll(); + } + getLogger().info("MonPlugin a désenregistré ses commandes."); + } +} +``` + +--- + +## ⚡ 3. Zéro `plugin.yml` + +Contrairement aux commandes Bukkit traditionnelles, **vous n'avez pas besoin d'ajouter vos commandes dans le fichier `plugin.yml`**. + +`betterMcCommands` injecte directement vos commandes par réflexion dans la `CommandMap` du serveur lors de l'appel à `.register()`, ce qui permet : +* D'enregistrer des commandes dynamiquement selon la configuration. +* De recharger (hot-reload) ou retirer des commandes sans redémarrer le serveur. +* D'éviter les doublons et les conflits de namespaces. + +--- + +## 💬 4. Formatage des Messages avec MiniMessage + +Le `CommandContext` intègre nativement **Kyori Adventure & MiniMessage** : + +```java +BetterMcCommands.builder("bienvenue") + .executes(context -> { + // Dégradé de couleurs hexadécimales et texte gras + context.reply("Bienvenue sur le serveur !"); + + // Message avec préfixe vert standard [✔] + context.replySuccess("Votre profil a été chargé avec succès."); + + // Message avec préfixe rouge [✖] + context.replyError("Une erreur est survenue."); + + // Message d'information [ℹ] + context.replyInfo("Tapez /aide pour obtenir de l'assistance."); + }) + .register(); +``` + +--- + +> 📖 **Étape suivante** : Découvrez la syntaxe complète du constructeur dans [02_COMMAND_BUILDER_DSL.md](02_COMMAND_BUILDER_DSL.md). diff --git a/docs/02_COMMAND_BUILDER_DSL.md b/docs/02_COMMAND_BUILDER_DSL.md new file mode 100644 index 0000000..1305a44 --- /dev/null +++ b/docs/02_COMMAND_BUILDER_DSL.md @@ -0,0 +1,128 @@ +# 🏗️ Fluent Builder DSL - betterMcCommands + +La création de commandes et de sous-commandes repose sur un pattern **Builder chainable et intuitif**. + +--- + +## 📑 Sommaire +- [1. Commande Racine (`BetterMcCommands.builder`)](#1-commande-racine-bettermccommandsbuilder) +- [2. Sous-Commandes Imbriquées (`BetterMcCommands.subBuilder`)](#2-sous-commandes-imbriquées-bettermccommandssubbuilder) +- [3. Restrictions d'Émetteurs (`CommandSenderType`)](#3-restrictions-démetteurs-commandsendertype) +- [4. Permissions et Alias](#4-permissions-et-alias) +- [5. Le Contexte d'Exécution (`CommandContext`)](#5-le-contexte-dexécution-commandcontext) + +--- + +## 1. Commande Racine (`BetterMcCommands.builder`) + +Une commande racine est créée via `BetterMcCommands.builder(String name)` : + +```java +BetterMcCommands.builder("spawn") + .description("Téléporte le joueur au point d'apparition") + .aliases("hub", "lobby") + .permission("monplugin.command.spawn") + .playerOnly() + .executes(context -> { + Player player = context.getPlayer(); + player.teleport(player.getWorld().getSpawnLocation()); + context.replySuccess("Téléportation au spawn effectuée !"); + }) + .register(); +``` + +--- + +## 2. Sous-Commandes Imbriquées (`BetterMcCommands.subBuilder`) + +Vous pouvez ajouter autant de sous-commandes que nécessaire avec `subBuilder(String name)` et les imbriquer sans limite de profondeur : + +```java +BetterMcCommands.builder("guilde") + .description("Gestion de votre guilde") + .permission("guilde.use") + // /guilde (sans sous-commande) affiche l'aide par défaut + .executes(context -> { + context.reply("=== Commandes de Guilde ==="); + context.reply("/guilde info - Informations sur votre guilde"); + context.reply("/guilde admin rank set - Rangs"); + }) + + // Sous-commande : /guilde info + .subcommand(BetterMcCommands.subBuilder("info") + .description("Affiche les infos de la guilde") + .executes(context -> { + context.replySuccess("Informations de la guilde affichées."); + }) + ) + + // Sous-commandes imbriquées : /guilde admin rank set + .subcommand(BetterMcCommands.subBuilder("admin") + .description("Administration de guilde") + .permission("guilde.admin") + .subcommand(BetterMcCommands.subBuilder("rank") + .description("Gestion des rangs") + .subcommand(BetterMcCommands.subBuilder("set") + .description("Attribue un rang à un membre") + .argument(Arguments.player("joueur")) + .argument(Arguments.string("grade")) + .executes(context -> { + Player cible = context.getTargetPlayer("joueur"); + String grade = context.getString("grade"); + context.replySuccess("Grade " + grade + " appliqué à " + cible.getName()); + }) + ) + ) + ) + .register(); +``` + +--- + +## 3. Restrictions d'Émetteurs (`CommandSenderType`) + +Contrôlez qui a le droit d'exécuter la commande : + +* `.playerOnly()` : Seuls les joueurs connectés en jeu peuvent exécuter la commande. +* `.consoleOnly()` : Seule la console du serveur peut exécuter la commande. +* `.senderType(CommandSenderType.ALL)` (défaut) : Tous les émetteurs sont autorisés. + +En cas de non-respect, un message d'erreur est automatiquement retourné et l'événement `CommandPermissionDeniedEvent` est déclenché. + +--- + +## 4. Permissions et Alias + +* `.permission("ma.permission")` : Spécifie la permission Bukkit requise. +* `.aliases("alias1", "alias2", ...)` : Enregistre un ou plusieurs alias qui redirigent vers cette commande ou sous-commande. + +--- + +## 5. Le Contexte d'Exécution (`CommandContext`) + +L'objet `CommandContext` transmis à l'exécuteur `executes(context -> ...)` fournit : + +| Méthode | Description | +|---|---| +| `context.getSender()` | Récupère l'émetteur brut (`CommandSender`). | +| `context.getPlayer()` | Récupère le joueur (`Player`) ou lève une exception si console. | +| `context.getConsole()` | Récupère la console (`ConsoleCommandSender`). | +| `context.isPlayer()` | Teste si l'émetteur est un joueur. | +| `context.getLabel()` | Retourne l'alias ou le nom utilisé pour l'appel. | +| `context.getRawArgs()` | Retourne le tableau des arguments bruts passés par le joueur. | +| `context.get(name, Class)` | Récupère un argument converti et typé. | +| `context.getString(name)` | Récupère un argument de type chaîne. | +| `context.getInt(name)` | Récupère un argument entier. | +| `context.getDouble(name)` | Récupère un argument décimal. | +| `context.getBoolean(name)` | Récupère un argument booléen. | +| `context.getTargetPlayer(name)` | Récupère un joueur cible (`Player`). | +| `context.getWorld(name)` | Récupère un monde Bukkit (`World`). | +| `context.getDuration(name)` | Récupère une durée (`Duration`). | +| `context.reply(miniMessage)` | Envoie un message formaté en MiniMessage. | +| `context.replySuccess(msg)` | Envoie un message de succès préfixé en vert. | +| `context.replyError(msg)` | Envoie un message d'erreur préfixé en rouge. | +| `context.setMetadata(key, val)`| Stocke une donnée contextuelle temporaire. | + +--- + +> 📖 **Étape suivante** : Explorez tous les types d'arguments disponibles dans [03_TYPED_ARGUMENTS.md](03_TYPED_ARGUMENTS.md). diff --git a/docs/03_TYPED_ARGUMENTS.md b/docs/03_TYPED_ARGUMENTS.md new file mode 100644 index 0000000..de4d411 --- /dev/null +++ b/docs/03_TYPED_ARGUMENTS.md @@ -0,0 +1,155 @@ +# 🧩 Système d'Arguments Typés & Auto-complétion - betterMcCommands + +La bibliothèque fournit un ensemble complet d'arguments prêts à l'emploi et supporte la création de types d'arguments personnalisés. + +--- + +## 📑 Sommaire +- [1. Déclaration d'Arguments](#1-déclaration-darguments) +- [2. Arguments Optionnels & Valeurs par Défaut](#2-arguments-optionnels--valeurs-par-défaut) +- [3. Arguments Gourmands (Greedy String)](#3-arguments-gourmands-greedy-string) +- [4. Catalogue des Types d'Arguments](#4-catalogue-des-types-darguments) +- [5. Création d'un Type d'Argument Personnalisé](#5-création-dun-type-dargument-personnalisé) + +--- + +## 1. Déclaration d'Arguments + +Les arguments sont déclarés via la fabrique statique `Arguments.*` et ajoutés avec `.argument(...)` : + +```java +BetterMcCommands.builder("givemoney") + .argument(Arguments.player("cible").description("Le joueur récepteur")) + .argument(Arguments.integer("montant", 1, 100000).description("Montant entre 1 et 100 000")) + .executes(context -> { + Player target = context.getTargetPlayer("cible"); + int montant = context.getInt("montant"); + + context.replySuccess("Transfert de " + montant + " $ à " + target.getName() + "."); + }) + .register(); +``` + +--- + +## 2. Arguments Optionnels & Valeurs par Défaut + +Vous pouvez marquer n'importe quel argument comme optionnel et lui attribuer une valeur par défaut : + +```java +// Valeur par défaut constante (ex: 1) +.argument(Arguments.integer("quantite", 1, 64).defaultValue(1)) + +// Valeur par défaut dynamique via Supplier +.argument(Arguments.world("monde").defaultValue(() -> Bukkit.getWorlds().get(0))) + +// Argument optionnel sans valeur par défaut (retourne null si omis) +.argument(Arguments.string("motif").optional()) +``` + +--- + +## 3. Arguments Gourmands (Greedy String) + +Un argument marqué comme **greedy** consomme tous les mots restants jusqu'à la fin de la ligne de commande, éliminant ainsi le besoin de guillemets pour les phrases longues : + +```java +BetterMcCommands.builder("broadcast") + .argument(Arguments.greedyString("message")) + .executes(context -> { + String annonce = context.getString("message"); + Bukkit.broadcastMessage("§6[Annonce] §f" + annonce); + }) + .register(); +``` + +--- + +## 4. Catalogue des Types d'Arguments + +### Primitifs & Standards +* `Arguments.word("nom")` : Mot simple délimité par un espace. +* `Arguments.string("texte")` : Chaîne de caractères standard. +* `Arguments.greedyString("message")` : Chaîne gourmande jusqu'à la fin de la ligne. +* `Arguments.integer("nb")` : Entier non borné. +* `Arguments.integer("nb", min)` : Entier avec un seuil minimal. +* `Arguments.integer("nb", min, max)` : Entier borné dans un intervalle fermé `[min, max]`. +* `Arguments.decimal("montant")` : Nombre décimal (supporte les points `.` et virgules `,`). +* `Arguments.decimal("montant", min, max)` : Nombre décimal borné. +* `Arguments.bool("actif")` : Booléen (`true`/`false`, `oui`/`non`, `1`/`0`, `on`/`off`, `yes`/`no`). +* `Arguments.enumOf("grade", MonEnum.class)` : Enum Java avec suggestions dynamiques de toutes ses constantes. + +### Types Minecraft & Utilitaires +* `Arguments.player("joueur")` : Joueur connecté avec suggestions en direct de tous les joueurs en ligne. +* `Arguments.offlinePlayer("joueur")` : Joueur hors-ligne ou en ligne, résolu par pseudo ou UUID. +* `Arguments.world("monde")` : Monde Bukkit avec auto-complétion des mondes chargés. +* `Arguments.location("coords")` : Coordonnées sous la forme `x,y,z` ou `x,y,z,monde` avec support des coordonnées relatives `~`. +* `Arguments.duration("duree")` : Durée temporelle (ex: `30s`, `15m`, `2h`, `1d`, `7d`, `1m 40s`). + +--- + +## 5. Création d'un Type d'Argument Personnalisé + +Pour créer un type de donnée métier (ex: une Guilde, un Kit, une Région) : + +```java +package fr.luc.monplugin.argument; + +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.stream.Collectors; + +public class GuildArgumentType implements ArgumentType { + + private final GuildManager guildManager; + + public GuildArgumentType(GuildManager guildManager) { + this.guildManager = guildManager; + } + + @Override + public Guild parse(String argumentName, String input, CommandContext context) throws CommandArgumentParseException { + Guild guild = guildManager.getGuildByName(input); + if (guild == null) { + throw new CommandArgumentParseException(argumentName, input, getTypeName(), "La guilde '" + input + "' n'existe pas."); + } + return guild; + } + + @Override + public Collection suggest(CommandContext context, String currentInput) { + String lower = currentInput == null ? "" : currentInput.toLowerCase(); + return guildManager.getAllGuilds().stream() + .map(Guild::getName) + .filter(name -> name.toLowerCase().startsWith(lower)) + .map(Suggestion::of) + .collect(Collectors.toList()); + } + + @Override + public String getTypeName() { + return "Guilde"; + } +} +``` + +### Utilisation dans une commande : +```java +BetterMcCommands.builder("guilde") + .subcommand(BetterMcCommands.subBuilder("join") + .argument(Arguments.custom("guilde", new GuildArgumentType(guildManager))) + .executes(context -> { + Guild guild = context.get("guilde", Guild.class); + context.replySuccess("Vous avez rejoint la guilde " + guild.getName()); + }) + ) + .register(); +``` + +--- + +> 📖 **Étape suivante** : Apprenez à intercepter les événements dans [04_LIFECYCLE_EVENTS.md](04_LIFECYCLE_EVENTS.md). diff --git a/docs/04_LIFECYCLE_EVENTS.md b/docs/04_LIFECYCLE_EVENTS.md new file mode 100644 index 0000000..703c088 --- /dev/null +++ b/docs/04_LIFECYCLE_EVENTS.md @@ -0,0 +1,120 @@ +# 🎯 Système d'Événements & Cycle de Vie - betterMcCommands + +`betterMcCommands` intègre un système d'événements complet permettant d'intercepter, modifier, auditer ou bloquer chaque étape de l'exécution d'une commande. + +--- + +## 📑 Sommaire +- [1. Vue d'Ensemble du Cycle de Vie](#1-vue-densemble-du-cycle-de-vie) +- [2. Les Deux Méthodes d'Écoute](#2-les-deux-méthodes-découte) + - [Méthode A : Hooks Fluides sur le Builder](#méthode-a--hooks-fluides-sur-le-builder) + - [Méthode B : Classes d'Écouteurs avec `@CommandEventHandler`](#méthode-b--classes-découteurs-avec-commandeventhandler) +- [3. Référence Exhaustive des Événements](#3-référence-exhaustive-des-événements) +- [4. Exemples Pratiques](#4-exemples-pratiques) + +--- + +## 1. Vue d'Ensemble du Cycle de Vie + +Lorsqu'un joueur ou la console exécute une commande : + +1. **Vérification d'émetteur et permission** -> `CommandPermissionDeniedEvent` (si refus). +2. **Vérification du Cooldown** -> `CommandCooldownEvent` (si cooldown actif). +3. **Parsing des arguments** -> `CommandSyntaxErrorEvent` (si syntaxe invalide). +4. **Demande de Confirmation** -> `CommandConfirmationRequiredEvent` (si confirmation requise). +5. **Pré-exécution** -> `CommandPreExecuteEvent` (**annulable**). +6. **Exécution métier** -> Appel du `CommandExecutor`. +7. **Post-exécution** -> `CommandPostExecuteEvent` (durée d'exécution, statut de réussite/échec, logs). + +--- + +## 2. Les Deux Méthodes d'Écoute + +### Méthode A : Hooks Fluides sur le Builder + +Permet d'attacher un comportement spécifique directement à une commande : + +```java +BetterMcCommands.builder("trade") + .onPreExecute(event -> { + if (event.isPlayer() && isInCombat(event.getPlayer())) { + event.setCancelled(true); + event.reply("Vous ne pouvez pas échanger en combat !"); + } + }) + .onPostExecute(event -> { + System.out.println("Trade exécuté en " + event.getExecutionDuration().toMillis() + "ms."); + }) + .executes(context -> { + context.replySuccess("Échange ouvert !"); + }) + .register(); +``` + +--- + +### Méthode B : Classes d'Écouteurs avec `@CommandEventHandler` + +Idéal pour centraliser la sécurité, l'audit et les métriques globales : + +```java +package fr.luc.monplugin.listener; + +import fr.luc.bettermccommands.event.*; +import fr.luc.bettermccommands.event.annotation.CommandEventHandler; + +public class CommandAuditListener { + + // Écoute TOUTES les commandes avant leur exécution + @CommandEventHandler(priority = 10) + public void onAnyPreExecute(CommandPreExecuteEvent event) { + System.out.println("[Audit] Demande d'exécution : /" + event.getNode().getFullName() + + " par " + event.getSender().getName()); + } + + // Écoute UNIQUEMENT la commande racine 'admin' + @CommandEventHandler(command = "admin") + public void onAdminPostExecute(CommandPostExecuteEvent event) { + if (!event.isSuccessful()) { + System.err.println("[Alerte Sécurité] Échec sur commande admin : " + event.getNode().getFullName()); + } + } + + // Personnalise les messages de refus de permission + @CommandEventHandler + public void onPermissionDenied(CommandPermissionDeniedEvent event) { + if (event.getRequiredPermission() != null) { + event.setCustomErrorMessage("Accès Refusé : Permission requise [" + event.getRequiredPermission() + "]."); + } + } + + // Personnalise l'avertissement de cooldown + @CommandEventHandler + public void onCooldown(CommandCooldownEvent event) { + event.setCustomMessage("⏳ Patientez " + event.getRemainingCooldown().toSeconds() + "s avant de réutiliser cette commande."); + } +} +``` + +Pour enregistrer cette classe : +```java +commandsManager.registerListeners(new CommandAuditListener()); +``` + +--- + +## 3. Référence Exhaustive des Événements + +| Classe d'Événement | Description | Données Disponibles | Annulable ? | +|---|---|---|:---:| +| `CommandPreExecuteEvent` | Déclenché immédiatement avant l'exécution métier. | `getNode()`, `getSender()`, `getContext()` | ✅ **Oui** | +| `CommandPostExecuteEvent` | Déclenché après la fin de l'exécution. | `getResult()`, `getExecutionDuration()`, `getException()`, `isSuccessful()` | ❌ Non | +| `CommandPermissionDeniedEvent` | Déclenché lors d'un échec de permission ou restriction console/joueur. | `getRequiredPermission()`, `getRequiredSenderType()`, `setCustomErrorMessage(...)` | ✅ **Oui** | +| `CommandSyntaxErrorEvent` | Déclenché lors d'un argument manquant ou format incorrect. | `getErrorReason()`, `getUsage()`, `setCustomMessage(...)` | ✅ **Oui** | +| `CommandCooldownEvent` | Déclenché lorsqu'un joueur tente d'exécuter une commande sous cooldown. | `getRemainingCooldown()`, `setCustomMessage(...)` | ✅ **Oui** | +| `CommandConfirmationRequiredEvent` | Déclenché lorsqu'une commande requiert une confirmation préalable. | `getTimeout()`, `setCustomPromptMessage(...)` | ✅ **Oui** | +| `CommandTabCompleteEvent` | Déclenché lors du calcul des suggestions d'auto-complétion. | `getSuggestions()`, `addSuggestion(...)`, `getCurrentInput()` | ✅ **Oui** | + +--- + +> 📖 **Étape suivante** : Découvrez la validation et la confirmation dans [05_VALIDATION_AND_CONFIRMATION.md](05_VALIDATION_AND_CONFIRMATION.md). diff --git a/docs/05_VALIDATION_AND_CONFIRMATION.md b/docs/05_VALIDATION_AND_CONFIRMATION.md new file mode 100644 index 0000000..ebc4c35 --- /dev/null +++ b/docs/05_VALIDATION_AND_CONFIRMATION.md @@ -0,0 +1,103 @@ +# 🛡️ Validation & Double Confirmation - betterMcCommands + +La bibliothèque intègre des mécanismes pour valider les pré-requis métier et sécuriser les actions destructrices ou sensibles. + +--- + +## 📑 Sommaire +- [1. Validation Métier au Niveau de la Commande](#1-validation-métier-au-niveau-de-la-commande) +- [2. Validation au Niveau d'un Argument](#2-validation-au-niveau-dun-argument) +- [3. Confirmation Préalable & Double Validation (`.requireConfirmation`)](#3-confirmation-préalable--double-validation-requireconfirmation) + +--- + +## 1. Validation Métier au Niveau de la Commande + +La méthode `.validate(...)` permet d'exécuter des assertions logiques avant que le gestionnaire principal ne soit appelé. Si la validation échoue, le message d'erreur est automatiquement envoyé et la commande s'interrompt. + +### Exemple : Validation simple par prédicat +```java +BetterMcCommands.builder("level-reward") + .playerOnly() + // Teste si le joueur a le niveau requis + .validate(ctx -> ctx.getPlayer().getLevel() >= 10, "Vous devez posséder le niveau 10 minimum !") + .executes(ctx -> { + ctx.replySuccess("Récompense de niveau 10 réclamée !"); + }) + .register(); +``` + +### Exemple : Validateur complexe (`CommandValidator`) +```java +BetterMcCommands.builder("claim") + .playerOnly() + .validate(ctx -> { + Player p = ctx.getPlayer(); + if (economy.getBalance(p) < 500) { + return ValidationResult.invalid("Solde insuffisant : 500 $ requis."); + } + if (territoryManager.isProtected(p.getLocation())) { + return ValidationResult.invalid("Cette zone est déjà protégée !"); + } + return ValidationResult.valid(); + }) + .executes(ctx -> { + ctx.replySuccess("Territoire revendiqué avec succès !"); + }) + .register(); +``` + +--- + +## 2. Validation au Niveau d'un Argument + +Vous pouvez également restreindre les valeurs acceptées par un argument : + +```java +BetterMcCommands.builder("setprice") + .argument(Arguments.decimal("prix") + // Vérification personnalisée de la valeur + .validate(prix -> prix > 0, "Le prix doit être strictement supérieur à 0 !") + .validate(prix -> prix <= 1000000, "Le prix maximal autorisé est de 1 000 000 $.") + ) + .executes(ctx -> { + double prix = ctx.getDouble("prix"); + ctx.replySuccess("Prix configuré : " + prix + " $."); + }) + .register(); +``` + +--- + +## 3. Confirmation Préalable & Double Validation (`.requireConfirmation`) + +Pour les commandes irréversibles (suppression de données, dissolution de guilde, réinitialisation de statistiques), activez la confirmation préalable avec `.requireConfirmation(...)` : + +```java +BetterMcCommands.builder("disband") + .description("Dissout votre guilde") + .playerOnly() + // Délai de 15 secondes pour confirmer avec message d'avertissement + .requireConfirmation(Duration.ofSeconds(15), "ATTENTION : Cette action est irréversible ! Retapez /disband dans les 15 secondes pour confirmer.") + .executes(context -> { + Player p = context.getPlayer(); + guildManager.disbandGuild(p); + context.replySuccess("Votre guilde a été dissoute."); + }) + .register(); +``` + +### Comment fonctionne le cycle de confirmation ? +1. **1ère exécution** : L'émetteur tape `/disband`. + - La commande est suspendue. + - Une demande de confirmation temporaire est enregistrée dans le `ConfirmationManager`. + - Le message d'avertissement est envoyé au joueur. + - L'événement `CommandConfirmationRequiredEvent` est diffusé. +2. **2ème exécution (dans les 15s)** : L'émetteur retape `/disband`. + - La confirmation active est détectée et consommée. + - Le code du gestionnaire métier `.executes(...)` s'exécute normalement. +3. **Expiration** : Si le joueur ne retape pas la commande dans le délai, la confirmation expire automatiquement. + +--- + +> 📖 **Étape suivante** : Découvrez les cooldowns et répétitions dans [06_COOLDOWNS_AND_REPEAT.md](06_COOLDOWNS_AND_REPEAT.md). diff --git a/docs/06_COOLDOWNS_AND_REPEAT.md b/docs/06_COOLDOWNS_AND_REPEAT.md new file mode 100644 index 0000000..b74e112 --- /dev/null +++ b/docs/06_COOLDOWNS_AND_REPEAT.md @@ -0,0 +1,114 @@ +# ⏳ Cooldowns & Répétition de Commandes - betterMcCommands + +Ce guide détaille la gestion native des temps de recharge (cooldowns) par joueur et l'exécution cadencée / répétée d'actions de commandes. + +--- + +## 📑 Sommaire +- [1. Configuration des Cooldowns](#1-configuration-des-cooldowns) +- [2. Permissions de Bypass de Cooldown](#2-permissions-de-bypass-de-cooldown) +- [3. Réinitialisation Manuelle de Cooldown](#3-réinitialisation-manuelle-de-cooldown) +- [4. Répétition & Tâches Cadencées (`CommandRepeater`)](#4-répétition--tâches-cadencées-commandrepeater) + +--- + +## 1. Configuration des Cooldowns + +Chaque commande ou sous-commande peut posséder son propre temps de recharge : + +```java +BetterMcCommands.builder("kit") + .description("Kit quotidien") + .playerOnly() + // Cooldown de 24 heures + .cooldown(Duration.ofHours(24)) + .executes(context -> { + context.replySuccess("Kit quotidien reçu !"); + }) + .register(); +``` + +* Si un joueur exécute la commande pendant son cooldown, l'exécution est bloquée et un message l'informe du temps restant (ex: `Veuillez patienter 14h 25m avant de réutiliser cette commande`). +* L'événement `CommandCooldownEvent` est déclenché, permettant d'adapter le message ou d'annuler le blocage sous certaines conditions. + +--- + +## 2. Permissions de Bypass de Cooldown + +Pour autoriser certains groupes (VIP, Modérateurs, Staff) à ignorer le temps de recharge : + +```java +BetterMcCommands.builder("heal") + .playerOnly() + .cooldown(Duration.ofMinutes(10)) + // Les joueurs possédant cette permission n'ont aucun cooldown + .cooldownBypass("monplugin.heal.bypass") + .executes(context -> { + Player p = context.getPlayer(); + p.setHealth(p.getMaxHealth()); + context.replySuccess("Vous avez été soigné !"); + }) + .register(); +``` + +--- + +## 3. Réinitialisation Manuelle de Cooldown + +Vous pouvez réinitialiser le cooldown d'un joueur par programmation : + +```java +Command healCommand = ...; +Player target = Bukkit.getPlayer("Luc"); + +commandsManager.getCooldownManager().resetCooldown(healCommand, target); +``` + +--- + +## 4. Répétition & Tâches Cadencées (`CommandRepeater`) + +L'utilitaire `CommandRepeater` permet d'exécuter une action liée à la commande de manière répétée à intervalle régulier, tout en gérant le suivi de progression et la possibilité d'annulation prématurée. + +### Exemple : Compte à rebours avant téléportation +```java +BetterMcCommands.builder("tpcountdown") + .playerOnly() + .executes(context -> { + Player p = context.getPlayer(); + Location destination = p.getWorld().getSpawnLocation(); + + // Répète 5 fois toutes les 1 seconde (20 ticks) + CommandRepeater.repeat(monPlugin, context, 5, Duration.ofSeconds(1), + progress -> { + int secondesRestantes = progress.getTotalRuns() - progress.getCurrentRun() + 1; + + // Si le joueur bouge, on peut annuler la boucle + if (hasMoved(p)) { + progress.cancel(); + context.replyError("Téléportation annulée car vous avez bougé !"); + return; + } + + context.reply("Téléportation dans " + secondesRestantes + "s..."); + }, + () -> { + // Action finale exécutée après la fin des 5 secondes + p.teleport(destination); + context.replySuccess("Téléporté avec succès !"); + } + ); + }) + .register(); +``` + +### Méthodes de `CommandRepeatProgress` : +* `progress.getCurrentRun()` : Numéro de l'itération en cours (commence à 1). +* `progress.getTotalRuns()` : Nombre total d'itérations prévues. +* `progress.isLastRun()` : true si c'est la dernière itération. +* `progress.cancel()` : Interrompt immédiatement toutes les itérations restantes. +* `progress.getContext()` : Récupère le contexte d'exécution d'origine. + +--- + +> 📖 **Étape suivante** : Découvrez le fonctionnement interne dans [07_PLATFORM_AND_INJECTION.md](07_PLATFORM_AND_INJECTION.md). diff --git a/docs/07_PLATFORM_AND_INJECTION.md b/docs/07_PLATFORM_AND_INJECTION.md new file mode 100644 index 0000000..ad75ba7 --- /dev/null +++ b/docs/07_PLATFORM_AND_INJECTION.md @@ -0,0 +1,60 @@ +# ⚙️ Plateforme & Injection CommandMap - betterMcCommands + +Ce document explique le fonctionnement sous le capot de l'injection dynamique, de l'isolation des namespaces et de la gestion du cycle de vie des commandes. + +--- + +## 📑 Sommaire +- [1. Comment Fonctionne l'Injection Dynamique ?](#1-comment-fonctionne-linjection-dynamique-) +- [2. Enveloppe Bukkit (`PaperCommandWrapper`)](#2-enveloppe-bukkit-papercommandwrapper) +- [3. Désenregistrement à Chaud & Hot Reload](#3-désenregistrement-à-chaud--hot-reload) +- [4. Isolation et Multi-Instances](#4-isolation-et-multi-instances) + +--- + +## 1. Comment Fonctionne l'Injection Dynamique ? + +Au lieu de forcer les développeurs à déclarer chaque commande dans le fichier `plugin.yml`, `betterMcCommands` utilise la classe `PaperCommandMapInjector` : + +1. **Résolution de la `CommandMap`** : + - Sur Paper / Bukkit moderne, `Bukkit.getServer().getCommandMap()` est interrogé. + - Sur les versions antérieures, un fallback par réflexion inspecte l'attribut `commandMap` de `CraftServer`. +2. **Accès à la table des commandes connues (`knownCommands`)** : + - L'injecteur accède à la table interne `Map` de `SimpleCommandMap`. +3. **Enregistrement de la commande racine et de ses alias** : + - La commande est enregistrée avec le préfixe du plugin (ex: `/monplugin:commande`). + - L'alias principal est rendu disponible directement (ex: `/commande`). + +--- + +## 2. Enveloppe Bukkit (`PaperCommandWrapper`) + +Pour que le serveur Minecraft sache router les commandes vers le moteur `betterMcCommands`, chaque `Command` racine est enveloppée dans une instance de `PaperCommandWrapper` qui étend `org.bukkit.command.Command` : + +* `execute(CommandSender sender, String label, String[] args)` -> Transmet l'appel au `CommandDispatcher`. +* `tabComplete(CommandSender sender, String alias, String[] args)` -> Calcule les suggestions via `CommandDispatcher.tabComplete(...)` et les retourne au client. + +--- + +## 3. Désenregistrement à Chaud & Hot Reload + +Lorsqu'un plugin est désactivé (`onDisable()`) ou rechargé, il est primordial de retirer proprement les commandes pour éviter les fuites de mémoire et les conflits d'alias. + +L'appel à `commandsManager.unregisterAll()` : +1. Dé-enregistre chaque wrapper de la `CommandMap`. +2. Supprime toutes les entrées associées de la table `knownCommands`. +3. Réinitialise le `CooldownManager` et le `ConfirmationManager`. +4. Vide le bus d'événements `CommandEventManager`. + +--- + +## 4. Isolation et Multi-Instances + +Chaque plugin utilisant `betterMcCommands` peut posséder sa propre instance indépendante grâce à : + +```java +BetterMcCommands pluginA = BetterMcCommands.create(monPluginA); +BetterMcCommands pluginB = BetterMcCommands.create(monPluginB); +``` + +Chaque instance possède son propre namespace, son propre bus d'événements et sa propre table de cooldowns/confirmations sans interférence mutuelle.