feat: SQLite persistence, default /core commands, Bukkit events, bootstrap

CRCore bootstrap class: one-line setup for game plugins (new CRCore(this).enable()).
Wires SQLite, services with event firing, and the /core command tree.

SQLite layer (fr.luc.crcore.database): Database wrapper exposing execute/update/
queryOne/query plus a fluent TableBuilder. ColumnType enum, RowMapper interface,
DatabaseException. Game plugins create their own tables in 2 lines via
db.table("foo").ifNotExists().column(...).create().

Repositories: SqliteTeamRepository and SqlitePlayerProfileRepository extend their
InMemory counterparts (write-through cache). 5 internal tables prefixed crcore_.

Command framework refactored for nested sub-commands: subcommand storage moved
from BaseCommand to AbstractCommand, recursive dispatch() and tabComplete(),
replaceSubCommand() for plugin overrides.

Default /core team commands (13 leaf sub-commands): create, delete, add, remove,
join, leave, info, list, transfer, visibility, score, top, setspawn. Each in its
own class under fr.luc.crcore.command.builtin.team, fully substitutable.

Bukkit events: 9 team events (Create/Dissolve/MemberAdd/MemberRemove/PlayerJoin/
LeadershipTransfer/VisibilityChange/ScoreChange/SpawnPointChange) + 3 player
events (ProfileCreate/Delete/ScoreChange). All post-only, non-cancellable.
BukkitEventFiringTeamServiceImpl and BukkitEventFiringPlayerProfileServiceImpl
override the on* hooks to call Bukkit.getPluginManager().callEvent.

JavaDoc on all new public classes and key existing ones. docs/, GEMINI.md and
PUML diagrams synced: new sections (built-in commands, events, database,
bootstrap), 4 new diagrams (builtin-commands, events, database, bootstrap-
sequence), and 7 new architecture decisions logged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-09 10:54:00 +02:00
parent ffc77c4213
commit c1b414f400
63 changed files with 3632 additions and 400 deletions
+254 -15
View File
@@ -1,7 +1,19 @@
# Domaines fonctionnels
CR-Core est une librairie. Chaque domaine est autonome ; le plugin de jeu
downstream pioche ce qu'il utilise.
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
---
@@ -245,24 +257,29 @@ UUID pour rester déterministe).
## 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.
**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, argIndex, partial)`, `matches(label)`.
`tabComplete(sender, args)`, `matches(label)`.
- **`AbstractCommand`** (abstract, implémente `Command`) — porte tous les
champs : nom, aliases, permission, player-only, description, usage,
arguments. Méthodes builder en `protected` : `addAlias`, `permission`,
`playerOnly`, `description`, `usage`, `argument`, `optionalArgument`.
- **`BaseCommand extends AbstractCommand`** — implémente aussi
`CommandExecutor` et `TabCompleter` de Bukkit. Conteneur de `SubCommand`,
fait le routage `args[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 abstraite `execute(CommandContext)` est à implémenter.
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`)
@@ -307,6 +324,228 @@ Voir [setup.md](setup.md#utilisation-depuis-un-plugin-de-jeu).
---
## 4. Commandes built-in `/core team ...`
**Statut** : 13 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`.
### Arborescence
```
/core (CoreCommand, BaseCommand racine, aliases: cr, crcore)
└── team (TeamGroupSubCommand, alias: t)
├── create <name> <tag> <color> [visibility] — créer une équipe
├── delete — dissoudre son équipe (chef)
├── add <player> — chef ajoute un membre
├── remove <player> — chef retire un membre
├── join <name> — auto-join sur team PUBLIC
├── leave — quitter son équipe
├── info [name] — infos d'une équipe
├── list — liste toutes les équipes
├── transfer <player> — transfert de leadership
├── visibility <PUBLIC|PRIVATE> — changer la visibilité
├── score <team> <name> <add|set> <value> — [admin] modifier un score
├── top [score] — classement (par score ou global)
└── setspawn — chef définit le spawn
```
### Aliases supplémentaires (équivalents Bukkit)
| Sous-commande | Aliases |
|---|---|
| `create` | `c`, `new` |
| `delete` | `disband`, `dissolve` |
| `add` | `invite` |
| `remove` | `kick`, `expel` |
| `join` | `j` |
| `leave` | `quit` |
| `info` | `i` |
| `list` | `ls` |
| `visibility` | `vis` |
| `top` | `ranking`, `leaderboard` |
| `setspawn` | `spawn` |
### Permissions par défaut
| Sous-commande | Permission |
|---|---|
| `create` | `crcore.team.create` |
| `score` | `crcore.team.score.modify` (admin) |
| autres | aucune (gated par appartenance / rôle de chef) |
### 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. 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. score, profil joueur, gestion d'event/round, ...)_
_(à remplir — ex. inventaires partagés d'équipe, kits, gestion de rounds, )_