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

13 KiB
Raw Blame History

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 (TeamExceptionTeamAlreadyExistsException, 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.