diff --git a/docs/decisions.md b/docs/decisions.md index 763dba4..53c9683 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -322,3 +322,24 @@ Format léger : une décision = un titre + contexte + choix + raison. de jeu créent dans la même DB. CR-Core et le plugin de jeu partagent le même fichier SQLite (par défaut `/crcore.db`) ; le préfixe isole proprement. + +## 2026-06-09 — Enregistrement dynamique de la commande (plugin.yml optionnel) + +- **Choix** : `CRCore.registerCommand()` tente d'abord + `plugin.getCommand(name)`. Si la commande n'est pas déclarée dans le + `plugin.yml` du plugin hôte, fallback sur enregistrement dynamique via le + `CommandMap` interne du serveur (accédé par réflexion sur + `CraftServer.commandMap`). +- **Raison** : un plugin de jeu peut maintenant utiliser CR-Core en + **changeant uniquement le `pom.xml` + une instanciation de `CRCore`** — + zéro modification du `plugin.yml`. Si l'utilisateur veut customiser + description/aliases côté Bukkit, il peut quand même déclarer la commande + dans plugin.yml ; CR-Core détecte et utilise cette déclaration. +- **Wrapper Bukkit** : on crée une `org.bukkit.command.Command` anonyme + qui délègue `execute` / `tabComplete` au `CoreCommand` (qui est notre + `BaseCommand`). On copie nom + aliases + description depuis le + `CoreCommand` vers le wrapper. +- **Réflexion** : stable sur Paper 1.16.5 ; le champ `commandMap` de + `CraftServer` existe depuis longtemps. Si une version future cassait + l'accès, le code log un severe et continue (les autres features de + CRCore restent fonctionnelles). diff --git a/docs/setup.md b/docs/setup.md index 5eba80e..b2ce415 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -48,13 +48,23 @@ name: MyGame main: fr.exemple.mygame.MyGamePlugin version: 1.0 api-version: 1.16 -commands: - core: - description: Commandes CR-Core - # aliases optionnels — équivalents au point de vue Bukkit - aliases: [cr, crcore] ``` +> **Pas besoin de déclarer la commande `core`** : CR-Core l'enregistre +> dynamiquement via le `CommandMap` du serveur quand elle est absente du +> plugin.yml. Si tu préfères la déclarer quand même (pour customiser la +> description ou les aliases côté Bukkit), tu peux ajouter : +> +> ```yaml +> commands: +> core: +> description: Commandes CR-Core +> aliases: [cr, crcore] +> ``` +> +> Dans ce cas, CR-Core détecte la commande déclarée et s'y branche +> normalement via `setExecutor` (pas d'enregistrement dynamique). + ### Code minimal ```java diff --git a/pom.xml b/pom.xml index ff60716..1fe7164 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,26 @@ + + + + gitea-luc-rival + https://gitea.luc-rival.fr/api/packages/admin/maven + + + gitea-luc-rival + https://gitea.luc-rival.fr/api/packages/admin/maven + + + ${project.artifactId}-${project.version} @@ -67,6 +87,57 @@ ${maven.compiler.target} + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar-no-fork + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + ${maven.compiler.source} + none + true + UTF-8 + UTF-8 + UTF-8 + false + + + + attach-javadocs + + jar + + + + diff --git a/src/main/java/fr/luc/crcore/CRCore.java b/src/main/java/fr/luc/crcore/CRCore.java index 2231502..86c968e 100644 --- a/src/main/java/fr/luc/crcore/CRCore.java +++ b/src/main/java/fr/luc/crcore/CRCore.java @@ -12,10 +12,16 @@ import fr.luc.crcore.team.InMemoryTeamRepository; import fr.luc.crcore.team.SqliteTeamRepository; import fr.luc.crcore.team.TeamRepository; import fr.luc.crcore.team.TeamService; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandMap; +import org.bukkit.command.CommandSender; import org.bukkit.command.PluginCommand; import org.bukkit.plugin.java.JavaPlugin; import java.io.File; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -147,16 +153,69 @@ public class CRCore { return new CoreCommand(teamService, playerProfileService); } + /** + * Enregistre {@link #coreCommand} sous le nom configuré, avec fallback + * dynamique. Stratégie : + *
    + *
  1. Si la commande est déclarée dans le {@code plugin.yml} du plugin + * hôte ({@code plugin.getCommand(name)} non null), on s'y branche + * classiquement via {@code setExecutor} / {@code setTabCompleter}.
  2. + *
  3. Sinon, on l'enregistre dynamiquement via le + * {@link CommandMap} interne du serveur (accédé par réflexion sur + * le champ {@code commandMap} de {@code CraftServer}). Le plugin + * hôte n'a alors rien à mettre dans son {@code plugin.yml}.
  4. + *
+ */ private void registerCommand() { - PluginCommand cmd = plugin.getCommand(config.getCommandName()); - if (cmd == null) { - plugin.getLogger().warning("Commande '" + config.getCommandName() + - "' absente du plugin.yml — /" + config.getCommandName() + - " ne sera pas reconnue."); + String name = config.getCommandName(); + PluginCommand cmd = plugin.getCommand(name); + if (cmd != null) { + cmd.setExecutor(coreCommand); + cmd.setTabCompleter(coreCommand); return; } - cmd.setExecutor(coreCommand); - cmd.setTabCompleter(coreCommand); + registerDynamicCommand(name); + } + + /** + * Enregistre la commande sans entrée plugin.yml en passant par le + * {@link CommandMap} interne du serveur. Wrappe le {@link CoreCommand} + * dans une {@link org.bukkit.command.Command} anonyme qui délègue à + * {@code onCommand} / {@code onTabComplete}. + */ + private void registerDynamicCommand(String name) { + try { + CommandMap commandMap = resolveCommandMap(); + final CoreCommand executor = coreCommand; + org.bukkit.command.Command bukkitCommand = new org.bukkit.command.Command(name) { + @Override + public boolean execute(CommandSender sender, String label, String[] args) { + return executor.onCommand(sender, this, label, args); + } + + @Override + public List tabComplete(CommandSender sender, String alias, String[] args) { + List result = executor.onTabComplete(sender, this, alias, args); + return result != null ? result : Collections.emptyList(); + } + }; + bukkitCommand.setDescription(executor.getDescription()); + bukkitCommand.setAliases(executor.getAliases()); + commandMap.register(plugin.getName().toLowerCase(), bukkitCommand); + plugin.getLogger().info("Commande /" + name + " enregistrée dynamiquement (plugin.yml non requis)."); + } catch (Exception ex) { + plugin.getLogger().severe("Échec d'enregistrement dynamique de /" + name + " : " + ex.getMessage()); + } + } + + /** + * Récupère le {@link CommandMap} interne du serveur via réflexion sur + * {@code CraftServer.commandMap}. Stable sur Paper 1.16.5. + */ + private CommandMap resolveCommandMap() throws ReflectiveOperationException { + Field field = Bukkit.getServer().getClass().getDeclaredField("commandMap"); + field.setAccessible(true); + return (CommandMap) field.get(Bukkit.getServer()); } // ---- Getters ----