New fr.luc.crcore.team.config module: - TeamSetting<T> (typed, with key/type/default/parser/serializer; factories ofBoolean/ofInt/ofString/ofEnum). - TeamSettings registry: 8 standard settings (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 register() for game plugins. - TeamConfigService interface with cascade get(team, setting) → per-team override (SQLite) → global YAML → hard default. Persists per- team via TeamRepository.save(), global via YamlConfiguration.save(). - YamlTeamConfigService default impl with bundled crcore-team-config.yml. Storage: - Team.getSettings() Map<String, Object> for per-team overrides. - New SQLite table crcore_team_settings (team_id, key, value, type) with load + write-through persist in SqliteTeamRepository. - Global YAML <plugin>-team-config.yml in dataFolder, auto-created at first boot (template from game plugin's resource of the same name takes priority). New reusable GUI framework fr.luc.crcore.gui: - AbstractInventoryGui (implements InventoryHolder, rebuild() abstract, setButton/setDecoration/clearSlot helpers, onClose hook, openTo()). - GuiClickHandler FunctionalInterface. - GuiListener (single Bukkit listener, detects via getHolder(), ALWAYS cancels clicks even on slots without handlers). - GuiItems builder (named/of/filler + lore/amount/build, '&' color codes translated). Concrete settings GUIs (fr.luc.crcore.team.config.gui): - AbstractSettingsGui base renderer: 27 slots, settings in row 2, booleans = LIME_DYE / GRAY_DYE toggle, integers = BOOK with left +1 / right -1 (shift × 10), strings/enums display-only. - GlobalSettingsGui: writes to YAML on each change. - TeamSettingsGui: writes to per-team overrides, "override active" flag in lore when value differs from global, "Reset all overrides" footer button. New /core team settings [team] subcommand: - No arg → GlobalSettingsGui (perm crcore.team.settings.global). - With arg → TeamSettingsGui (perm crcore.team.settings). - Player-only (Bukkit needs HumanEntity to open inventory). - Lives under /core team to stay modular (objective: split into modules later; everything team-related under /core team). CRCore: buildTeamConfigService() override point, teamConfig()/getTeamConfig() getters, GuiListener.registerOn(plugin) at enable(). CoreCommand, TeamGroupSubCommand and CoreReloadSubCommand extended to receive TeamConfigService. /core reload now reloads messages + broadcasts + team-config. Docs: new section 10 "Paramètres d'équipe", new decisions logged, setup.md tree updated, two new diagrams (team-config + gui). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
38 KiB
Domaines fonctionnels
CR-Core est une librairie. Le plugin de jeu downstream l'instancie en une
ligne via new CRCore(this).enable() dans son onEnable(), et tout est
branché : SQLite, services team + player, commandes /core team ...,
évènements Bukkit.
Architecture des domaines :
- Team — équipes (membres, leader, visibilité, scores, classements, spawn)
- Player — profils joueurs (scores nommés, classements individuels)
- Framework de commandes —
BaseCommand/SubCommandimbriqués - Commandes built-in —
/core team [create|delete|add|remove|join|leave|...] - Évènements Bukkit — 9 events team + 3 events player
- Database — wrapper SQLite + table builder pour les plugins downstream
- Bootstrap — classe
CRCorequi câble tout
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 (nullable) |
Identifiant du joueur chef d'équipe. Peut être null (équipe leaderless — typique après création par admin). |
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) |
Crée une équipe leaderless en PRIVATE. |
createTeam(name, tag, color, visibility) |
Crée une équipe leaderless avec la visibilité spécifiée. |
createTeam(name, tag, color, leaderId) |
Crée une équipe PRIVATE avec ce joueur comme chef. |
createTeam(name, tag, color, leaderId, visibility) |
Surcharge complète. leaderId peut être null (équivalent leaderless). |
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) |
Transfert chef→chef strict : le nouveau chef doit être membre, et la team doit avoir un chef actuel. |
setLeader(teamId, newLeaderId) |
Plus permissif : fonctionne sur team leaderless et auto-ajoute le joueur s'il n'est pas membre. C'est l'opération admin. |
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> (classe immutable, accesseurs rank()/team()/score()) 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
Classe immutable : PlayerRanking(int rank, PlayerProfile profile, int score) avec accesseurs rank()/profile()/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é avec sous-commandes imbriquées récursives
(supporte /core team create, /core team join, etc.). Les commandes par
défaut sont fournies en section 4.
Architecture
Command(interface) — contrat partagé :getName(),getAliases(),getPermission(),isPlayerOnly(),getDescription(),execute(ctx),tabComplete(sender, args),matches(label).AbstractCommand(abstract, implémenteCommand) — porte tous les champs ET la table des sous-commandes (imbrication). Méthodes builder enprotected:addAlias,permission,playerOnly,description,usage,argument,optionalArgument,addSubCommand. Routage récursif viadispatch(...)ettabComplete(...).BaseCommand extends AbstractCommand— implémenteCommandExecutoretTabCompleterde Bukkit, brancheonCommand→dispatch. À utiliser pour la racine d'un arbre (/core).SubCommand extends AbstractCommand— sous-commande ; peut être feuille (overrideexecute) ou groupe (appelleaddSubCommanddans son constructeur).replaceSubCommand(name, newSub)surAbstractCommand— permet aux plugins de jeu de remplacer une sous-commande par défaut par leur propre implémentation (pattern standard d'override).
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
4. Commandes built-in /core team ...
Statut : 14 sous-commandes prêtes à l'emploi, branchées par
CRCore.enable(). Chaque sous-commande vit dans
fr.luc.crcore.command.builtin.team et est substituable individuellement par
sous-classe ou via replaceSubCommand.
Pas d'aliases courts : les commandes ont leur nom long uniquement
(/core team create et pas /core team c).
Modèle simplifié à 2 rôles : toutes les opérations de gestion d'équipe
sont admin (perm requise + team passée en argument). Les opérations
joueur (join, leave, info, list, top) sont gated par permission
mais ne nécessitent pas le rôle chef. Le rôle LEADER reste présent dans
le modèle de données (utilisable par les game plugins via l'API) mais
n'accorde aucun privilège de commande pour l'instant.
Arborescence
/core (CoreCommand)
└── team (TeamGroupSubCommand)
├── create <name> <tag> <color> [leader] [admin] créer (chef optionnel)
├── delete <team> [admin] dissoudre une équipe
├── setleader <team> <player> [admin] (re)assigner le chef
├── score <team> <name> <add|set> <value> [admin] modifier un score
├── add <team> <player> [admin] ajouter un joueur
├── remove <team> <player> [admin] retirer un joueur
├── transfer <team> <player> [admin] transfert chef→membre existant
├── visibility <team> <PUBLIC|PRIVATE> [admin] changer visibilité
├── setspawn <team> [admin] spawn à la position de l'admin
├── join <team> [joueur] rejoindre une PUBLIC
├── leave [joueur] quitter son équipe
├── info [team] [joueur] infos
├── list [joueur] toutes les équipes
└── top [score] [joueur] classement
Permissions
Chaque sous-commande a une permission crcore.team.<action> :
| Niveau | Commandes |
|---|---|
| Admin | create, delete, setleader, score, add, remove, transfer, visibility, setspawn |
| Joueur | join, leave, info, list, top |
| Sous-commande | Permission |
|---|---|
create |
crcore.team.create |
delete |
crcore.team.delete |
setleader |
crcore.team.setleader |
score |
crcore.team.score |
add |
crcore.team.add |
remove |
crcore.team.remove |
transfer |
crcore.team.transfer |
visibility |
crcore.team.visibility |
setspawn |
crcore.team.setspawn |
join |
crcore.team.join |
leave |
crcore.team.leave |
info |
crcore.team.info |
list |
crcore.team.list |
top |
crcore.team.top |
Le plugin de jeu ou le serveur configure les défauts via LuckPerms / Bukkit permissions plugin.
Override d'une sous-commande par défaut
// Option A : remplacer une feuille
core.getCoreCommand().findSubCommand("team")
.ifPresent(team -> team.replaceSubCommand("create",
new MyCustomTeamCreate(core.getTeamService())));
// Option B : sous-classer et override execute()
public class MyTeamCreate extends TeamCreateSubCommand {
public MyTeamCreate(TeamService service) { super(service); }
@Override public CommandResult execute(CommandContext ctx) {
// règles métier custom puis fallback super
return super.execute(ctx);
}
}
Diagramme
- Classes : builtin-commands-diagram.puml
5. Évènements Bukkit
Statut : 12 évènements implémentés, tirés automatiquement par les services
par défaut (BukkitEventFiringTeamServiceImpl et BukkitEventFiringPlayerProfileServiceImpl).
Tous les évènements sont post (non-cancellable) — la validation se fait en
amont dans les services via les hooks validate*. Pour bloquer un comportement,
override le hook ou la sous-commande, pas l'évènement.
Évènements Team (fr.luc.crcore.team.event)
| Évènement | Quand | Champs spécifiques |
|---|---|---|
TeamCreateEvent |
Après création + persist | — |
TeamDissolveEvent |
Après dissolution | — |
TeamMemberAddEvent |
Après ajout d'un membre (chef OU auto-join) | getMember() |
TeamMemberRemoveEvent |
Après retrait | getPlayerId() |
PlayerJoinTeamEvent |
Spécifique auto-join (joueur lui-même) | getMember() |
TeamLeadershipTransferEvent |
Après transfert | getOldLeaderId(), getNewLeaderId() |
TeamVisibilityChangeEvent |
Après changement effectif | getOldVisibility(), getNewVisibility() |
TeamScoreChangeEvent |
Après changement effectif d'un score | getScoreName(), getOldValue(), getNewValue(), getDelta() |
TeamSpawnPointChangeEvent |
Après changement du spawn | getOldLocation(), getNewLocation() (nullable) |
Tous étendent TeamEvent (porte la Team).
Évènements Player (fr.luc.crcore.player.event)
| Évènement | Quand | Champs spécifiques |
|---|---|---|
PlayerProfileCreateEvent |
Après création (lazy ou explicite) | — |
PlayerProfileDeleteEvent |
Après suppression | — |
PlayerScoreChangeEvent |
Après changement effectif d'un score joueur | getScoreName(), getOldValue(), getNewValue(), getDelta() |
Tous étendent PlayerProfileEvent (porte le PlayerProfile).
Usage
@EventHandler
public void onTeamCreate(TeamCreateEvent e) {
Team t = e.getTeam();
// ...
}
Diagramme
- Classes : events-diagram.puml
6. Persistance SQLite (fr.luc.crcore.database)
Statut : wrapper minimal + table builder fluide. Repositories SQLite
write-through pour Team et PlayerProfile activés par défaut via CRCore.
API
Database(AutoCloseable) — 4 méthodes principales :execute(sql, params...)— DDL ou statement sans résultatupdate(sql, params...)— INSERT/UPDATE/DELETE, renvoie le nombre de lignes affectéesqueryOne(sql, mapper, params...)— au plus une ligne (Optional)query(sql, mapper, params...)— plusieurs lignesinTransaction(Runnable)— commit/rollback autotable(name)— démarre unTableBuilderfluidetableExists(name)— check
TableBuilder—.ifNotExists().column(name, type).primaryKey().notNull()...create()ColumnType— enum : INTEGER, REAL, TEXT, BLOB, BOOLEAN, UUIDRowMapper<T>—T map(ResultSet rs) throws SQLException(lambda-friendly)DatabaseException— runtime exception, wrap lesSQLExceptionJDBC
Les paramètres Object... sont liés via PreparedStatement (anti-injection),
avec conversion auto pour UUID (→ TEXT), Enum<?> (→ name() TEXT), Boolean
(→ 0/1).
Tables internes CR-Core
Le préfixe crcore_ évite les collisions :
crcore_teams,crcore_team_members,crcore_team_scorescrcore_player_profiles,crcore_player_scores
Tables custom côté plugin de jeu
Database db = core.getDatabase();
db.table("my_kills")
.ifNotExists()
.column("player_id", ColumnType.UUID).primaryKey()
.column("kills", ColumnType.INTEGER).notNull().defaultValue("0")
.create();
Diagramme
- Classes : database-diagram.puml
7. Intégration PlaceholderAPI (optionnelle)
Statut : implémentée. Auto-détectée par CRCore.enable() — si le plugin
PlaceholderAPI est installé sur le serveur, les placeholders %crcore_*%
sont enregistrés automatiquement. Si PAPI est absent, la lib reste
fonctionnelle, juste sans placeholders.
Placeholders Team
Renvoient vides si le joueur n'est dans aucune équipe.
| Placeholder | Renvoie | Exemple |
|---|---|---|
%crcore_team% |
récap formaté coloré | §c[#WOLF] Wolves |
%crcore_team_name% |
nom de l'équipe | Wolves |
%crcore_team_tag% |
tag court | WOLF |
%crcore_team_color% |
nom de la couleur | Red |
%crcore_team_color_chat% |
code couleur ChatColor | §c |
%crcore_team_size% |
nombre de membres | 5 |
%crcore_team_visibility% |
PUBLIC ou PRIVATE |
PRIVATE |
%crcore_team_leader_name% |
nom du chef (vide si leaderless) | Alice |
%crcore_team_total_score% |
somme des scores de l'équipe | 42 |
%crcore_team_score_<name>% |
score nommé de l'équipe | %crcore_team_score_kills% → 12 |
Placeholders Player
| Placeholder | Renvoie |
|---|---|
%crcore_player_score_<name>% |
score nommé du joueur (0 si pas set) |
%crcore_player_score_total% |
somme de tous les scores du joueur |
Usage côté plugin de jeu / config
Pas d'action à faire côté plugin de jeu — la hook s'enregistre toute seule. Les placeholders sont disponibles partout où PAPI les résout (scoreboard, tablist, chat, hologrammes via DecentHolograms, etc.) :
# Exemple de scoreboard config (FeatherBoard / Scoreboard plugin)
lines:
- "&aÉquipe : %crcore_team%"
- "&aChef : %crcore_team_leader_name%"
- "&aKills : %crcore_player_score_kills% (total équipe %crcore_team_score_kills%)"
Override
CRCore.registerPlaceholderHook() est protected. Override dans une
sous-classe de CRCore pour ajouter ses propres placeholders ou désactiver
la hook.
8. Service de messages (fr.luc.crcore.message)
Statut : implémenté. Toutes les commandes built-in /core team ... passent
par ce service ; plus rien n'est hardcodé dans le code.
Modèle « un seul fichier par plugin »
Au premier démarrage, CR-Core crée un seul fichier dans le dataFolder du plugin de jeu :
<plugin-dataFolder>/<plugin-name-lowercase>-messages.yml
(par exemple cites-messages.yml si le plugin s'appelle Cites.) C'est le
fichier que l'admin du serveur édite. Pas de crcore-messages.yml séparé sur
disque — les defaults CR-Core vivent dans le jar et sont chargés en mémoire
comme couche de fallback.
Deux couches en mémoire
| Ordre | Source | Mutabilité |
|---|---|---|
| 1 | crcore-messages.yml embarqué dans le jar CR-Core |
Read-only, in-memory |
| 2 | <plugin>-messages.yml dans le dataFolder |
Lecture du disque |
La couche 2 écrase la couche 1 sur les clés communes. Une clé manquante dans le fichier user retombe automatiquement sur le default CR-Core. Si une future release CR-Core ajoute une nouvelle clé, l'admin n'a rien à faire — ça marche immédiatement.
Création du fichier user au premier démarrage
L'ordre de priorité pour générer le starter file :
- Si le plugin de jeu bundle son propre
<plugin-name-lowercase>-messages.ymldans ses ressources → c'est ce fichier qui devient le template (donc tu peux pré-remplir avec tes overrides + tes messages perso au build). - Sinon → copie des defaults CR-Core comme starter (l'admin voit toutes les clés CR-Core et peut les éditer).
API MessagesService
// Lecture avec placeholders nommés
core.messages().get("team.create.success",
"name", team.getName(),
"tag", team.getTag(),
"visibility", team.getVisibility().name());
// → "&aÉquipe Wolves [#WOLF] créée (PRIVATE, sans chef)." (codes & déjà traduits)
// Lecture brute (sans substitution ni couleur)
core.messages().raw("team.create.success");
// Ajout/override programmatique en mémoire (non persisté)
core.messages().set("mygame.welcome", "&aBienvenue {player} !");
// Charge un fichier YAML additionnel (en plus du fichier user principal)
core.messages().loadAdditional("my-extras.yml");
// Hot reload (relit le fichier user, garde les defaults en mémoire)
core.messages().reload();
// Toggle codes couleur
core.messages().setApplyColorCodes(true); // défaut
// Chemin du fichier user (informationnel)
File path = core.messages().getUserFile();
Placeholders
Format {name} avec substitution via varargs paire-par-paire. Codes couleur
Bukkit &a, &c, &7, &f, etc. traduits automatiquement en §….
Clés manquantes
Si une clé n'existe ni dans le fichier user ni dans les defaults,
messages.get(...) renvoie [missing: key] pour rendre visible la clé qu'il
manque (debug facile).
Override côté plugin de jeu
Option 1 — Bundler son propre template dans les ressources du plugin de
jeu, fichier nommé <plugin-name-lowercase>-messages.yml. À la première
exécution, ce fichier sert de starter dans le dataFolder.
Option 2 — Éditer le fichier généré après premier démarrage. Le YAML contient déjà les clés CR-Core ; ajoute / modifie / supprime ce que tu veux.
Option 3 — Override programmatique :
core.messages().set("team.create.success", "Custom format for {name}");
Override de l'impl
CRCore.buildMessagesService() est protected. Sous-classe CRCore et
redéfinis-la pour une impl custom (ex. messages venant d'une base de données,
d'un microservice de traduction, etc.).
Fichier crcore-messages.yml — référence
~25 clés organisées en sections : common.* (no-permission, player-only, …),
team.create.*, team.delete.success, team.add.*, etc. Voir le fichier
dans src/main/resources/crcore-messages.yml pour la liste complète avec les
placeholders documentés en commentaire.
Diagramme
- Classes : messages-class-diagram.puml
9. Service de broadcasts (fr.luc.crcore.broadcast)
Statut : implémenté. Un seul listener Bukkit interne route les 12 événements CR-Core vers le {@code BroadcastService} qui décide à qui envoyer le message selon la config YAML.
Séparation routes / templates
- Routes (« qui reçoit quoi ») →
<plugin>-broadcasts.yml - Templates (« quel texte ») →
<plugin>-messages.yml, clés<eventKey>.broadcast(ex.team.create.broadcast)
Les deux fichiers sont modifiables indépendamment. L'admin peut couper
tous les broadcasts en passant tout en [NONE] sans toucher aux templates,
ou inversement changer la formulation sans toucher aux routes.
BroadcastAudience — qui reçoit
| Audience | Résolution |
|---|---|
NONE |
Personne (équivalent à liste vide). |
LEADER |
Le chef de l'équipe concernée (s'il est en ligne). |
TEAM |
Tous les membres en ligne de l'équipe concernée. |
ADMIN |
Joueurs en ligne ayant la perm crcore.broadcast.admin. |
ALL |
Tous les joueurs en ligne sur le serveur. |
Multi-cibles : une clé d'event mappe sur une liste d'audiences. Union (pas de doublon : un joueur dans deux audiences reçoit un seul message).
Le fichier <plugin>-broadcasts.yml — exemple
team:
create: [ADMIN] # admins voient les créations
dissolve: [TEAM, ADMIN]
member:
add: [TEAM]
remove: [TEAM]
player:
join: [TEAM]
leadership:
transfer: [TEAM, ADMIN]
visibility:
change: [LEADER]
score:
change: [NONE] # noisy par défaut
spawn:
change: [LEADER]
player:
profile:
create: [NONE]
delete: [ADMIN]
score:
change: [NONE]
Liste des eventKey (= mapping listener)
| Bukkit event | Clé broadcasts.yml | Clé messages.yml |
|---|---|---|
TeamCreateEvent |
team.create |
team.create.broadcast |
TeamDissolveEvent |
team.dissolve |
team.dissolve.broadcast |
TeamMemberAddEvent |
team.member.add |
team.member.add.broadcast |
TeamMemberRemoveEvent |
team.member.remove |
team.member.remove.broadcast |
PlayerJoinTeamEvent |
team.player.join |
team.player.join.broadcast |
TeamLeadershipTransferEvent |
team.leadership.transfer |
team.leadership.transfer.broadcast |
TeamVisibilityChangeEvent |
team.visibility.change |
team.visibility.change.broadcast |
TeamScoreChangeEvent |
team.score.change |
team.score.change.broadcast |
TeamSpawnPointChangeEvent |
team.spawn.change |
team.spawn.change.broadcast |
PlayerProfileCreateEvent |
player.profile.create |
player.profile.create.broadcast |
PlayerProfileDeleteEvent |
player.profile.delete |
player.profile.delete.broadcast |
PlayerScoreChangeEvent |
player.score.change |
player.score.change.broadcast |
Placeholders injectés par le listener
Pour les events team, le contexte inclut toujours : {name}, {team_name}
(alias), {tag}, {color} (code couleur ChatColor), {visibility}.
Quand pertinent, en plus : {player} (nom du joueur impliqué),
{new_leader}, {old_leader}, {old_visibility}, {new_visibility},
{score_name}, {old_value}, {new_value}, {delta}.
API pour les game plugins
// Broadcast custom depuis un game plugin
core.broadcasts().broadcast("mygame.round.start",
BroadcastContext.empty()
.with("round", String.valueOf(currentRound))
.with("map", mapName));
// Lecture des audiences configurées (debug)
List<BroadcastAudience> who = core.broadcasts().getAudiences("team.create");
// Hot reload
core.broadcasts().reload();
Pour ajouter ses propres events broadcast, le game plugin :
- Ajoute la clé dans son
<plugin>-broadcasts.yml(ex.mygame.round.start: [ALL]) - Ajoute le template dans
<plugin>-messages.yml(clémygame.round.start.broadcast) - Appelle
core.broadcasts().broadcast(...)quand l'event survient
Override de l'impl
CRCore.buildBroadcastService(messages) est protected. Sous-classe
{@code CRCore} pour fournir une impl alternative (base de données, queue
externe, etc.).
Commande /core reload
Permission : crcore.reload. Recharge à la fois messages et broadcasts
depuis les fichiers user du dataFolder. Les defaults en jar ne bougent pas
(pas re-chargés).
Diagramme
- Classes : broadcasts-class-diagram.puml
10. Paramètres d'équipe (fr.luc.crcore.team.config)
Statut : implémenté. 8 settings standards + GUI in-game + cascade per-team → global → default.
Modèle de résolution
1. hard default défini en code dans TeamSettings (constantes)
2. global config <plugin>-team-config.yml ← admin via GUI ou YAML
3. per-team override table SQLite crcore_team_settings ← admin via GUI
config.get(team, setting) cascade per-team → global → default.
config.getGlobal(setting) cascade global → default (skip per-team).
Toutes les valeurs retournées sont non-null grâce au default en bout
de chaîne.
Settings standards (TeamSettings)
| Constante | Clé YAML/SQL | Type | Défaut |
|---|---|---|---|
FRIENDLY_FIRE |
friendly_fire |
bool | false |
PVP_PROTECTION_SECONDS |
pvp_protection_seconds |
int | 0 |
MAX_SIZE |
max_size |
int | 0 (illimité) |
MIN_SIZE |
min_size |
int | 0 |
RESPAWN_AT_TEAM_SPAWN |
respawn_at_team_spawn |
bool | true |
TEAM_CHAT_ENABLED |
team_chat_enabled |
bool | true |
SHOW_TAG_ABOVE_HEAD |
show_tag_above_head |
bool | true |
TEAM_COLOR_IN_NAME |
team_color_in_name |
bool | true |
CR-Core fournit les défauts ; c'est au plugin de jeu d'appliquer ces
settings dans sa logique (ex. écouter EntityDamageByEntityEvent et
checker config.get(team, FRIENDLY_FIRE) pour décider si le coup passe).
API typée
boolean ff = core.teamConfig().get(team, TeamSettings.FRIENDLY_FIRE);
int max = core.teamConfig().getGlobal(TeamSettings.MAX_SIZE);
core.teamConfig().setPerTeam(team, TeamSettings.FRIENDLY_FIRE, true);
core.teamConfig().resetPerTeam(team, TeamSettings.FRIENDLY_FIRE);
core.teamConfig().setGlobal(TeamSettings.MAX_SIZE, 8); // persiste le YAML
core.teamConfig().reload();
Settings custom (game plugin)
Un game plugin peut enregistrer ses propres settings :
public static final TeamSetting<Boolean> CITES_PVP_ROUND_END =
TeamSetting.ofBoolean("cites_pvp_round_end", false);
@Override public void onEnable() {
core = new CRCore(this).enable();
TeamSettings.register(CITES_PVP_ROUND_END);
}
→ La clé apparaîtra automatiquement dans les GUI globaux et per-team, et sera persistée en SQLite + YAML comme les standards.
Commande GUI
/core team settings [team] — player-only, ouvre l'interface graphique.
- Sans argument → GUI globaux (perm
crcore.team.settings.global). Modif → écrit dans<plugin>-team-config.yml. - Avec argument → GUI per-team (perm
crcore.team.settings). Modif → écrit en SQLite (overrides). Bouton "Reset tous les overrides" pour remettre toutes les valeurs au global.
Mécaniques GUI
- Inventaire 27 slots, settings sur la ligne du milieu (slots 10..16).
- Booléens : lampe verte (ON) / grise (OFF), clic = toggle.
- Entiers : item livre.
- Clic gauche = +1, shift = +10
- Clic droit = -1, shift = -10
- Clamp à 0 minimum
- Strings/Enums : affichage seul (édition via YAML — pas dans la V1 du GUI).
- Per-team : indication visuelle "Override per-team actif" dans la lore quand une valeur est différente du global.
- Slot 22 : bouton Fermer.
- Slot 18 (per-team uniquement) : bouton "Reset tous les overrides".
Framework GUI réutilisable
Le module fr.luc.crcore.gui est générique — réutilisable pour tout
futur GUI CR-Core ou game plugin :
AbstractInventoryGui implements InventoryHolder— base abstraite,rebuild(),setButton(slot, item, handler),setDecoration(...),clearSlot(...), hookonClose(...).GuiClickHandler(FunctionalInterface) — handler de clic par slot.GuiListener— un seul Listener Bukkit qui route les clics et les fermetures vers le bon GUI viainventory.getHolder().GuiItems— builder fluidenamed(material, "&aTitre").lore(...).build(), filler décoratif gris.
Pour faire un GUI custom : extends AbstractInventoryGui, créer
l'inventaire dans le constructeur, override rebuild(). Le GuiListener
est déjà enregistré par CRCore.enable().
Diagrammes
11. Bootstrap CRCore
Statut : implémenté. Point d'entrée unique pour les plugins de jeu.
Usage
public class MyGamePlugin extends JavaPlugin {
private CRCore core;
@Override
public void onEnable() {
core = new CRCore(this).enable();
}
@Override
public void onDisable() {
if (core != null) core.disable();
}
}
Configuration
CRCoreConfig en builder :
new CRCore(this, new CRCoreConfig()
.withSqliteFile("mygame.db") // défaut : crcore.db
.withCommandName("game")) // défaut : core
.enable();
withInMemoryStorage() désactive SQLite (tests, ou contexte stateless).
Override de la construction des services
Sous-classer CRCore et redéfinir :
buildTeamService(repo)— pour utiliser une impl custom du service teambuildPlayerProfileService(repo)— idem playerbuildCoreCommand(...)— pour ajouter des groupes top-level
Diagramme
- Séquence : bootstrap-sequence.puml
Backlog / idées de futurs domaines
(à remplir — ex. inventaires partagés d'équipe, kits, gestion de rounds, …)