c1b414f400
CRCore bootstrap class: one-line setup for game plugins (new CRCore(this).enable()).
Wires SQLite, services with event firing, and the /core command tree.
SQLite layer (fr.luc.crcore.database): Database wrapper exposing execute/update/
queryOne/query plus a fluent TableBuilder. ColumnType enum, RowMapper interface,
DatabaseException. Game plugins create their own tables in 2 lines via
db.table("foo").ifNotExists().column(...).create().
Repositories: SqliteTeamRepository and SqlitePlayerProfileRepository extend their
InMemory counterparts (write-through cache). 5 internal tables prefixed crcore_.
Command framework refactored for nested sub-commands: subcommand storage moved
from BaseCommand to AbstractCommand, recursive dispatch() and tabComplete(),
replaceSubCommand() for plugin overrides.
Default /core team commands (13 leaf sub-commands): create, delete, add, remove,
join, leave, info, list, transfer, visibility, score, top, setspawn. Each in its
own class under fr.luc.crcore.command.builtin.team, fully substitutable.
Bukkit events: 9 team events (Create/Dissolve/MemberAdd/MemberRemove/PlayerJoin/
LeadershipTransfer/VisibilityChange/ScoreChange/SpawnPointChange) + 3 player
events (ProfileCreate/Delete/ScoreChange). All post-only, non-cancellable.
BukkitEventFiringTeamServiceImpl and BukkitEventFiringPlayerProfileServiceImpl
override the on* hooks to call Bukkit.getPluginManager().callEvent.
JavaDoc on all new public classes and key existing ones. docs/, GEMINI.md and
PUML diagrams synced: new sections (built-in commands, events, database,
bootstrap), 4 new diagrams (builtin-commands, events, database, bootstrap-
sequence), and 7 new architecture decisions logged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
325 lines
18 KiB
Markdown
325 lines
18 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.
|