Files
Cites_Plugins/docs/features.md
T
Antone Barbaud 8b7cad3fce feat: chef commands moved to admin + PlaceholderAPI integration
Chef → admin: the chef role no longer grants any command privilege.
All team-management subcommands now take <team> as an argument and are
gated by their crcore.team.<action> permission only:
- add <team> <player>
- remove <team> <player>
- transfer <team> <player>
- visibility <team> <PUBLIC|PRIVATE>
- setspawn <team> (still player-only — needs admin's location)

The LEADER role is kept in the data model (Team / TeamMember) and remains
usable by game plugins via the API, but does not unlock any default
command. Future work can re-introduce chef-specific commands if needed.

PlaceholderAPI: auto-detected at CRCore.enable(). If the PAPI plugin is
present on the server, CRCorePlaceholderExpansion registers automatically;
otherwise the lib runs without it (no NoClassDefFoundError thanks to the
indirection through doRegisterPlaceholderHook).

Placeholders exposed:
- Team: %crcore_team%, %crcore_team_name/tag/color/color_chat/size/
  visibility/leader_name/total_score%, %crcore_team_score_<name>%
- Player: %crcore_player_score_<name>%, %crcore_player_score_total%

Dependency: me.clip:placeholderapi:2.11.6, scope provided. New repo:
https://repo.extendedclip.com/content/repositories/placeholderapi/.

docs/features.md, decisions.md and the builtin-commands diagram updated to
reflect the simpler admin/player two-tier model and the PAPI section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 15:05:02 +02:00

26 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 :

  1. Team — équipes (membres, leader, visibilité, scores, classements, spawn)
  2. Player — profils joueurs (scores nommés, classements individuels)
  3. Framework de commandesBaseCommand / SubCommand imbriqués
  4. Commandes built-in/core team [create|delete|add|remove|join|leave|...]
  5. Évènements Bukkit — 9 events team + 3 events player
  6. Database — wrapper SQLite + table builder pour les plugins downstream
  7. Bootstrap — classe CRCore qui 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éthodes isPublic(), isPrivate().
  • TeamColor : 16 valeurs, chacune expose ChatColor, DyeColor, displayName.

Règles d'intégrité

  1. Une équipe a exactement un chef à tout instant.
  2. Un joueur appartient à au plus une équipe (au sein du registre du plugin de jeu — chaque plugin a son propre registre).
  3. name et tag sont uniques (case-insensitive) dans le registre.
  4. Le chef ne peut pas être retiré sans transferLeadership préalable.
  5. Une équipe PRIVATE ne peut être rejointe que via addMember (action du chef) ; une équipe PUBLIC peut être rejointe via joinTeam (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 : Location référence un World Bukkit, donc ce n'est pas trivialement sérialisable. Pour l'instant on stocke en mémoire ; quand on branchera une persistance fichier, on utilisera Location.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é (team PRIVATE, joueur déjà dans une équipe, ou règle custom dans validateJoinable).

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


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


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émente Command) — porte tous les champs ET la table des sous-commandes (imbrication). Méthodes builder en protected : addAlias, permission, playerOnly, description, usage, argument, optionalArgument, addSubCommand. Routage récursif via dispatch(...) et tabComplete(...).
  • BaseCommand extends AbstractCommand — implémente CommandExecutor et TabCompleter de Bukkit, branche onCommanddispatch. À utiliser pour la racine d'un arbre (/core).
  • SubCommand extends AbstractCommand — sous-commande ; peut être feuille (override execute) ou groupe (appelle addSubCommand dans son constructeur).
  • replaceSubCommand(name, newSub) sur AbstractCommand — 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) — raccourci sender.sendMessage

Exemple complet

Voir setup.md.

Diagramme


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


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


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ésultat
    • update(sql, params...) — INSERT/UPDATE/DELETE, renvoie le nombre de lignes affectées
    • queryOne(sql, mapper, params...) — au plus une ligne (Optional)
    • query(sql, mapper, params...) — plusieurs lignes
    • inTransaction(Runnable) — commit/rollback auto
    • table(name) — démarre un TableBuilder fluide
    • tableExists(name) — check
  • TableBuilder.ifNotExists().column(name, type).primaryKey().notNull()...create()
  • ColumnType — enum : INTEGER, REAL, TEXT, BLOB, BOOLEAN, UUID
  • RowMapper<T>T map(ResultSet rs) throws SQLException (lambda-friendly)
  • DatabaseException — runtime exception, wrap les SQLException JDBC

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_scores
  • crcore_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


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. 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 team
  • buildPlayerProfileService(repo) — idem player
  • buildCoreCommand(...) — pour ajouter des groupes top-level

Diagramme


Backlog / idées de futurs domaines

(à remplir — ex. inventaires partagés d'équipe, kits, gestion de rounds, …)