8b7cad3fce
Chef → admin: the chef role no longer grants any command privilege. All team-management subcommands now take <team> as an argument and are gated by their crcore.team.<action> permission only: - add <team> <player> - remove <team> <player> - transfer <team> <player> - visibility <team> <PUBLIC|PRIVATE> - setspawn <team> (still player-only — needs admin's location) The LEADER role is kept in the data model (Team / TeamMember) and remains usable by game plugins via the API, but does not unlock any default command. Future work can re-introduce chef-specific commands if needed. PlaceholderAPI: auto-detected at CRCore.enable(). If the PAPI plugin is present on the server, CRCorePlaceholderExpansion registers automatically; otherwise the lib runs without it (no NoClassDefFoundError thanks to the indirection through doRegisterPlaceholderHook). Placeholders exposed: - 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% Dependency: me.clip:placeholderapi:2.11.6, scope provided. New repo: https://repo.extendedclip.com/content/repositories/placeholderapi/. docs/features.md, decisions.md and the builtin-commands diagram updated to reflect the simpler admin/player two-tier model and the PAPI section. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
476 lines
26 KiB
Markdown
476 lines
26 KiB
Markdown
# 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ère `id` + `equals/hashCode`).
|
||
- **Classes concrètes** : entités (`Team`, `TeamMember`), implémentations
|
||
(`InMemoryTeamRepository`, `TeamServiceImpl`).
|
||
- **Exceptions** dédiées avec hiérarchie (`TeamException` ➜
|
||
`TeamAlreadyExistsException`, `TeamNotFoundException`).
|
||
- **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** : `Team` mute (ajouts/retraits de membres, transfert de leadership)
|
||
; `TeamMember` est immuable, sa transition de rôle passe par `withRole(...)`
|
||
qui renvoie une nouvelle instance.
|
||
- **Raison** : un `TeamMember` est identifié par son `playerId` ; il est plus
|
||
simple de raisonner sur des états successifs en remplaçant l'instance. Le
|
||
`Set<TeamMember>` reste cohérent car `equals/hashCode` ne dépend que du
|
||
`playerId`.
|
||
|
||
## 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 sous `fr.luc.crcore.*`.
|
||
L'ancien `CitesPlugin` (le jeu lui-même) deviendra un futur plugin séparé qui
|
||
déclarera `depend: [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 le `ServicesManager` Bukkit
|
||
(`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`) sans `plugin.yml` ni `JavaPlugin`. 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 +
|
||
`ServicesManager` n'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 de `maven-shade-plugin` cô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 `final` sur les classes ; méthodes-clés en `protected` ; factories
|
||
`newXxx(...)` pour substituer des sous-classes ; hooks `onBeforeXxx` /
|
||
`onAfterXxx` autour des opérations importantes.
|
||
- **Exemples sur `TeamServiceImpl`** : `newTeam`, `validateName`, `validateTag`,
|
||
`validateLeader`, `onBeforeSave`, `onAfterCreate`, `onBeforeDissolve`,
|
||
`onAfterDissolve`, `onMemberAdded`, `onMemberRemoved`,
|
||
`onLeadershipTransferred`. Sur `Team` : `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), plus
|
||
`CommandContext`, `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é par
|
||
`Team`. Une équipe `PUBLIC` peut être rejointe par un joueur via
|
||
`TeamService.joinTeam(teamId, playerId)` ; une équipe `PRIVATE` ne peut
|
||
recevoir des membres que via `addMember` appelé 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 surcharge
|
||
`createTeam(..., visibility)` permet de créer directement en `PUBLIC`.
|
||
- **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 dans `validateJoinable`).
|
||
- **Nouveaux hooks** : `validateJoinable(team, playerId)`,
|
||
`onPlayerJoined(team, member)`, `onVisibilityChanged(team, oldV, newV)`.
|
||
- **Décision écartée pour l'instant** : un mode `INVITE_ONLY` avec 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 via
|
||
`addMember`. À 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 seul `int 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.
|
||
- **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** : `Integer` plutôt que `Long` ou `Double`. 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** : `TeamService` expose `getRankingByScore(scoreName)` et
|
||
`getGlobalRanking()`. 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)`.
|
||
Le `rank` est 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/toString` gratuits. Les
|
||
records étant `final`, un jeu qui veut un type custom devra wrapper et
|
||
override `newRanking(...)` au niveau du service.
|
||
|
||
## 2026-06-08 — Spawn point par équipe (Bukkit Location)
|
||
|
||
- **Choix** : chaque `Team` peut avoir un `Location` Bukkit optionnel comme
|
||
point de spawn. Stocké en mémoire pour l'instant.
|
||
- **Clonage défensif** : `getSpawnPoint()` retourne un `Optional<Location>` où
|
||
la `Location` est **clonée** ; idem à l'entrée dans `setSpawnPoint`.
|
||
`Location` étant mutable côté Bukkit, ça évite que du code externe modifie
|
||
accidentellement le spawn en faisant `team.getSpawnPoint().get().setX(...)`.
|
||
- **Persistance différée** : `Location` n'est pas trivialement sérialisable
|
||
(référence au `World`). On utilisera `ConfigurationSerializable` quand on
|
||
branchera un repo fichier ; pour l'instant, le `InMemoryTeamRepository`
|
||
s'en moque.
|
||
- **Pas de téléport intégré** : le noyau ne fournit pas `teleportToSpawn(...)`.
|
||
C'est au plugin de jeu d'enchaîner `player.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.player` complet et parallèle au
|
||
domaine `team` : `PlayerProfile` (entité identifiée par l'UUID Bukkit),
|
||
`PlayerProfileService`, `PlayerProfileRepository`,
|
||
`InMemoryPlayerProfileRepository`, `PlayerRanking` (record),
|
||
`PlayerException` / `PlayerProfileNotFoundException`.
|
||
- **Pourquoi pas sur `TeamMember`** : `TeamMember` est immuable (transitions de
|
||
rôle via `withRole`), et un joueur peut changer/quitter une équipe — son
|
||
profil doit persister. Mettre les scores sur `TeamMember` aurait couplé la
|
||
durée de vie du score à l'appartenance à l'équipe.
|
||
- **Auto-création** : `addScore` / `setScore` créent le profil automatiquement
|
||
s'il n'existe pas (`getOrCreateProfile(playerId)`). Pas besoin d'appeler
|
||
explicitement un `register(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.ScoreHolder`
|
||
qui déclare le contrat de scoring (`getScore`, `addScore`, `setScore`,
|
||
`resetScore`, `getTotalScore`, etc.). `Team` et `PlayerProfile`
|
||
l'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 `Scoreboard` aggregate** : envisagé un composant `Scoreboard`
|
||
composé 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 de
|
||
`JavaPlugin`). Le plugin de jeu downstream instancie `new CRCore(this)` dans
|
||
son `onEnable()` 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 (`core` par
|
||
défaut) dans son `plugin.yml`.
|
||
|
||
## 2026-06-09 — Sous-commandes imbriquées récursives
|
||
|
||
- **Choix** : `AbstractCommand` porte la table des sous-commandes
|
||
(pas seulement `BaseCommand`). `SubCommand` peut donc avoir ses propres
|
||
sous-commandes (récursion). Routage via la méthode `dispatch(...)`
|
||
récursive.
|
||
- **Raison** : c'est ce qui permet `/core team create` (3 niveaux : root /
|
||
group / leaf). Sans ça, il faudrait flatter en `/core team-create` ou faire
|
||
du routage manuel dans chaque groupe.
|
||
- **Conséquence** : `BaseCommand` ne fait plus que pont Bukkit
|
||
(`CommandExecutor`/`TabCompleter` → `dispatch`) ; toute la logique de
|
||
routage vit dans `AbstractCommand`.
|
||
|
||
## 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 `final` couvre les deux patterns :
|
||
- **Remplacement par instance** : `team.replaceSubCommand("create", new MyCreate(svc))`
|
||
- **Override par héritage** : `extends TeamCreateSubCommand` + `super.execute(ctx)`
|
||
|
||
## 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émente `Cancellable`.
|
||
- **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 hook `validate*` du service.
|
||
- **Boilerplate** : chaque event a sa propre `HandlerList` statique
|
||
(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** : `SqliteTeamRepository` et `SqlitePlayerProfileRepository`
|
||
**étendent** leurs jumeaux `InMemory*` et overrident `save`/`delete` pour
|
||
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 les `team_members` et `team_scores` d'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** : `Database` expose 4 méthodes (execute / update / queryOne /
|
||
query) + un `TableBuilder` fluide. 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 `TableBuilder`** existe 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 par `db.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 permission
|
||
`crcore.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`.
|
||
- **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).
|
||
- **`delete` devient admin** : `/core team delete <team>` (au lieu de
|
||
l'ancien `/core team delete` qui ciblait l'équipe du chef). Cohérent avec
|
||
`create` qui est aussi admin.
|
||
|
||
## 2026-06-09 — Team peut être leaderless
|
||
|
||
- **Choix** : `Team.leaderId` devient nullable. `getLeaderId()` renvoie
|
||
`Optional<UUID>`, `getLeader()` renvoie `Optional<TeamMember>`. Nouveau
|
||
`hasLeader()` et `isLeader(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, PRIVATE
|
||
- `new 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ève `IllegalStateException` si la team
|
||
est leaderless. Utilisé par la commande `/core team transfer` (chef).
|
||
- **`TeamLeadershipTransferEvent.getOldLeaderId()`** renvoie maintenant
|
||
`Optional<UUID>` (vide si la team était leaderless avant l'opération).
|
||
- **Schéma SQLite** : la colonne `crcore_teams.leader_id` n'a plus la
|
||
contrainte `NOT 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-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(...)` dans `execute()`.
|
||
- **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
|
||
`LEADER` reste 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 doit
|
||
`setleader` d'abord). Pas un check chef, juste une garde de cohérence.
|
||
- `TeamTransferSubCommand` : devient l'équivalent admin "strict" de
|
||
`setleader` (membre existant uniquement). Les deux cohabitent ; doc dit
|
||
quand préférer l'un ou l'autre.
|
||
- `TeamSetSpawnSubCommand` : reste `playerOnly` car nécessite la
|
||
`Location` de 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
|
||
via `pluginManager.getPlugin("PlaceholderAPI")` et enregistre
|
||
automatiquement `CRCorePlaceholderExpansion` si présent. Aucune action
|
||
requise côté plugin de jeu.
|
||
- **Dépendance Maven** : `me.clip:placeholderapi:2.11.6` en scope
|
||
`provided` (depuis `https://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çant `me.clip.placeholderapi.*` n'est pas
|
||
vérifié → pas de `NoClassDefFoundError`.
|
||
- **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%`
|
||
- **Override** : `CRCore.registerPlaceholderHook()` est `protected` — 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>`). Permission `crcore.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`) dans `SqliteTeamRepository`.
|
||
- Le **pattern matching `instanceof X x`** (Java 16) → classique
|
||
`instanceof 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`.
|
||
- **Ce qui reste utilisé de Java 11** : `var` (Java 10+), `List.of()` /
|
||
`Map.of()` (Java 9+), interfaces avec méthodes `default`, lambdas, method
|
||
references.
|
||
|
||
## 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).
|