75d2fa866d
New fr.luc.crcore.team.config module: - TeamSetting<T> (typed, with key/type/default/parser/serializer; factories ofBoolean/ofInt/ofString/ofEnum). - TeamSettings registry: 8 standard settings (FRIENDLY_FIRE, PVP_PROTECTION_SECONDS, MAX_SIZE, MIN_SIZE, RESPAWN_AT_TEAM_SPAWN, TEAM_CHAT_ENABLED, SHOW_TAG_ABOVE_HEAD, TEAM_COLOR_IN_NAME), extensible via register() for game plugins. - TeamConfigService interface with cascade get(team, setting) → per-team override (SQLite) → global YAML → hard default. Persists per- team via TeamRepository.save(), global via YamlConfiguration.save(). - YamlTeamConfigService default impl with bundled crcore-team-config.yml. Storage: - Team.getSettings() Map<String, Object> for per-team overrides. - New SQLite table crcore_team_settings (team_id, key, value, type) with load + write-through persist in SqliteTeamRepository. - Global YAML <plugin>-team-config.yml in dataFolder, auto-created at first boot (template from game plugin's resource of the same name takes priority). New reusable GUI framework fr.luc.crcore.gui: - AbstractInventoryGui (implements InventoryHolder, rebuild() abstract, setButton/setDecoration/clearSlot helpers, onClose hook, openTo()). - GuiClickHandler FunctionalInterface. - GuiListener (single Bukkit listener, detects via getHolder(), ALWAYS cancels clicks even on slots without handlers). - GuiItems builder (named/of/filler + lore/amount/build, '&' color codes translated). Concrete settings GUIs (fr.luc.crcore.team.config.gui): - AbstractSettingsGui base renderer: 27 slots, settings in row 2, booleans = LIME_DYE / GRAY_DYE toggle, integers = BOOK with left +1 / right -1 (shift × 10), strings/enums display-only. - GlobalSettingsGui: writes to YAML on each change. - TeamSettingsGui: writes to per-team overrides, "override active" flag in lore when value differs from global, "Reset all overrides" footer button. New /core team settings [team] subcommand: - No arg → GlobalSettingsGui (perm crcore.team.settings.global). - With arg → TeamSettingsGui (perm crcore.team.settings). - Player-only (Bukkit needs HumanEntity to open inventory). - Lives under /core team to stay modular (objective: split into modules later; everything team-related under /core team). CRCore: buildTeamConfigService() override point, teamConfig()/getTeamConfig() getters, GuiListener.registerOn(plugin) at enable(). CoreCommand, TeamGroupSubCommand and CoreReloadSubCommand extended to receive TeamConfigService. /core reload now reloads messages + broadcasts + team-config. Docs: new section 10 "Paramètres d'équipe", new decisions logged, setup.md tree updated, two new diagrams (team-config + gui). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
36 KiB
36 KiB
Journal des décisions
Format léger : une décision = un titre + contexte + choix + raison.
2026-06-08 — Cible Minecraft 1.16.5 / Paper
- Choix : utiliser l'API Paper 1.16.5 (
paper-api) plutôt que Spigot. - Raison : Paper est un sur-ensemble de Spigot, expose plus d'API utiles pour les évènements, et reste compatible avec un serveur Spigot 1.16.5.
- Conséquence : un serveur Paper 1.16.5 est recommandé pour le test.
2026-06-08 — Java 16
- Choix :
maven.compiler.source/target = 16. - Raison : Paper 1.16.5 tourne avec un JDK 8–16. Java 16 donne accès aux records, pattern matching simple, etc., tout en restant exécutable sur un serveur 1.16.5.
2026-06-08 — docs/ = source de vérité
- Choix : toutes les décisions, règles de gameplay, commandes et idées
doivent être notées dans
docs/avant ou pendant l'implémentation. - Raison : éviter la dérive entre intention et code, garder une trace partageable des échanges.
2026-06-08 — Code en anglais standard
- Choix : tout le code (classes, méthodes, attributs, variables) est écrit
en anglais. La doc utilisateur (
docs/, messages joueurs) reste en français. - Raison : conventions standards du monde Java/Bukkit, lisibilité par tout développeur, cohérence avec l'API Paper.
2026-06-08 — Architecture en couches pour le domaine
- Choix : séparer chaque domaine fonctionnel (ex.
team) en :- Interfaces d'abstraction (ex.
Identifiable,Named,Repository<T>,TeamRepository,TeamService). - Enums pour les ensembles fermés (
TeamRole,TeamColor). - Classe abstraite commune
AbstractEntity(gèreid+equals/hashCode). - Classes concrètes : entités (
Team,TeamMember), implémentations (InMemoryTeamRepository,TeamServiceImpl). - Exceptions dédiées avec hiérarchie (
TeamException➜TeamAlreadyExistsException,TeamNotFoundException).
- Interfaces d'abstraction (ex.
- Raison : testabilité (mocker une interface), évolutivité (changer le backend de persistance sans toucher au service), lisibilité.
2026-06-08 — Persistence en mémoire pour démarrer
- Choix :
InMemoryTeamRepository(Map<UUID, Team>) comme première implémentation. - Raison : permet d'avancer sur le gameplay sans dépendre d'un schéma de stockage. À remplacer par une implémentation YAML/SQLite/Postgres plus tard sans toucher au service.
2026-06-08 — Team = entité mutable, TeamMember = quasi-immutable
- Choix :
Teammute (ajouts/retraits de membres, transfert de leadership) ;TeamMemberest immuable, sa transition de rôle passe parwithRole(...)qui renvoie une nouvelle instance. - Raison : un
TeamMemberest identifié par sonplayerId; il est plus simple de raisonner sur des états successifs en remplaçant l'instance. LeSet<TeamMember>reste cohérent carequals/hashCodene dépend que duplayerId.
2026-06-08 — Renommage du projet : CitesPlugin ➜ CR-Core
- Choix : le projet devient CR-Core, un plugin "noyau" réutilisable.
Les anciens packages
fr.luc.citesplugin.*sont déplacés sousfr.luc.crcore.*. L'ancienCitesPlugin(le jeu lui-même) deviendra un futur plugin séparé qui déclareradepend: [CR-Core]. - Raison : centraliser les briques transverses (équipes, futurs scores, profils joueurs, etc.) pour pouvoir les réutiliser sur plusieurs plugins de jeu sans dupliquer le code.
- Conséquence : downstream consomme CR-Core soit via la façade statique
fr.luc.crcore.CR(ex.CR.teams()), soit via leServicesManagerBukkit (getServicesManager().load(TeamService.class)).
2026-06-08 — Distribution : CR-Core devient une librairie Maven pure (révision)
- Révision des décisions précédentes "plugin Bukkit autonome" et "architecture en modules (PluginModule / ModuleRegistry)".
- Choix : CR-Core n'est plus un plugin Bukkit, c'est une librairie
(
jar) sansplugin.ymlniJavaPlugin. Chaque plugin de jeu consomme la lib en dépendance Maven, instancie lui-même ses services (new TeamServiceImpl(...)) et garde son propre registre. - Raison : la complexité du système de modules + façade statique +
ServicesManagern'apporte rien quand on est dans un contexte d'events ponctuels où chaque jeu vit dans sa propre session. La lib est nettement plus simple à utiliser et à tester. Le partage d'état entre jeux n'est pas un besoin réel pour les events entre amis. - Conséquence : suppression de
CRCorePlugin,CR(façade),plugin.yml,PluginModule,AbstractModule,ModuleRegistry,TeamModule. Suppression aussi demaven-shade-plugincôté core (c'est le plugin de jeu qui décide de shader ou non).
2026-06-08 — Overridabilité par défaut
- Choix : toutes les classes du noyau sont conçues pour être étendues.
Pas de
finalsur les classes ; méthodes-clés enprotected; factoriesnewXxx(...)pour substituer des sous-classes ; hooksonBeforeXxx/onAfterXxxautour des opérations importantes. - Exemples sur
TeamServiceImpl:newTeam,validateName,validateTag,validateLeader,onBeforeSave,onAfterCreate,onBeforeDissolve,onAfterDissolve,onMemberAdded,onMemberRemoved,onLeadershipTransferred. SurTeam:newMember. - Raison : le noyau doit fournir un comportement par défaut "qui marche", mais chaque jeu doit pouvoir greffer ses propres règles (logging, persistance custom, validations supplémentaires, hooks d'events Bukkit) sans réécrire toute la classe.
2026-06-08 — Framework de commandes intégré
- Choix : CR-Core fournit
Command(interface),AbstractCommand(classe abstraite avec tous les builders),BaseCommand(top-level Bukkit-aware, conteneur de sous-commandes),SubCommand(feuille), plusCommandContext,CommandResult,ArgumentType<T>et un jeu de types built-in (STRING,INTEGER,DOUBLE,BOOLEAN,ONLINE_PLAYER,enumOf(...),choice(...)). - Raison : chaque plugin de jeu aura ses commandes ; mutualiser le routage args[0]→sous-commande, les checks permission / player-only, le parsing des arguments et la tab-completion évite de redévelopper le même squelette à chaque fois.
- Non-choix : pas d'annotations (
@Command,@Argument) — la résolution par réflexion ajouterait une dépendance d'outillage et compliquerait le debug. L'API builder reste assez concise. - Découplage : le framework ne contient pas de commande "team" prête à l'emploi. C'est au plugin de jeu de définir ses commandes en utilisant les briques. Ça permet à chaque jeu d'avoir ses propres permissions, messages, et règles métier.
2026-06-08 — Visibilité publique / privée des équipes
- Choix : ajout d'un enum
TeamVisibility { PUBLIC, PRIVATE }porté parTeam. Une équipePUBLICpeut être rejointe par un joueur viaTeamService.joinTeam(teamId, playerId); une équipePRIVATEne peut recevoir des membres que viaaddMemberappelé par le chef. - Défaut :
PRIVATEà la création (le chef garde le contrôle ; il faut une action explicite pour ouvrir l'équipe au public). Une surchargecreateTeam(..., visibility)permet de créer directement enPUBLIC. - Nouvelle exception :
TeamAccessException extends TeamException— levée quand un auto-join est refusé (team privée, ou joueur déjà dans une équipe, ou refus custom dansvalidateJoinable). - Nouveaux hooks :
validateJoinable(team, playerId),onPlayerJoined(team, member),onVisibilityChanged(team, oldV, newV). - Décision écartée pour l'instant : un mode
INVITE_ONLYavec un système d'invitations pendantes (Player A invite Player B → B accepte). Pas indispensable pour démarrer ; le chef peut déjà ajouter directement viaaddMember. À reconsidérer si le besoin remonte.
2026-06-08 — Scores nommés (Map<String, Integer>) plutôt qu'un score unique
- Choix : chaque équipe porte un
Map<String, Integer>de scores nommés ("kills","objectives","global", …) plutôt qu'un seulint score. - Raison : tous les jeux n'ont pas la même métrique. BedWars a "beds_broken"
- "final_kills" ; un mode Capture the Flag a "flags" + "kills" ; un mode
simple peut n'utiliser que
"global". Un Map évite d'imposer un schéma fixe et reste compact pour les cas mono-score.
- "final_kills" ; un mode Capture the Flag a "flags" + "kills" ; un mode
simple peut n'utiliser que
- Conséquence : les noms de scores sont libres et non typés au niveau du noyau ; chaque jeu choisit ses propres noms. Pour de la sûreté, un jeu peut exposer un enum ou des constantes côté plugin.
- Type :
Integerplutôt queLongouDouble. Suffisant pour des scores de match (limite ~2 milliards). Si un jeu a besoin de Long ou Double, il peut wrapper et stocker un encodage custom ; ou bien on étendra l'API plus tard.
2026-06-08 — Classements : ranking par score + ranking global (= somme)
- Choix :
TeamServiceexposegetRankingByScore(scoreName)etgetGlobalRanking(). Le ranking global est calculé comme la somme de tous les scores nommés de chaque équipe. - Raison : couvre les deux cas usuels (« qui a le plus de kills ? » et
« qui a la meilleure perf globale ? ») sans imposer de pondération par
défaut. Un jeu qui veut une formule custom (pondérée, ratio, …) override
rank(ToIntFunction<Team>)ou ajoute une méthode de service dans sa propre sous-classe. - Format du résultat :
record TeamRanking(int rank, Team team, int score). Lerankest 1-based, le tri est descendant sur le score, le tiebreaker est alphabétique (case-insensitive) sur le nom de l'équipe. - Records : choix d'un record Java 16 plutôt qu'une classe immutable
manuelle — moins de boilerplate,
equals/hashCode/toStringgratuits. Les records étantfinal, un jeu qui veut un type custom devra wrapper et overridenewRanking(...)au niveau du service.
2026-06-08 — Spawn point par équipe (Bukkit Location)
- Choix : chaque
Teampeut avoir unLocationBukkit optionnel comme point de spawn. Stocké en mémoire pour l'instant. - Clonage défensif :
getSpawnPoint()retourne unOptional<Location>où laLocationest clonée ; idem à l'entrée danssetSpawnPoint.Locationétant mutable côté Bukkit, ça évite que du code externe modifie accidentellement le spawn en faisantteam.getSpawnPoint().get().setX(...). - Persistance différée :
Locationn'est pas trivialement sérialisable (référence auWorld). On utiliseraConfigurationSerializablequand on branchera un repo fichier ; pour l'instant, leInMemoryTeamRepositorys'en moque. - Pas de téléport intégré : le noyau ne fournit pas
teleportToSpawn(...). C'est au plugin de jeu d'enchaînerplayer.teleport(team.getSpawnPoint())s'il veut. La lib reste purement "data + règles".
2026-06-08 — Scores joueurs : domaine player indépendant du domaine team
- Choix : ajout d'un domaine
fr.luc.crcore.playercomplet et parallèle au domaineteam:PlayerProfile(entité identifiée par l'UUID Bukkit),PlayerProfileService,PlayerProfileRepository,InMemoryPlayerProfileRepository,PlayerRanking(record),PlayerException/PlayerProfileNotFoundException. - Pourquoi pas sur
TeamMember:TeamMemberest immuable (transitions de rôle viawithRole), et un joueur peut changer/quitter une équipe — son profil doit persister. Mettre les scores surTeamMemberaurait couplé la durée de vie du score à l'appartenance à l'équipe. - Auto-création :
addScore/setScorecréent le profil automatiquement s'il n'existe pas (getOrCreateProfile(playerId)). Pas besoin d'appeler explicitement unregister(playerId)avant de tracker un score. - Symétrie : les noms de méthodes, hooks et factories reflètent
exactement le domaine team (
newProfile/newTeam,newRanking/newRanking,rank(scoreFn),onScoreChanged,getRankingByScore,getGlobalRanking, etc.).
2026-06-08 — Interface ScoreHolder mutualisée
- Choix : extraction d'une interface
fr.luc.crcore.common.ScoreHolderqui déclare le contrat de scoring (getScore,addScore,setScore,resetScore,getTotalScore, etc.).TeametPlayerProfilel'implémentent. - Raison : documenter le contrat et permettre du code générique côté
plugin de jeu (ex.
ScoreHolder.getTotalScore()traité uniformément pour l'affichage). Pas de classe abstraite partagée pour éviter le couplage serré (Team et PlayerProfile ont des cycles de vie très différents) ; on reste sur deux implémentations indépendantes mais avec un contrat commun. - Pas de
Scoreboardaggregate : envisagé un composantScoreboardcomposé dans Team et PlayerProfile, mais ça aurait imposé une indirection pour ~8 méthodes simples. Choix actuel : duplication contrôlée des implémentations (Map + getters/setters), interface commune pour le contrat.
2026-06-09 — CRCore = bootstrap library, pas un plugin
- Choix : CR-Core reste une librairie (pas de
plugin.yml, pas deJavaPlugin). Le plugin de jeu downstream instancienew CRCore(this)dans sononEnable()et appelle.enable()— c'est ce qui câble SQLite, services, commandes et events. - Alternative écartée : faire de CR-Core un plugin standalone (à installer côté serveur). Refusé pour deux raisons : (1) chaque jeu a son propre état (registre d'équipes, scores) — on ne veut pas partager entre jeux par défaut ; (2) la friction de déploiement (2 jars sur le serveur) est inutile pour des plugins shadés.
- Conséquence : chaque plugin de jeu shade CR-Core, a sa propre DB SQLite
dans son
dataFolder, et déclare la commande Bukkit racine (corepar défaut) dans sonplugin.yml.
2026-06-09 — Sous-commandes imbriquées récursives
- Choix :
AbstractCommandporte la table des sous-commandes (pas seulementBaseCommand).SubCommandpeut donc avoir ses propres sous-commandes (récursion). Routage via la méthodedispatch(...)récursive. - Raison : c'est ce qui permet
/core team create(3 niveaux : root / group / leaf). Sans ça, il faudrait flatter en/core team-createou faire du routage manuel dans chaque groupe. - Conséquence :
BaseCommandne fait plus que pont Bukkit (CommandExecutor/TabCompleter→dispatch) ; toute la logique de routage vit dansAbstractCommand.
2026-06-09 — Override par sous-classe + replaceSubCommand
- Choix :
AbstractCommand.replaceSubCommand(name, newSub)permet aux plugins de jeu de remplacer une feuille (ex.TeamCreateSubCommand) par leur propre implémentation, sans tout recâbler. - Raison : le user a explicitement demandé "Les futures plugins ne
feront qu'override les fonctions si besoin". Cette méthode + le fait que
les classes ne soient pas
finalcouvre les deux patterns :- Remplacement par instance :
team.replaceSubCommand("create", new MyCreate(svc)) - Override par héritage :
extends TeamCreateSubCommand+super.execute(ctx)
- Remplacement par instance :
2026-06-09 — Évènements Bukkit : post-only, non-cancellable
- Choix : tous les évènements CR-Core (team + player) sont post-events,
tirés via les hooks
on*après commit. Aucun n'implémenteCancellable. - Raison : la validation pré-action vit côté service dans les hooks
validate*(overridables). Mélanger pré-cancellable côté event et hooks côté service dédoublerait les points de blocage. Pour bloquer un comportement, le pattern est : override le hookvalidate*du service. - Boilerplate : chaque event a sa propre
HandlerListstatique (contrainte Bukkit, pas de moyen de partager via héritage). 12 events = 12 occurrences du même pattern, accepté pour rester idiomatique Bukkit.
2026-06-09 — SQLite write-through cache pour les repositories
- Choix :
SqliteTeamRepositoryetSqlitePlayerProfileRepositoryétendent leurs jumeauxInMemory*et overridentsave/deletepour persister synchronement vers SQLite. Au démarrage,loadAll()recharge tout le state depuis la DB dans le cache mémoire. - Raison : les lectures (findAll, findByName, classements en
Collection<Team>.stream().sorted(...)) restent rapides — pas de hit DB. Les écritures vont en DB synchronement (acceptable au rythme des actions joueur, qui sont rares à l'échelle d'un event entre amis). - Approche delete + reinsert pour les collections : sur
save(), on remplace en bloc lesteam_membersetteam_scoresd'une équipe (DELETE puis INSERT). Plus simple et moins bug-prone qu'un diff fin, et négligeable en perf pour des équipes de quelques joueurs.
2026-06-09 — Type Database minimaliste plutôt qu'un ORM
- Choix :
Databaseexpose 4 méthodes (execute / update / queryOne / query) + unTableBuilderfluide. Pas d'ORM, pas d'annotations d'entités, pas de DSL SQL. - Raison : un ORM ajouterait une dépendance lourde (Hibernate / jOOQ / …), un poids de classloading non négligeable côté serveur Bukkit, et abstrairait des opérations qu'on veut garder triviales et lisibles. SQL brut + PreparedStatement est largement suffisant pour les volumes d'un serveur d'event.
- Le
TableBuilderexiste pour répondre au "pouvoir rapidement et simplement créer des tables" — c'est l'API la plus user-friendly à proposer. Pour les cas avancés (FOREIGN KEY, contraintes composites), l'utilisateur passe pardb.execute("CREATE TABLE ...")direct.
2026-06-09 — Préfixe crcore_ sur toutes les tables internes
- Choix :
crcore_teams,crcore_team_members,crcore_team_scores,crcore_player_profiles,crcore_player_scores. - Raison : éviter les collisions avec les tables custom que les plugins
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
<dataFolder>/crcore.db) ; le préfixe isole proprement.
2026-06-09 — Refonte permissions + modèle admin/chef/joueur
- Choix : chaque sous-commande
/core team <action>a sa propre permissioncrcore.team.<action>. Trois niveaux fonctionnels :- Admin (permission seule, cible une team par argument) :
create,delete,setleader,score. - Chef (permission + check chef dans
execute()) :add,remove,transfer,visibility,setspawn. - Joueur (permission seule, défaut « tout le monde » côté LuckPerms si
voulu) :
join,leave,info,list,top.
- Admin (permission seule, cible une team par argument) :
- Aliases courts supprimés :
c(create),i(info),t(team),j(join),vis(visibility),disband/dissolve(delete),kick/expel(remove), etc. Plus que les noms longs. Raison : réduire la friction d'apprentissage et la confusion (les game plugins ont leurs propres noms, l'aliasing devient un bruit). deletedevient admin :/core team delete <team>(au lieu de l'ancien/core team deletequi ciblait l'équipe du chef). Cohérent aveccreatequi est aussi admin.
2026-06-09 — Team peut être leaderless
- Choix :
Team.leaderIddevient nullable.getLeaderId()renvoieOptional<UUID>,getLeader()renvoieOptional<TeamMember>. NouveauhasLeader()etisLeader(UUID)pour les checks. - Raison : le modèle admin requiert qu'on puisse créer une équipe sans chef et l'assigner ensuite via {@code setLeader}. Avant, créer une équipe imposait de connaître l'UUID du chef.
- Constructeurs ajoutés :
new Team(id, name, tag, color)— leaderless, PRIVATEnew Team(id, name, tag, color, visibility)— leaderless avec visibilité- Les constructeurs avec leaderId acceptent maintenant
null.
Team.setLeader(playerId): assigne un chef à n'importe quel moment. Si la team a déjà un chef, il est démis en MEMBER. Si le nouveau n'est pas membre, il est auto-ajouté.Team.transferLeadership(playerId): conserve sa sémantique stricte (chef→chef, membre déjà existant). LèveIllegalStateExceptionsi la team est leaderless. Utilisé par la commande/core team transfer(chef).TeamLeadershipTransferEvent.getOldLeaderId()renvoie maintenantOptional<UUID>(vide si la team était leaderless avant l'opération).- Schéma SQLite : la colonne
crcore_teams.leader_idn'a plus la contrainteNOT NULL. Migration automatique sur nouvelle base — pour les bases existantes, ALTER TABLE manuel ou suppression du fichier (les bases d'event sont jetables).
2026-06-10 — Settings d'équipe : cascade per-team → global → default + GUI
- Choix : nouveau module
fr.luc.crcore.team.configavec :TeamSetting<T>typé (factoriesofBoolean,ofInt,ofString,ofEnum) — chaque setting porte sa clé, son type, son default et sa sérialisation YAML/SQL.TeamSettingsregistry des 8 settings standards (FRIENDLY_FIRE,PVP_PROTECTION_SECONDS,MAX_SIZE,MIN_SIZE,RESPAWN_AT_TEAM_SPAWN,TEAM_CHAT_ENABLED,SHOW_TAG_ABOVE_HEAD,TEAM_COLOR_IN_NAME), extensible viaTeamSettings.register(...)pour les game plugins.TeamConfigService(interface) +YamlTeamConfigService(impl).
- Cascade de résolution : per-team → global → hard default. Garantie
non-null grâce au default. La couche per-team est stockée dans
{@code Team.getSettings()} (Map<String, Object>) persistée en SQLite ;
la couche globale dans
<plugin>-team-config.yml; les defaults sont des constantes Java. - Stockage per-team SQLite : nouvelle table
crcore_team_settings(team_id, key, value, type). Le type tag (bool/int/str) permet de reconstruire le type Java au load sans réflexion. - Settings custom (game plugin) : le game plugin peut faire
TeamSettings.register(MON_SETTING)dans son onEnable() pour l'enregistrer ; il apparaîtra automatiquement dans les GUI globaux et per-team, et sera persisté comme les standards. - Pas d'application automatique : CR-Core ne fait que stocker /
exposer les settings. C'est au game plugin d'écouter les events Bukkit
pertinents (ex.
EntityDamageByEntityEvent) et de consulterconfig.get(team, FRIENDLY_FIRE)pour appliquer la règle. CR-Core ne veut pas hardcoder des semantics gameplay.
2026-06-10 — Framework GUI réutilisable (fr.luc.crcore.gui)
- Choix : module GUI générique avec
AbstractInventoryGui implements InventoryHolder(base abstraite),GuiClickHandler(FunctionalInterface),GuiListener(un seul Listener Bukkit pour TOUS les GUI CR-Core),GuiItems(builder fluide d'ItemStackavec codes couleur). - Détection par holder :
event.getInventory().getHolder() instanceof AbstractInventoryGui— propre, sans titre/UUID custom, marche même après un translate. - Click toujours annulé : le
GuiListenercancel TOUT clic dans un GUI CR-Core (avant invocation du handler) — l'utilisateur ne peut jamais déplacer un item du GUI, même sur un slot sans handler. - Réutilisable : c'est un framework, pas un GUI métier. Tout futur
GUI (settings, kits, classements interactifs, etc.) hérite
d'
AbstractInventoryGui.
2026-06-10 — /core team settings (global = sans arg, per-team = avec arg)
- Choix : commande unique
/core team settings [team]qui multiplexe :- Sans arg → ouvre
GlobalSettingsGui(permcrcore.team.settings.global). - Avec arg
team→ ouvreTeamSettingsGui(permcrcore.team.settings).
- Sans arg → ouvre
- Pas
/core settingsau top-level — l'objectif est de séparer plus tard en modules (team, score, kits, …). Tout ce qui touche les teams reste sous/core team. - Player-only : Bukkit a besoin d'un
HumanEntitypour ouvrir un inventaire. Pas de fallback console. - Mécaniques : booléens → toggle, entiers → clic gauche +1/right -1 (shift = ×10), strings/enums → édition différée au YAML (V1).
- GUI per-team : un bouton "Reset tous les overrides" qui efface tous les per-team de l'équipe pour les faire retomber sur le global.
2026-06-10 — Système de broadcasts configurables + /core reload
- Choix : nouveau module
fr.luc.crcore.broadcastavecBroadcastService+BroadcastAudienceenum +BroadcastContextdata class +YamlBroadcastServiceimpl. Un listener Bukkit interne (CRCoreBroadcastListener) écoute les 12 events CR-Core et les traduit en appelsbroadcast(eventKey, ctx). - Modèle « un seul fichier par plugin » identique à messages :
<plugin-dataFolder>/<plugin-name-lowercase>-broadcasts.yml. Defaults bundlés dans le jar àcrcore-broadcasts.yml, copiés au premier boot (avec priorité au template du plugin de jeu sous le même nom s'il en fournit un). - Séparation routes / templates :
- Routes = qui reçoit quoi =
<plugin>-broadcasts.yml(liste d'audiences par event) - Templates = quel texte =
<plugin>-messages.yml(clés<eventKey>.broadcast) - L'admin peut modifier l'un sans toucher à l'autre. Modulaire.
- Routes = qui reçoit quoi =
- 5 audiences :
NONE,LEADER,TEAM,ADMIN,ALL. Multi-cibles via liste, union sans doublon. - Permission ADMIN :
crcore.broadcast.admin(granular, configurable côté LuckPerms). - Listener Bukkit interne :
CRCoreBroadcastListenerest instancié et enregistré dansCRCore.enable(). Les game plugins n'ont rien à faire pour bénéficier du broadcast des events natifs CR-Core ; pour leurs propres events, ils appellentcore.broadcasts().broadcast(...). - Pas de cancellation : le broadcast est post-event ; si une route est mal configurée, on ne casse pas la logique métier — au pire un message non envoyé ou envoyé trop large.
- Nouvelle commande
/core reload: permissioncrcore.reload, rechargemessages+broadcastsdepuis les fichiers user. Les defaults en jar restent fixes. Hot reload utile en dev / pour ajuster les routes sans restart. - Override de l'impl :
CRCore.buildBroadcastService(messages)estprotected— comme pour les autres services.
2026-06-09 — Réorganisation packages : impl/ et exception/ séparés
- Choix : pour chaque domaine (
team,player,message), les implémentations passent dans un sous-packageimpl/et les exceptions dans un sous-packageexception/. Le top-level du package ne contient plus que les contrats publics (interfaces, entités, enums, values). - Conséquences sur les FQN publics (importer côté plugin de jeu si on les
utilise) :
fr.luc.crcore.team.TeamException→fr.luc.crcore.team.exception.TeamException(et ses 3 sous-classes)fr.luc.crcore.team.TeamServiceImpl→fr.luc.crcore.team.impl.TeamServiceImpl(idemBukkitEventFiring*,InMemory*Repository,Sqlite*Repository)fr.luc.crcore.player.PlayerException→fr.luc.crcore.player.exception.PlayerException(etPlayerProfileNotFoundException)fr.luc.crcore.player.*Impl/*Repositoryimpl →fr.luc.crcore.player.impl.*fr.luc.crcore.message.YamlMessagesService→fr.luc.crcore.message.impl.YamlMessagesService
- Inchangés : tous les enums, entités, interfaces de service et de repo, ranking records, events (qui étaient déjà dans un sous-package {@code event/}).
- Raison : lisibilité. Un dev qui ouvre
fr.luc.crcore.team/voit immédiatement les contrats (Team, TeamService, TeamRepository, enums, events) sans se faire noyer par les impls. Pour overrider, il sait où chercher (impl/). Pour catch une exception, il sait où chercher (exception/). - Convention top-level vs impl/ :
- Top-level = ce qu'un consommateur doit connaître pour utiliser ou étendre l'API : interfaces, entités, enums, values, events.
- impl/ = ce que CR-Core fournit par défaut, qu'un game plugin peut swap. C'est aussi là que vivent les sous-classes utilisées en interne par le bootstrap (BukkitEventFiringServiceImpl, SqliteRepository).
- Pas appliqué à
database/,command/etcommon/: ces packages sont déjà petits et bien lisibles ; ajouterimpl/à 3 fichiers serait cosmétique.
2026-06-09 — MessagesService : YAML externalisable, un seul fichier par plugin
- Choix : nouveau module
fr.luc.crcore.messageavec une interfaceMessagesServiceet une implYamlMessagesService. Toutes les chaînes utilisateur des commandes built-in passent par ce service. - Modèle « un seul fichier par plugin » :
- Defaults CR-Core embarqués dans le jar à
resources/crcore-messages.yml— jamais écrits sur disque, juste chargés en mémoire comme couche de fallback. - Fichier user unique :
<plugin-dataFolder>/<plugin-name-lowercase>-messages.yml. Auto-créé au premierenable()à partir du template du plugin de jeu s'il en bundle un sous le même nom, sinon à partir des defaults CR-Core. - Lecture : le fichier user écrase les defaults sur les mêmes clés ; une clé manquante retombe automatiquement sur le default CR-Core (donc une future release CR-Core qui ajoute une clé marche sans intervention admin).
- Defaults CR-Core embarqués dans le jar à
- Pourquoi un seul fichier : (1) UX admin — il édite un fichier, pas deux ; (2) le plugin de jeu peut pré-remplir le template avec ses overrides + ses propres messages en bundlant simplement son fichier homonyme dans les ressources ; (3) zéro maintenance pour les clés inchangées — elles restent en jar.
- Substitution : placeholders
{name}style, varargs key/value, codes couleur&traduits automatiquement. - Override de l'impl :
CRCore.buildMessagesService()protected, surchargeable pour passer à une autre source (DB, microservice, etc.). - Pas de programmatique-only : le service supporte
set(key, template)en mémoire pour des cas dynamiques, mais le mode principal reste le YAML pour l'éditabilité par l'admin sans recompile.
2026-06-09 — Toutes les commandes "chef" deviennent admin (révision)
- Révision de la décision "Refonte permissions + modèle admin/chef/joueur" prise plus tôt aujourd'hui.
- Choix : le rôle chef n'apporte plus aucun privilège de commande pour
l'instant. Toutes les opérations de gestion d'équipe (
add,remove,transfer,visibility,setspawn) deviennent admin :- Signature avec
<team>en argument (au lieu d'implicite "ma team"). - Permission
crcore.team.<action>requise. - Plus de check
isLeader(...)dansexecute().
- Signature avec
- Raison : le user a explicitement décidé que pour l'instant le chef
n'a pas plus de privilèges qu'un joueur lambda côté commandes. Le rôle
LEADERreste dans le modèle de données (utile pour les game plugins qui pourraient l'exploiter via l'API, ou pour de futures commandes), mais il ne gate plus rien au niveau du framework de commandes. - Conséquences :
TeamRemoveSubCommand: refuse de retirer le chef (l'admin doitsetleaderd'abord). Pas un check chef, juste une garde de cohérence.TeamTransferSubCommand: devient l'équivalent admin "strict" desetleader(membre existant uniquement). Les deux cohabitent ; doc dit quand préférer l'un ou l'autre.TeamSetSpawnSubCommand: resteplayerOnlycar nécessite laLocationde l'exécutant — mais c'est désormais l'admin qui se place à l'endroit voulu et tape/core team setspawn <team>.
2026-06-09 — Intégration PlaceholderAPI (optionnelle, auto-détectée)
- Choix :
CRCore.enable()détecte la présence du plugin PlaceholderAPI viapluginManager.getPlugin("PlaceholderAPI")et enregistre automatiquementCRCorePlaceholderExpansionsi présent. Aucune action requise côté plugin de jeu. - Dépendance Maven :
me.clip:placeholderapi:2.11.6en scopeprovided(depuishttps://repo.extendedclip.com/...). Le jar PAPI n'est PAS embarqué — c'est un plugin runtime indépendant. - Indirection de chargement : la méthode privée
doRegisterPlaceholderHook()isole la référence àCRCorePlaceholderExpansion. Si PAPI est absent, la méthode n'est jamais appelée et le bytecode référençantme.clip.placeholderapi.*n'est pas vérifié → pas deNoClassDefFoundError. - Placeholders exposés :
- Team :
%crcore_team%,%crcore_team_name/tag/color/color_chat/size/visibility/leader_name/total_score%,%crcore_team_score_<name>% - Player :
%crcore_player_score_<name>%,%crcore_player_score_total%
- Team :
- Override :
CRCore.registerPlaceholderHook()estprotected— une sous-classe peut ajouter des placeholders ou skipper la hook.
2026-06-09 — Nouvelle commande /core team setleader
- Choix : ajout de
TeamSetLeaderSubCommand(/core team setleader <team> <player>). Permissioncrcore.team.setleader. Délègue àTeamService.setLeader(...). - Différence avec
/core team transfer:transfer: action chef, cible son équipe, le nouveau chef doit déjà être membre.setleader: action admin, cible n'importe quelle équipe, le nouveau chef peut être non-membre (auto-ajouté).
- Use cases couverts :
- Admin assigne un chef à une équipe leaderless fraîchement créée.
- Admin remplace le chef d'une équipe (le membre target est déjà dans l'équipe ou pas — peu importe).
- Admin "promote member up to leader" (cas explicitement demandé).
2026-06-09 — Bascule Java 16 → Java 11 (révision)
- Révision de la décision "Java 16" du 2026-06-08.
- Choix :
maven.compiler.source/target = 11. Le code se compile et s'exécute sur tout JDK 11+. - Raison : Java 11 reste très répandu côté serveurs Bukkit/Paper 1.16.5, et le coût de revenir en arrière est faible. On garde une cible plus conservatrice pour maximiser la compatibilité d'exécution.
- Conséquences sur le code :
- Les
record(Java 16) → classes immutables manuelles, avec mêmes noms d'accesseurs (rank(),team(), etc.) pour ne pas casser l'API publique. Concerné :TeamRanking,PlayerRanking, plus deux tuples internes (TeamRow,MemberRow) dansSqliteTeamRepository. - Le pattern matching
instanceof X x(Java 16) → classiqueinstanceof X+ cast explicite. Concerné :CommandContext.requirePlayer,Database.normalize. - Les switch expressions à flèche (
case X -> ..., Java 14) →switch (...) { case X: ...; break; }classique, ou chaînes if/else. Concerné :BaseCommand.handleResult,ArgumentTypes.BOOLEAN.parse,TeamScoreSubCommand.execute.
- Les
- Ce qui reste utilisé de Java 11 :
var(Java 10+),List.of()/Map.of()(Java 9+), interfaces avec méthodesdefault, lambdas, method references.
2026-06-09 — Enregistrement dynamique de la commande (plugin.yml optionnel)
- Choix :
CRCore.registerCommand()tente d'abordplugin.getCommand(name). Si la commande n'est pas déclarée dans leplugin.ymldu plugin hôte, fallback sur enregistrement dynamique via leCommandMapinterne du serveur (accédé par réflexion surCraftServer.commandMap). - Raison : un plugin de jeu peut maintenant utiliser CR-Core en
changeant uniquement le
pom.xml+ une instanciation deCRCore— zéro modification duplugin.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.Commandanonyme qui délègueexecute/tabCompleteauCoreCommand(qui est notreBaseCommand). On copie nom + aliases + description depuis leCoreCommandvers le wrapper. - Réflexion : stable sur Paper 1.16.5 ; le champ
commandMapdeCraftServerexiste depuis longtemps. Si une version future cassait l'accès, le code log un severe et continue (les autres features de CRCore restent fonctionnelles).