Files
Antone Barbaud 4efaa5bbde feat: moderation feature skeleton (/core admin + mod mode + tools)
New feature fr.luc.crcore.features.moderation, opt-in via
CRCoreConfig.setupModeration() (also enabled by setupAll()).

Core abstractions:
- ModerationState: full player snapshot (inv + armor + offhand, XP,
  health, food, location, gamemode, allowFlight/flying, walk/fly speed).
  Immutable, restoreTo(player) restores everything.
- ModerationService interface + ModerationServiceImpl (with
  protected onAfterEnter/onAfterExit hooks) +
  BukkitEventFiringModerationServiceImpl (fires ModerationEnterEvent /
  ModerationExitEvent).
- ModerationRepository interface + InMemoryModerationRepository
  (skeleton — SQLite impl with BukkitObjectOutputStream serialization
  planned).
- ModeratorTool interface: getKey/getSlot(0..8)/buildIcon +
  onLeftClick/onRightClick/onInteractEntity. ModeratorToolRegistry
  preserves registration order, slot collision = replace.
- Exceptions: ModerationException base + AlreadyActive + NotActive.
- Events: ModerationEvent base + Enter + Exit.

5 skeleton tools in the hotbar:
- slot 0: TeleportRandomPlayerTool (compass, right-click → tp random)
- slot 1: InventorySpyTool (chest, right-click on player → open inv)
- slot 2: FreezeTool (ice, right-click on player → toggle freeze)
- slot 7: VanishToggleTool (ender eye, click → toggle vanish)
- slot 8: ExitTool (barrier, click → exit mod mode)
Slots 3-6 free for custom tools.

ModerationListener routes interactions and locks hotbar:
- PlayerInteractEvent → tool.onLeftClick / onRightClick (with cancel).
- PlayerInteractEntityEvent → tool.onInteractEntity (with cancel).
- PlayerDropItemEvent / PlayerSwapHandItemsEvent: cancel for mods.
- InventoryClickEvent: cancel only when top inv is the mod's own inv
  (preserves InventorySpyTool's ability to manipulate target's inv).
- PlayerJoinEvent: re-applies vanish for already-vanished mods.
- PlayerQuitEvent: cleanup freeze state.
- PlayerMoveEvent: cancel block-position changes for frozen players,
  keeping head rotation free.

Mod mode lifecycle:
- enter: snapshot + clear inv + populate hotbar + CREATIVE +
  allowFlight + vanish + ModerationEnterEvent.
- exit: state.restoreTo(player) + unvanish + unfreeze + repo delete +
  ModerationExitEvent.

/core admin (perm crcore.admin, player-only): toggle on/off.
Messages moderation.enter.success / moderation.exit.success added
to crcore-messages.yml.

CRCoreConfig.setupModeration() + isModerationEnabled() flag.
CRCore: buildModerationService() and registerDefaultModeratorTools()
override points, moderation() / getModerationService() getters with
IllegalStateException guard. Builds + registers ModerationListener at
enable() when feature on. CoreCommand extended to take ModerationService;
registers AdminToggleSubCommand only when service non-null.

Skeleton limitations documented in features.md:
- In-memory repo only (server crash = lost inv) — SQLite planned.
- InventorySpyTool opens real inv (no read-only wrapping yet).
- TeleportRandomPlayerTool is a placeholder for a future player-picker
  GUI.
- No moderation.*.broadcast keys yet.
- No /core admin <player> (self-toggle only).

Docs: section 11 in features.md, decision logged in decisions.md
(skeleton scope + rationale), setup.md snippet updated,
new moderation-class-diagram.puml, README.md updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 12:19:21 +02:00

