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

624 lines
26 KiB
Markdown

# 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 commandes**`BaseCommand` / `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
- Classes : [team-class-diagram.puml](diagrams/team-class-diagram.puml)
- Séquence création : [team-create-sequence.puml](diagrams/team-create-sequence.puml)
- Séquence auto-join : [team-join-sequence.puml](diagrams/team-join-sequence.puml)
- Activité création : [team-create-activity.puml](diagrams/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](diagrams/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é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 `onCommand``dispatch`. À 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](setup.md#utilisation-depuis-un-plugin-de-jeu).
### Diagramme
- Classes : [command-class-diagram.puml](diagrams/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
```java
// 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](diagrams/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
```java
@EventHandler
public void onTeamCreate(TeamCreateEvent e) {
Team t = e.getTeam();
// ...
}
```
### Diagramme
- Classes : [events-diagram.puml](diagrams/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é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
```java
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](diagrams/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.) :
```yaml
# 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
```java
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 :
```java
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
- Séquence : [bootstrap-sequence.puml](diagrams/bootstrap-sequence.puml)
---
## Backlog / idées de futurs domaines
_(à remplir — ex. inventaires partagés d'équipe, kits, gestion de rounds, …)_