Files
Cites_Plugins/docs/decisions.md
T
Antone Barbaud ffc77c4213 feat: initial CR-Core library (team + player + command framework)
Pure Maven library for CR Minecraft game plugins, targeting Paper 1.16.5.

Common abstractions (fr.luc.crcore.common): Identifiable, Named, ScoreHolder,
AbstractEntity, Repository<T>.

Team domain (fr.luc.crcore.team): Team entity with name/tag/color/leader/
visibility (PUBLIC|PRIVATE)/members/scores/spawn point, TeamMember,
TeamRole/TeamColor/TeamVisibility enums, TeamRanking record, TeamService with
overridable hooks (factories, validations, lifecycle events), in-memory
repository, dedicated exception hierarchy.

Player domain (fr.luc.crcore.player): PlayerProfile with named scores per
player, PlayerProfileService with auto-creation, individual rankings,
exception hierarchy. Both Team and PlayerProfile implement ScoreHolder.

Command framework (fr.luc.crcore.command): Command interface,
AbstractCommand base, BaseCommand (CommandExecutor + TabCompleter), SubCommand,
CommandContext, CommandResult, ArgumentType<T> + ArgumentTypes catalogue
(STRING, INTEGER, DOUBLE, BOOLEAN, ONLINE_PLAYER, enumOf, choice).

Docs (docs/) is the single source of truth: README, setup, features,
decisions log, and 6 PlantUML diagrams (team class/sequence/activity/join,
player class, command class).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 17:17:56 +02:00

235 lines
13 KiB
Markdown
Raw 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.