742 lines
41 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 816. 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>`
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-10 — Feature modération (skeleton)
- **Choix** : nouveau module `fr.luc.crcore.features.moderation`,
opt-in via `CRCoreConfig.setupModeration()`. Skeleton complet :
- `ModerationState` (snapshot total : inv, armor, offhand, XP, health,
food, location, gamemode, flight/fly speed)
- `ModerationService` (interface) + `ModerationServiceImpl` +
`BukkitEventFiringModerationServiceImpl` (tire `ModerationEnterEvent`
/ `ModerationExitEvent`)
- `ModeratorTool` interface + `ModeratorToolRegistry`
- 5 outils squelette (slots 0, 1, 2, 7, 8) :
`TeleportRandomPlayerTool`, `InventorySpyTool`, `FreezeTool`,
`VanishToggleTool`, `ExitTool`
- `ModerationListener` unique : route les clics → tool, lock hotbar
(drop, swap, inventory click sur self), vanish-on-join,
freeze (PlayerMoveEvent cancel)
- `/core admin` toggle on/off (perm `crcore.admin`, player-only)
- **Vanish** : `Player.hidePlayer(plugin, vanished)` — propre, sans
modif visuelle, pas de fake-quit. Le listener re-applique
automatiquement aux joueurs qui join.
- **Persistance in-memory uniquement** (skeleton). Si le serveur crash
pendant qu'un mod est en mod mode, son inventaire est perdu. Une
impl `SqliteModerationRepository` avec sérialisation
`BukkitObjectOutputStream` → base64 est prévue pour plus tard. Pas
bloquant pour la première itération.
- **Mod mode → CREATIVE + allowFlight** par défaut. Choisi pour la
visibilité totale + mobility. Override possible en sous-classant
`ModerationServiceImpl.enter()`.
- **Outils en slots fixes** plutôt qu'avec PersistentDataContainer.
Plus simple ; le `ModerationListener` annule tout drop/swap pour
garantir que les outils restent en place. Pour identification croisée
(NBT) on pourra ajouter plus tard.
- **Tools extensibles** : `core.moderation().getToolRegistry().register(...)`
côté game plugin. Les slots libres (3, 4, 5, 6) sont prêts pour les
outils custom.
- **Listener responsibilities** : un seul `ModerationListener`
centralise toutes les hooks (clicks, freeze, vanish, lock hotbar).
Évite de disperser dans 5 listeners séparés.
## 2026-06-10 — Réorganisation : `util/` (toujours) vs `features/` (opt-in)
- **Choix** : séparation nette en deux couches :
- `fr.luc.crcore.util.*` — couche **utilitaire**, toujours active : common,
command framework, database, message, broadcast, gui, placeholder.
- `fr.luc.crcore.features.*` — couche **features**, opt-in : team, player.
- `fr.luc.crcore.builtin.*` — les commandes top-level CoreCommand +
CoreReloadSubCommand (pas un util, pas une feature, mais le routing
global).
- **Renames de FQN** (importer côté plugin de jeu si on les utilise) :
- `fr.luc.crcore.common.*``fr.luc.crcore.util.common.*`
- `fr.luc.crcore.command.*``fr.luc.crcore.util.command.*`
(le framework — Command, BaseCommand, SubCommand, ArgumentType…)
- `fr.luc.crcore.command.builtin.team.*`
`fr.luc.crcore.features.team.command.*`
(les 14+ Team*SubCommand)
- `fr.luc.crcore.command.builtin.CoreCommand` / `CoreReloadSubCommand`
`fr.luc.crcore.builtin.*`
- `fr.luc.crcore.database.*``fr.luc.crcore.util.database.*`
- `fr.luc.crcore.message.*``fr.luc.crcore.util.message.*`
- `fr.luc.crcore.broadcast.*``fr.luc.crcore.util.broadcast.*`
- `fr.luc.crcore.gui.*``fr.luc.crcore.util.gui.*`
- `fr.luc.crcore.placeholder.*``fr.luc.crcore.util.placeholder.*`
- `fr.luc.crcore.team.*``fr.luc.crcore.features.team.*`
(et sous-packages event/exception/impl/config)
- `fr.luc.crcore.player.*``fr.luc.crcore.features.player.*`
- **Raison** : prépare la modularisation à long terme. Chaque feature est
isolée dans son sous-package `features/<nom>/` et peut éventuellement
être extraite en module Maven séparé plus tard. Les utils sont
partagés. Le top-level reste minimal (CRCore, CRCoreConfig, builtin).
## 2026-06-10 — Setup modulaire via `CRCoreConfig.setupX()`
- **Choix** : les features sont désormais **opt-in**. Par défaut une
instance de `CRCoreConfig` n'active **aucune** feature ; le plugin de
jeu opt-in via :
- `setupTeams()` — active team service, repo, config + GUI, sous-cmds
- `setupPlayers()` — active player profile service + repo
- `setupPlaceholders()` — active la hook PAPI (no-op si PAPI absent)
- `setupAll()` — raccourci, active tout
- **Comportement quand off** : les getters de service (ex.
`core.getTeamService()`) lèvent `IllegalStateException` avec un
message explicite. Les commandes built-in correspondantes ne sont
simplement pas enregistrées (ex. `/core team` n'existe pas si teams
off).
- **Util toujours actif** : messages, broadcasts, GUI framework, command
framework, database sont systématiquement chargés. C'est la couche
infrastructure que les features et les game plugins consomment.
- **Listeners Bukkit toujours register** : `CRCoreBroadcastListener` et
`GuiListener` sont register unconditionnellement. Si aucune feature ne
tire d'event, ils sont idle — aucun coût.
- **Snippet d'usage** :
```java
this.core = new CRCore(this,
new CRCoreConfig().setupAll()).enable();
// ou granular :
this.core = new CRCore(this,
new CRCoreConfig().setupTeams()).enable();
```
- **Raison** : un game plugin qui n'a pas besoin des teams ne charge
pas le service ; pas de fichier `<plugin>-team-config.yml` créé, pas
de table `crcore_teams` créée, pas de sous-commande `/core team`. La
surface est minimale par défaut.
## 2026-06-10 — Settings d'équipe : cascade per-team → global → default + GUI
- **Choix** : nouveau module `fr.luc.crcore.team.config` avec :
- `TeamSetting<T>` typé (factories `ofBoolean`, `ofInt`, `ofString`,
`ofEnum`) — chaque setting porte sa clé, son type, son default et sa
sérialisation YAML/SQL.
- `TeamSettings` registry 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 via `TeamSettings.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 consulter
`config.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'`ItemStack` avec 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 `GuiListener` cancel 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` (perm
`crcore.team.settings.global`).
- Avec arg `team` → ouvre `TeamSettingsGui` (perm `crcore.team.settings`).
- **Pas `/core settings`** au 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 `HumanEntity` pour 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.broadcast` avec
`BroadcastService` + `BroadcastAudience` enum + `BroadcastContext` data
class + `YamlBroadcastService` impl. Un listener Bukkit interne
(`CRCoreBroadcastListener`) écoute les 12 events CR-Core et les traduit
en appels `broadcast(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.
- **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** : `CRCoreBroadcastListener` est instancié
et enregistré dans `CRCore.enable()`. Les game plugins n'ont rien à
faire pour bénéficier du broadcast des events natifs CR-Core ; pour
leurs propres events, ils appellent `core.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`** : permission `crcore.reload`,
recharge `messages` + `broadcasts` depuis 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)` est
`protected` — 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-package `impl/` et les exceptions dans
un sous-package `exception/`. 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`
(idem `BukkitEventFiring*`, `InMemory*Repository`, `Sqlite*Repository`)
- `fr.luc.crcore.player.PlayerException` → `fr.luc.crcore.player.exception.PlayerException`
(et `PlayerProfileNotFoundException`)
- `fr.luc.crcore.player.*Impl` / `*Repository` impl → `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 (BukkitEventFiring*ServiceImpl, Sqlite*Repository).
- **Pas appliqué à `database/`, `command/` et `common/`** : ces packages
sont déjà petits et bien lisibles ; ajouter `impl/` à 3 fichiers serait
cosmétique.
## 2026-06-09 — `MessagesService` : YAML externalisable, un seul fichier par plugin
- **Choix** : nouveau module `fr.luc.crcore.message` avec une interface
`MessagesService` et une impl `YamlMessagesService`. 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 premier `enable()` à 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).
- **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(...)` 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).