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>
14 KiB
Domaines fonctionnels
CR-Core est une librairie. Chaque domaine est autonome ; le plugin de jeu downstream pioche ce qu'il utilise.
1. Domaine Team
Statut : modèle + service implémenté (repository en mémoire). Overridable étape par étape via hooks et factories.
Définition d'une équipe (Team)
| Attribut | Type | Description |
|---|---|---|
id |
UUID |
Identifiant interne unique, généré automatiquement. |
name |
String |
Nom lisible. Unique (case-insensitive). |
tag |
String |
Tag court (le « # »). Unique. Affiché entre [# … ]. |
color |
TeamColor |
Couleur associée (enum). |
leaderId |
UUID |
Identifiant du joueur chef d'équipe. |
visibility |
TeamVisibility |
PUBLIC (les joueurs peuvent rejoindre) ou PRIVATE (seul le chef ajoute). Défaut : PRIVATE. |
members |
Set<TeamMember> |
Ensemble des membres (inclut le chef). |
Membre (TeamMember)
| Attribut | Type | Description |
|---|---|---|
playerId |
UUID |
UUID du joueur Bukkit (= id de l'entité). |
role |
TeamRole |
LEADER ou MEMBER. |
joinedAt |
Instant |
Date d'entrée. |
Enums
TeamRole:LEADER,MEMBER.TeamVisibility:PUBLIC,PRIVATE. MéthodesisPublic(),isPrivate().TeamColor: 16 valeurs, chacune exposeChatColor,DyeColor,displayName.
Règles d'intégrité
- Une équipe a exactement un chef à tout instant.
- Un joueur appartient à au plus une équipe (au sein du registre du plugin de jeu — chaque plugin a son propre registre).
nameettagsont uniques (case-insensitive) dans le registre.- Le chef ne peut pas être retiré sans
transferLeadershippréalable. - Une équipe
PRIVATEne peut être rejointe que viaaddMember(action du chef) ; une équipePUBLICpeut être rejointe viajoinTeam(action du joueur lui-même).
Opérations (TeamService)
| Opération | Description |
|---|---|
createTeam(name, tag, color, leaderId) |
Crée une équipe PRIVATE avec ce joueur comme chef. Échoue si nom/tag/joueur déjà pris. |
createTeam(name, tag, color, leaderId, visibility) |
Surcharge : permet de créer directement en PUBLIC ou PRIVATE. |
dissolveTeam(teamId) |
Supprime l'équipe. |
addMember(teamId, playerId) |
Action du chef : ajoute un joueur comme MEMBER (marche en PUBLIC comme en PRIVATE). |
removeMember(teamId, playerId) |
Retire un joueur (interdit sur le chef). |
joinTeam(teamId, playerId) |
Action du joueur : auto-rejoindre une équipe PUBLIC. Lève TeamAccessException si la team est PRIVATE ou si le joueur est déjà dans une équipe. |
transferLeadership(teamId, newLeaderId) |
Le nouveau chef doit déjà être membre. |
setVisibility(teamId, visibility) |
Change la visibilité (typiquement appelée par le chef). |
addScore(teamId, name, delta) / setScore / getScore / resetScore / resetAllScores |
Gestion des scores (voir section Scores). |
getRankingByScore(name) / getGlobalRanking() / getTopRankingByScore(name, n) / getTopGlobalRanking(n) |
Classements (voir section Classements). |
setSpawnPoint(teamId, loc) / clearSpawnPoint(teamId) / getSpawnPoint(teamId) |
Point de spawn (voir section Spawn). |
getTeam / getTeamByName / getTeamByTag / getTeamOfPlayer |
Recherches. |
getAllTeams() |
Toutes les équipes. |
Scores
Chaque équipe porte une Map<String, Integer> de scores nommés. Le nom du
score est libre ("kills", "objectives", "global", …). Un jeu qui n'a
besoin que d'un seul score peut utiliser un nom unique (typiquement
"global").
Opération sur Team |
Description |
|---|---|
getScore(name) |
Valeur courante (0 si jamais set). |
hasScore(name) |
true si le score a été initialisé au moins une fois. |
getScores() |
Map immuable de tous les scores. |
getTotalScore() |
Somme de tous les scores (utilisée pour le classement global). |
addScore(name, delta) |
Incrémente (ou décrémente avec un delta négatif), renvoie la nouvelle valeur. |
setScore(name, value) |
Affecte une valeur absolue. |
resetScore(name) |
Supprime un score (revient à 0). |
resetAllScores() |
Vide tous les scores. |
Côté TeamService : mêmes opérations préfixées du teamId (addScore(teamId, name, delta) etc.). Hook onScoreChanged(team, name, oldValue, newValue)
appelé uniquement quand la valeur change réellement.
Classements (rankings)
Le service expose deux types de classements :
| Méthode | Description |
|---|---|
getRankingByScore(name) |
Classement par un score précis (descendant). |
getGlobalRanking() |
Classement par la somme des scores de chaque équipe. |
getTopRankingByScore(name, n) |
Top N par score. |
getTopGlobalRanking(n) |
Top N global. |
Le résultat est une List<TeamRanking> (record Java 16) avec rank (1-based),
team et score. Tiebreaker : ordre alphabétique sur le nom de l'équipe
(insensible à la casse).
La méthode protected rank(ToIntFunction<Team>) est exposée pour permettre à
une sous-classe de créer ses propres classements (ex. score pondéré, ratio
kills/deaths, score borné). La factory newRanking(rank, team, score)
permet de retourner un type custom étendant TeamRanking (les records étant
final, on peut wrapper plutôt qu'étendre).
Spawn point
Chaque équipe peut avoir un Location Bukkit comme point de spawn. Optionnel
(défaut : pas de spawn défini).
Opération sur Team |
Description |
|---|---|
getSpawnPoint() |
Optional<Location> (clonée — modifier l'objet retourné n'affecte pas l'équipe). |
hasSpawnPoint() |
true si un spawn a été défini. |
setSpawnPoint(location) |
Définit le spawn (cloné à l'entrée). null accepté = clear. |
clearSpawnPoint() |
Supprime le spawn. |
Côté TeamService : setSpawnPoint(teamId, location),
clearSpawnPoint(teamId), getSpawnPoint(teamId). Hook
onSpawnPointChanged(team, oldLocation, newLocation).
Persistance :
Locationréférence unWorldBukkit, donc ce n'est pas trivialement sérialisable. Pour l'instant on stocke en mémoire ; quand on branchera une persistance fichier, on utiliseraLocation.serialize()/deserialize()de l'API Bukkit (ConfigurationSerializable).
Hooks d'override (sur TeamServiceImpl)
| Hook | Quand |
|---|---|
newTeam(id, name, tag, color, leaderId, visibility) |
Factory — instancier une sous-classe de Team. |
newRanking(rank, team, score) |
Factory — wrapper / sous-classe de TeamRanking. |
rank(scoreFn) |
Logique de tri du classement (override pour pondération, tiebreaker custom, etc.). |
validateName(name) |
Avant création — règles custom sur le nom. |
validateTag(tag) |
Avant création — règles custom sur le tag. |
validateLeader(leaderId) |
Avant création — règles custom sur l'éligibilité du chef. |
validateJoinable(team, playerId) |
Avant joinTeam — règles custom. |
onBeforeSave(team) |
Juste avant le repository.save. |
onAfterCreate(team) |
Juste après une création réussie. |
onBeforeDissolve(team) / onAfterDissolve(team) |
Autour de la dissolution. |
onMemberAdded(team, member) |
Après ajout d'un membre (via addMember OU joinTeam). |
onMemberRemoved(team, playerId) |
Après retrait d'un membre. |
onPlayerJoined(team, member) |
Spécifique à joinTeam (en plus de onMemberAdded). |
onLeadershipTransferred(team, oldId, newId) |
Après transfert. |
onVisibilityChanged(team, oldV, newV) |
Après changement de visibilité. |
onScoreChanged(team, scoreName, oldV, newV) |
Après changement effectif d'un score. |
onSpawnPointChanged(team, oldLoc, newLoc) |
Après changement du point de spawn. |
Côté Team, factory newMember(playerId, role) pour utiliser un TeamMember
custom dans une sous-classe.
Exceptions
TeamException(base,RuntimeException).TeamAlreadyExistsException: nom, tag ou joueur déjà pris.TeamNotFoundException: équipe introuvable.TeamAccessException: auto-join refusé (teamPRIVATE, joueur déjà dans une équipe, ou règle custom dansvalidateJoinable).
Persistance
InMemoryTeamRepository (Map<UUID, Team>) par défaut. Le contrat
TeamRepository extends Repository<Team> permet de brancher YAML / SQLite /
Postgres sans toucher au service.
Diagrammes
- Classes : team-class-diagram.puml
- Séquence création : team-create-sequence.puml
- Séquence auto-join : team-join-sequence.puml
- Activité création : team-create-activity.puml
2. Profils joueurs et scores individuels
Statut : modèle + service implémenté (en mémoire). Symétrique au domaine Team pour tout ce qui touche aux scores et classements.
Pourquoi un domaine séparé
Les scores joueur vivent en dehors de l'équipe : un joueur peut changer
d'équipe, en quitter une, en rejoindre une autre — son profil et ses scores
persistent. Le domaine player est indépendant du domaine team ; libre au
plugin de jeu de les combiner (ex. score d'équipe = somme des scores des
membres).
PlayerProfile
| Attribut | Type | Description |
|---|---|---|
id |
UUID |
UUID Bukkit du joueur (= id de l'entité). |
scores |
Map<String, Integer> |
Scores nommés ("kills", "deaths", "global", …). |
PlayerProfile implements ScoreHolder — la même interface que Team. Toutes
les méthodes de scoring (getScore, addScore, setScore, resetScore,
resetAllScores, getTotalScore, getScores, hasScore) sont identiques
à celles de Team.
PlayerProfileService
| Opération | Description |
|---|---|
getOrCreateProfile(playerId) |
Retourne le profil existant ou en crée un. Utilisé en interne par les méthodes de scoring (auto-création à la première écriture). |
getProfile(playerId) |
Optional<PlayerProfile> sans création. |
deleteProfile(playerId) |
Supprime un profil. |
getAllProfiles() |
Tous les profils. |
addScore / setScore / getScore / resetScore / resetAllScores |
Identiques au service Team mais par playerId. |
getRankingByScore(name) / getGlobalRanking() / getTopRankingByScore(name, n) / getTopGlobalRanking(n) |
Classements de joueurs. |
PlayerRanking
Record Java 16 : record PlayerRanking(int rank, PlayerProfile profile, int score).
Mêmes règles que TeamRanking (rank 1-based, tri descendant, tiebreaker par
UUID pour rester déterministe).
Hooks d'override (sur PlayerProfileServiceImpl)
| Hook | Quand |
|---|---|
newProfile(playerId) |
Factory — instancier une sous-classe de PlayerProfile. |
newRanking(rank, profile, score) |
Factory — sous-classe de PlayerRanking. |
rank(scoreFn) |
Logique de tri (override pour pondération, tiebreaker custom, etc.). |
onProfileCreated(profile) |
Après création (lazy ou explicite). |
onProfileDeleted(profile) |
Après suppression. |
onScoreChanged(profile, name, oldV, newV) |
Après changement effectif d'un score. |
Exceptions
PlayerException(base,RuntimeException).PlayerProfileNotFoundException: profil introuvable (peu utilisé côté service car la plupart des opérations auto-créent).
Diagrammes
- Classes : player-class-diagram.puml
3. Framework de commandes
Statut : framework implémenté. Pas de commande Team intégrée — c'est au plugin de jeu de définir ses commandes en utilisant les briques fournies.
Architecture
Command(interface) — contrat partagé :getName(),getAliases(),getPermission(),isPlayerOnly(),getDescription(),execute(ctx),tabComplete(sender, argIndex, partial),matches(label).AbstractCommand(abstract, implémenteCommand) — porte tous les champs : nom, aliases, permission, player-only, description, usage, arguments. Méthodes builder enprotected:addAlias,permission,playerOnly,description,usage,argument,optionalArgument.BaseCommand extends AbstractCommand— implémente aussiCommandExecutoretTabCompleterde Bukkit. Conteneur deSubCommand, fait le routageargs[0]→ sous-commande, gère permissions, player-only, invalid usage, affichage de l'aide par défaut.SubCommand extends AbstractCommand— sous-commande sans logique Bukkit. La méthode abstraiteexecute(CommandContext)est à implémenter.
Types d'arguments (ArgumentTypes)
| Constante / factory | Type produit | Tab-complete |
|---|---|---|
STRING |
String |
(aucun) |
INTEGER |
Integer |
(aucun) |
DOUBLE |
Double |
(aucun) |
BOOLEAN |
Boolean |
true / false |
ONLINE_PLAYER |
Player |
joueurs connectés |
enumOf(Enum.class) |
l'enum | toutes les valeurs |
choice("a", "b", …) |
String |
les choix |
Un ArgumentType<T> custom = une classe qui implémente parse(String): T et
optionnellement suggestions(sender, partial): List<String>.
Résultats (CommandResult)
SUCCESS, FAILURE, INVALID_USAGE, NO_PERMISSION, PLAYER_ONLY — chacun
avec un message optionnel. Factories statiques CommandResult.success(...),
failure(...), invalidUsage(...).
Rendus automatiquement par BaseCommand.handleResult avec un code couleur
standard (vert succès, rouge erreur). Override handleResult pour personnaliser.
Contexte (CommandContext)
Wrapper passé à execute() :
getSender(),isPlayer(),getPlayer(),requirePlayer()get(name)— récupère un argument parsé typé (String name = ctx.get("name"))getOptional(name)/has(name)reply(message)— raccourcisender.sendMessage
Exemple complet
Voir setup.md.
Diagramme
- Classes : command-class-diagram.puml
Backlog / idées de futurs domaines
(à remplir — ex. score, profil joueur, gestion d'event/round, ...)