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
+31 -20
View File
@@ -5,29 +5,35 @@ mécaniques, règles et spécifications du noyau sont consignées ici.
## Objectif du projet
**CR-Core** est une **librairie Maven** réutilisable pour construire des plugins
Minecraft. Elle fournit :
**CR-Core** est une **librairie Maven** réutilisable pour construire des
plugins Minecraft Paper 1.16.5. Elle fournit, prêt à l'emploi en une ligne
d'initialisation côté plugin de jeu :
- des abstractions communes (`Identifiable`, `Named`, `ScoreHolder`,
`AbstractEntity`, `Repository<T>`) ;
- un **domaine équipes** clé en main (`Team`, `TeamMember`, `TeamService`,
`TeamRepository`) — visibilité, scores, classements, spawn, overridable ;
- un **domaine profils joueurs** (`PlayerProfile`, `PlayerProfileService`) —
scores nommés par joueur, classements individuels ;
- un **framework de commandes** (`BaseCommand`, `SubCommand`, `ArgumentType`,
tab-completion intégrée).
Les plugins de jeu (futur `CitesPlugin`, BedWars, etc.) déclarent CR-Core en
dépendance Maven `provided` (ou shadent la lib) et l'utilisent directement.
- Cible runtime : **Minecraft 1.16.5** (API Paper).
- Build : Maven, Java 16.
- **Abstractions communes** — `Identifiable`, `Named`, `ScoreHolder`,
`AbstractEntity`, `Repository<T>`.
- **Domaine Team** — équipes (nom, tag, couleur, chef, membres,
visibilité PUBLIC/PRIVATE, scores nommés, classements, point de spawn),
service overridable, exceptions dédiées.
- **Domaine Player** — profils joueurs (scores nommés, classements
individuels), service auto-créant les profils à la demande.
- **Framework de commandes** — `BaseCommand` / `SubCommand` imbriqués,
arguments typés, tab-complétion, permissions, player-only.
- **Commandes par défaut** — `/core team [create|delete|add|remove|join|`
`leave|info|list|transfer|visibility|score|top|setspawn]` fonctionnelles
out-of-the-box, chacune substituable par sous-classe.
- **Évènements Bukkit** — 9 events team + 3 events player, à écouter avec
`@EventHandler` côté plugin de jeu.
- **Persistance SQLite** — wrapper `Database` + `TableBuilder` fluide,
repositories SQLite write-through, table custom en 2 lignes pour les
plugins downstream.
- **Bootstrap unique** — `new CRCore(this).enable()` dans le `onEnable()`
du plugin de jeu, et tout est branché.
## Structure de la documentation
- `README.md` — Ce fichier. Vue d'ensemble et index.
- `setup.md` — Build, intégration dans un plugin de jeu, exemple d'usage.
- `features.md` — Domaines fonctionnels (team, command).
- `features.md` — Domaines fonctionnels détaillés.
- `decisions.md` — Journal des décisions importantes (ADR léger).
- `diagrams/` — Diagrammes PlantUML (`.puml`).
@@ -36,17 +42,22 @@ dépendance Maven `provided` (ou shadent la lib) et l'utilisent directement.
| Fichier | Type | Sujet |
|---|---|---|
| [team-class-diagram.puml](diagrams/team-class-diagram.puml) | Classe | Domaine Team + abstractions communes |
| [team-create-sequence.puml](diagrams/team-create-sequence.puml) | Séquence | Création d'une équipe |
| [team-create-sequence.puml](diagrams/team-create-sequence.puml) | Séquence | Création d'une équipe via la commande |
| [team-join-sequence.puml](diagrams/team-join-sequence.puml) | Séquence | Auto-join sur une équipe publique |
| [team-create-activity.puml](diagrams/team-create-activity.puml) | Activité | Flux de validation à la création |
| [player-class-diagram.puml](diagrams/player-class-diagram.puml) | Classe | Domaine Player (profils + scores + classements) |
| [command-class-diagram.puml](diagrams/command-class-diagram.puml) | Classe | Framework de commandes |
| [player-class-diagram.puml](diagrams/player-class-diagram.puml) | Classe | Domaine Player + scores joueur |
| [command-class-diagram.puml](diagrams/command-class-diagram.puml) | Classe | Framework de commandes (nested) |
| [builtin-commands-diagram.puml](diagrams/builtin-commands-diagram.puml) | Classe | Arbre des commandes `/core team ...` |
| [events-diagram.puml](diagrams/events-diagram.puml) | Classe | Évènements Bukkit team + player |
| [database-diagram.puml](diagrams/database-diagram.puml) | Classe | Wrapper SQLite + table builder |
| [bootstrap-sequence.puml](diagrams/bootstrap-sequence.puml) | Séquence | `CRCore.enable()` côté plugin de jeu |
## Conventions
- Code : **anglais standard**, séparation stricte interfaces / enums / classes
abstraites / classes concrètes / exceptions.
- Doc & messages joueur : **français**.
- JavaDoc en français sur les classes publiques et méthodes non triviales.
- Package racine : `fr.luc.crcore`.
- Classes du noyau **non-`final`**, méthodes-clés `protected`, hooks
`onBefore…`/`onAfter…` et factories `new…` pour l'override.
+90
View File
@@ -232,3 +232,93 @@ Format léger : une décision = un titre + contexte + choix + raison.
pour ~8 méthodes simples. Choix actuel : duplication contrôlée des
implémentations (Map + getters/setters), interface commune pour le
contrat.
## 2026-06-09 — CRCore = bootstrap library, pas un plugin
- **Choix** : CR-Core reste une **librairie** (pas de `plugin.yml`, pas de
`JavaPlugin`). Le plugin de jeu downstream instancie `new CRCore(this)` dans
son `onEnable()` et appelle `.enable()` — c'est ce qui câble SQLite,
services, commandes et events.
- **Alternative écartée** : faire de CR-Core un plugin standalone (à
installer côté serveur). Refusé pour deux raisons : (1) chaque jeu a son
propre état (registre d'équipes, scores) — on ne veut pas partager entre
jeux par défaut ; (2) la friction de déploiement (2 jars sur le serveur)
est inutile pour des plugins shadés.
- **Conséquence** : chaque plugin de jeu shade CR-Core, a sa propre DB SQLite
dans son `dataFolder`, et déclare la commande Bukkit racine (`core` par
défaut) dans son `plugin.yml`.
## 2026-06-09 — Sous-commandes imbriquées récursives
- **Choix** : `AbstractCommand` porte la table des sous-commandes
(pas seulement `BaseCommand`). `SubCommand` peut donc avoir ses propres
sous-commandes (récursion). Routage via la méthode `dispatch(...)`
récursive.
- **Raison** : c'est ce qui permet `/core team create` (3 niveaux : root /
group / leaf). Sans ça, il faudrait flatter en `/core team-create` ou faire
du routage manuel dans chaque groupe.
- **Conséquence** : `BaseCommand` ne fait plus que pont Bukkit
(`CommandExecutor`/`TabCompleter``dispatch`) ; toute la logique de
routage vit dans `AbstractCommand`.
## 2026-06-09 — Override par sous-classe + `replaceSubCommand`
- **Choix** : `AbstractCommand.replaceSubCommand(name, newSub)` permet aux
plugins de jeu de remplacer une feuille (ex. `TeamCreateSubCommand`) par
leur propre implémentation, sans tout recâbler.
- **Raison** : le user a explicitement demandé "Les futures plugins ne
feront qu'override les fonctions si besoin". Cette méthode + le fait que
les classes ne soient pas `final` couvre les deux patterns :
- **Remplacement par instance** : `team.replaceSubCommand("create", new MyCreate(svc))`
- **Override par héritage** : `extends TeamCreateSubCommand` + `super.execute(ctx)`
## 2026-06-09 — Évènements Bukkit : post-only, non-cancellable
- **Choix** : tous les évènements CR-Core (team + player) sont **post-events**,
tirés via les hooks `on*` après commit. Aucun n'implémente `Cancellable`.
- **Raison** : la validation pré-action vit côté service dans les hooks
`validate*` (overridables). Mélanger pré-cancellable côté event et hooks
côté service dédoublerait les points de blocage. Pour bloquer un comportement,
le pattern est : override le hook `validate*` du service.
- **Boilerplate** : chaque event a sa propre `HandlerList` statique
(contrainte Bukkit, pas de moyen de partager via héritage). 12 events =
12 occurrences du même pattern, accepté pour rester idiomatique Bukkit.
## 2026-06-09 — SQLite write-through cache pour les repositories
- **Choix** : `SqliteTeamRepository` et `SqlitePlayerProfileRepository`
**étendent** leurs jumeaux `InMemory*` et overrident `save`/`delete` pour
persister synchronement vers SQLite. Au démarrage, `loadAll()` recharge
tout le state depuis la DB dans le cache mémoire.
- **Raison** : les lectures (findAll, findByName, classements en
`Collection<Team>.stream().sorted(...)`) restent rapides — pas de hit DB.
Les écritures vont en DB synchronement (acceptable au rythme des actions
joueur, qui sont rares à l'échelle d'un event entre amis).
- **Approche delete + reinsert pour les collections** : sur `save()`, on
remplace en bloc les `team_members` et `team_scores` d'une équipe (DELETE
puis INSERT). Plus simple et moins bug-prone qu'un diff fin, et négligeable
en perf pour des équipes de quelques joueurs.
## 2026-06-09 — Type Database minimaliste plutôt qu'un ORM
- **Choix** : `Database` expose 4 méthodes (execute / update / queryOne /
query) + un `TableBuilder` fluide. Pas d'ORM, pas d'annotations
d'entités, pas de DSL SQL.
- **Raison** : un ORM ajouterait une dépendance lourde (Hibernate / jOOQ /
…), un poids de classloading non négligeable côté serveur Bukkit, et
abstrairait des opérations qu'on veut garder triviales et lisibles. SQL
brut + PreparedStatement est largement suffisant pour les volumes d'un
serveur d'event.
- **Le `TableBuilder`** existe pour répondre au "pouvoir rapidement et
simplement créer des tables" — c'est l'API la plus user-friendly à proposer.
Pour les cas avancés (FOREIGN KEY, contraintes composites), l'utilisateur
passe par `db.execute("CREATE TABLE ...")` direct.
## 2026-06-09 — Préfixe `crcore_` sur toutes les tables internes
- **Choix** : `crcore_teams`, `crcore_team_members`, `crcore_team_scores`,
`crcore_player_profiles`, `crcore_player_scores`.
- **Raison** : éviter les collisions avec les tables custom que les plugins
de jeu créent dans la même DB. CR-Core et le plugin de jeu partagent le
même fichier SQLite (par défaut `<dataFolder>/crcore.db`) ; le préfixe
isole proprement.
+62
View File
@@ -0,0 +1,62 @@
@startuml bootstrap-sequence
title CR-Core — Bootstrap from a game plugin (onEnable)
participant "MyGamePlugin\nextends JavaPlugin" as Plugin
participant "CRCore" as Core
participant "Database" as DB
participant "SqliteTeamRepository" as TeamRepo
participant "SqlitePlayerProfileRepository" as PlayerRepo
participant "BukkitEventFiringTeamServiceImpl" as TeamSvc
participant "BukkitEventFiringPlayerProfileServiceImpl" as PlayerSvc
participant "CoreCommand\n(/core)" as Cmd
participant "Bukkit" as Bukkit
Plugin -> Core : new CRCore(this)
activate Core
Plugin -> Core : enable()
Core -> DB : new Database(<dataFolder>/crcore.db)
activate DB
DB -> DB : ensure parent dir + PRAGMA foreign_keys
DB --> Core
deactivate DB
Core -> TeamRepo : new SqliteTeamRepository(db)
activate TeamRepo
TeamRepo -> DB : ensureSchema() — crcore_teams + crcore_team_members + crcore_team_scores
TeamRepo -> DB : loadAll() — SELECT pour ré-hydrater le cache mémoire
TeamRepo --> Core
deactivate TeamRepo
Core -> PlayerRepo : new SqlitePlayerProfileRepository(db)
activate PlayerRepo
PlayerRepo -> DB : ensureSchema() — crcore_player_profiles + crcore_player_scores
PlayerRepo -> DB : loadAll()
PlayerRepo --> Core
deactivate PlayerRepo
Core -> TeamSvc : buildTeamService(teamRepo)
Core -> PlayerSvc : buildPlayerProfileService(playerRepo)
Core -> Cmd : new CoreCommand(teamSvc, playerSvc)
activate Cmd
Cmd -> Cmd : registerDefaults() — TeamGroupSubCommand avec 13 leaf sub-cmds
Cmd --> Core
deactivate Cmd
Core -> Bukkit : plugin.getCommand("core").setExecutor(cmd)
Core -> Bukkit : .setTabCompleter(cmd)
Core --> Plugin : this (chainable)
deactivate Core
note over Plugin
À ce stade :
- /core team create/delete/add/... fonctionnel
- SQLite persiste team + player + leurs scores
- Évènements Bukkit sont tirés sur chaque opération
- Le plugin de jeu peut listen avec @EventHandler
end note
@enduml
@@ -0,0 +1,76 @@
@startuml builtin-commands-diagram
title CR-Core — Default /core team commands
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.command" {
abstract class BaseCommand
abstract class SubCommand
}
package "fr.luc.crcore.command.builtin" {
class CoreCommand {
+ CoreCommand(teamSvc, playerSvc)
# registerDefaults(): void
}
CoreCommand --|> BaseCommand
package "fr.luc.crcore.command.builtin.team" {
class TeamGroupSubCommand {
+ TeamGroupSubCommand(service)
# registerDefaults(): void
}
TeamGroupSubCommand --|> SubCommand
class TeamArgumentTypes <<utility>> {
+ {static} teamByName(service): ArgumentType<Team>
}
class TeamCreateSubCommand {
+ execute(ctx): CommandResult
}
class TeamDeleteSubCommand
class TeamAddSubCommand
class TeamRemoveSubCommand
class TeamJoinSubCommand
class TeamLeaveSubCommand
class TeamInfoSubCommand
class TeamListSubCommand
class TeamTransferSubCommand
class TeamVisibilitySubCommand
class TeamScoreSubCommand
class TeamTopSubCommand
class TeamSetSpawnSubCommand
TeamCreateSubCommand --|> SubCommand
TeamDeleteSubCommand --|> SubCommand
TeamAddSubCommand --|> SubCommand
TeamRemoveSubCommand --|> SubCommand
TeamJoinSubCommand --|> SubCommand
TeamLeaveSubCommand --|> SubCommand
TeamInfoSubCommand --|> SubCommand
TeamListSubCommand --|> SubCommand
TeamTransferSubCommand --|> SubCommand
TeamVisibilitySubCommand --|> SubCommand
TeamScoreSubCommand --|> SubCommand
TeamTopSubCommand --|> SubCommand
TeamSetSpawnSubCommand --|> SubCommand
CoreCommand "1" *-- "1" TeamGroupSubCommand : contains
TeamGroupSubCommand "1" *-- "13" SubCommand : contains
}
}
note right of CoreCommand
Le plugin de jeu downstream
remplace une feuille avec :
core.getCoreCommand()
.findSubCommand("team")
.replaceSubCommand("create",
new MyCreate(svc));
end note
@enduml
+41 -49
View File
@@ -1,5 +1,5 @@
@startuml command-class-diagram
title CR-Core — Command framework (class diagram)
title CR-Core — Command framework (class diagram, nested sub-commands)
skinparam classAttributeIconSize 0
hide empty members
@@ -13,7 +13,7 @@ package "fr.luc.crcore.command" {
+ isPlayerOnly(): boolean
+ getDescription(): String
+ execute(ctx: CommandContext): CommandResult
+ tabComplete(sender, argIndex, partial): List<String>
+ tabComplete(sender, args: String[]): List<String>
+ matches(label: String): boolean
}
@@ -25,46 +25,41 @@ package "fr.luc.crcore.command" {
- description: String
- usage: String
- arguments: List<ArgumentDef>
# addAlias(...): void
# permission(p): void
# playerOnly(): void
# description(d): void
# usage(u): void
# argument(name, type): void
# optionalArgument(name, type): void
# buildContext(sender, label, subArgs): CommandContext
+ getRequiredArgumentCount(): int
+ getTotalArgumentCount(): int
+ getUsage(): String
- subCommandsByName: Map<String, SubCommand>
- subCommandsByAlias: Map<String, SubCommand>
--
# addAlias(...) / permission / playerOnly / description / usage
# argument(name, type) / optionalArgument(name, type)
# addSubCommand(sub: SubCommand): void
--
+ findSubCommand(label): Optional<SubCommand>
+ getSubCommands(): Collection<SubCommand>
+ replaceSubCommand(name, newSub): Optional<SubCommand>
+ hasSubCommands(): boolean
--
+ dispatch(sender, label, args): CommandResult
+ tabComplete(sender, args): List<String>
+ execute(ctx): CommandResult
# listSubCommands(ctx): CommandResult
# checkAccess(sender): boolean
# buildContext(sender, label, rawArgs): CommandContext
}
abstract class BaseCommand {
- subCommandsByName: Map<String, SubCommand>
- subCommandsByAlias: Map<String, SubCommand>
# addSubCommand(sub: SubCommand): void
+ findSubCommand(label: String): Optional<SubCommand>
+ getSubCommands(): Collection<SubCommand>
# execute(ctx): CommandResult
+ onCommand(sender, cmd, label, args): boolean
+ onTabComplete(sender, cmd, alias, args): List<String>
# checkAccess(sender, target): boolean
# handleResult(sender, result): void
}
abstract class SubCommand {
+ {abstract} execute(ctx: CommandContext): CommandResult
}
abstract class SubCommand
class CommandContext {
- sender: CommandSender
- label: String
- rawArgs: String[]
- parsedArgs: Map<String, Object>
+ getSender(): CommandSender
+ isPlayer(): boolean
+ getPlayer(): Optional<Player>
+ requirePlayer(): Player
+ get(name: String): T
+ getSender / isPlayer / getPlayer / requirePlayer
+ get(name): T
+ getOptional(name): Optional<T>
+ has(name): boolean
+ reply(msg): void
@@ -73,15 +68,7 @@ package "fr.luc.crcore.command" {
class CommandResult {
- type: Type
- message: String
+ getType(): Type
+ getMessage(): String
+ isSuccess(): boolean
+ {static} success(): CommandResult
+ {static} success(msg): CommandResult
+ {static} failure(msg): CommandResult
+ {static} invalidUsage(): CommandResult
+ {static} noPermission(): CommandResult
+ {static} playerOnly(): CommandResult
+ {static} success / failure / invalidUsage / noPermission / playerOnly
}
enum "CommandResult.Type" as ResultType {
@@ -99,17 +86,13 @@ package "fr.luc.crcore.command" {
+ suggestions(sender, partial): List<String>
}
class ArgumentTypes << (S, #FFC107) static >> {
+ {static} STRING: ArgumentType<String>
+ {static} INTEGER: ArgumentType<Integer>
+ {static} DOUBLE: ArgumentType<Double>
+ {static} BOOLEAN: ArgumentType<Boolean>
+ {static} ONLINE_PLAYER: ArgumentType<Player>
+ {static} enumOf(type): ArgumentType<E>
+ {static} choice(choices): ArgumentType<String>
class ArgumentTypes <<utility>> {
+ STRING / INTEGER / DOUBLE / BOOLEAN / ONLINE_PLAYER
+ enumOf(Class<E>): ArgumentType<E>
+ choice(String...): ArgumentType<String>
}
class ArgumentDef << (P, #BBBBBB) package-private >> {
class ArgumentDef <<package-private>> {
- name: String
- type: ArgumentType<?>
- required: boolean
@@ -118,15 +101,24 @@ package "fr.luc.crcore.command" {
AbstractCommand ..|> Command
BaseCommand --|> AbstractCommand
SubCommand --|> AbstractCommand
BaseCommand "1" o-- "*" SubCommand : subCommands
BaseCommand ..|> "org.bukkit.command.CommandExecutor"
BaseCommand ..|> "org.bukkit.command.TabCompleter"
AbstractCommand "1" o-- "*" SubCommand : sub-commands\n(recursive)
AbstractCommand "1" *-- "*" ArgumentDef : arguments
ArgumentDef --> ArgumentType
CommandResult +-- ResultType
CommandException --|> RuntimeException
BaseCommand ..> CommandContext : creates
AbstractCommand ..> CommandContext : creates
SubCommand ..> CommandResult : returns
AbstractCommand ..> CommandResult : returns
}
note bottom of AbstractCommand
Le routage est récursif :
/core team create → CoreCommand.dispatch("team", ["create",...])
→ TeamGroup.dispatch("create", [...])
→ TeamCreate.execute(ctx)
end note
@enduml
+86
View File
@@ -0,0 +1,86 @@
@startuml database-diagram
title CR-Core — Database (SQLite wrapper)
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.database" {
class Database {
- connection: Connection
+ Database(file: File)
+ execute(sql, params...): void
+ update(sql, params...): int
+ queryOne(sql, mapper, params...): Optional<T>
+ query(sql, mapper, params...): List<T>
+ inTransaction(block: Runnable): void
+ table(name: String): TableBuilder
+ tableExists(name: String): boolean
+ getConnection(): Connection
+ close(): void
}
Database ..|> "java.lang.AutoCloseable"
class TableBuilder {
- database: Database
- name: String
- columns: List<ColumnDef>
- ifNotExists: boolean
+ ifNotExists(): TableBuilder
+ column(name, type): ColumnDef
+ create(): void
}
class "TableBuilder.ColumnDef" as ColumnDef {
- name: String
- type: ColumnType
- primaryKey: boolean
- notNull: boolean
- unique: boolean
- defaultValue: String
+ primaryKey(): ColumnDef
+ notNull(): ColumnDef
+ unique(): ColumnDef
+ defaultValue(expr: String): ColumnDef
+ column(name, type): ColumnDef
+ create(): void
}
enum ColumnType {
INTEGER
REAL
TEXT
BLOB
BOOLEAN
UUID
--
+ getSqlType(): String
}
interface "RowMapper<T>" as RowMapper {
+ map(rs: ResultSet): T
}
class DatabaseException
DatabaseException --|> RuntimeException
Database "1" *-- "*" TableBuilder : creates
TableBuilder "1" *-- "*" ColumnDef : contains
ColumnDef --> ColumnType : type
Database ..> RowMapper : uses
Database ..> DatabaseException : throws
}
note right of Database
Repositories SQLite de CR-Core
(SqliteTeamRepository,
SqlitePlayerProfileRepository)
utilisent Database pour
persister state team/player.
Les plugins de jeu utilisent
Database.table(...) pour
créer leurs tables custom.
end note
@enduml
+89
View File
@@ -0,0 +1,89 @@
@startuml events-diagram
title CR-Core — Bukkit events (team + player)
skinparam classAttributeIconSize 0
hide empty members
package "org.bukkit.event" {
abstract class Event
}
package "fr.luc.crcore.team.event" {
abstract class TeamEvent {
- team: Team
+ getTeam(): Team
}
TeamEvent --|> Event
class TeamCreateEvent
class TeamDissolveEvent
class TeamMemberAddEvent {
+ getMember(): TeamMember
}
class TeamMemberRemoveEvent {
+ getPlayerId(): UUID
}
class PlayerJoinTeamEvent {
+ getMember(): TeamMember
}
class TeamLeadershipTransferEvent {
+ getOldLeaderId(): UUID
+ getNewLeaderId(): UUID
}
class TeamVisibilityChangeEvent {
+ getOldVisibility(): TeamVisibility
+ getNewVisibility(): TeamVisibility
}
class TeamScoreChangeEvent {
+ getScoreName(): String
+ getOldValue(): int
+ getNewValue(): int
+ getDelta(): int
}
class TeamSpawnPointChangeEvent {
+ getOldLocation(): Location
+ getNewLocation(): Location
}
TeamCreateEvent --|> TeamEvent
TeamDissolveEvent --|> TeamEvent
TeamMemberAddEvent --|> TeamEvent
TeamMemberRemoveEvent --|> TeamEvent
PlayerJoinTeamEvent --|> TeamEvent
TeamLeadershipTransferEvent --|> TeamEvent
TeamVisibilityChangeEvent --|> TeamEvent
TeamScoreChangeEvent --|> TeamEvent
TeamSpawnPointChangeEvent --|> TeamEvent
}
package "fr.luc.crcore.player.event" {
abstract class PlayerProfileEvent {
- profile: PlayerProfile
+ getProfile(): PlayerProfile
}
PlayerProfileEvent --|> Event
class PlayerProfileCreateEvent
class PlayerProfileDeleteEvent
class PlayerScoreChangeEvent {
+ getScoreName(): String
+ getOldValue(): int
+ getNewValue(): int
+ getDelta(): int
}
PlayerProfileCreateEvent --|> PlayerProfileEvent
PlayerProfileDeleteEvent --|> PlayerProfileEvent
PlayerScoreChangeEvent --|> PlayerProfileEvent
}
note right of TeamEvent
Tous post-events, non-cancellable.
Tirés par les sous-classes
BukkitEventFiring*ServiceImpl
via les hooks on* hérités.
end note
@enduml
+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, )_
+170 -115
View File
@@ -4,10 +4,9 @@
- **Type** : librairie Java (`jar`) — pas un plugin Bukkit
- **artifactId Maven** : `CR-Core`
- **Build** : Maven
- **Java** : 16
- **Build** : Maven, Java 16
- **API serveur (provided)** : Paper 1.16.5
(`com.destroystokyo.paper:paper-api:1.16.5-R0.1-SNAPSHOT`)
- **SQLite (compile)** : `org.xerial:sqlite-jdbc:3.45.3.0`
- **Package racine** : `fr.luc.crcore`
## Dépôts Maven
@@ -22,12 +21,12 @@
mvn clean install
```
Cela publie `fr.luc:CR-Core:1.0-SNAPSHOT` dans le repo Maven local `~/.m2/`,
Publie `fr.luc:CR-Core:1.0-SNAPSHOT` dans le repo Maven local `~/.m2/`,
prêt à être consommé par les plugins de jeu.
## Utilisation depuis un plugin de jeu
## Intégration dans un plugin de jeu
Dans le `pom.xml` du plugin de jeu :
### `pom.xml`
```xml
<dependency>
@@ -38,106 +37,111 @@ Dans le `pom.xml` du plugin de jeu :
</dependency>
```
> Scope `compile` (et non `provided`) **si** le plugin de jeu shade CR-Core dans
> son propre jar (recommandé pour de la pure librairie). Pensez à utiliser un
> `<relocation>` dans le `maven-shade-plugin` du plugin de jeu pour éviter les
> conflits si plusieurs plugins shadent CR-Core sur le même serveur.
Le plugin de jeu doit **shader** CR-Core dans son jar final (avec
`maven-shade-plugin`) pour que sqlite-jdbc + le code du noyau soient bien
embarqués sur le serveur.
Côté code du plugin de jeu :
### `plugin.yml` du plugin de jeu
```yaml
name: MyGame
main: fr.exemple.mygame.MyGamePlugin
version: 1.0
api-version: 1.16
commands:
core:
description: Commandes CR-Core
# aliases optionnels — équivalents au point de vue Bukkit
aliases: [cr, crcore]
```
### Code minimal
```java
public final class MyGamePlugin extends JavaPlugin {
public class MyGamePlugin extends JavaPlugin {
private TeamService teamService;
private CRCore core;
@Override
public void onEnable() {
// Instancie le service team (chaque plugin a son propre registre)
this.teamService = new TeamServiceImpl(new InMemoryTeamRepository());
// 1 ligne = SQLite + services + /core team ... opérationnels
this.core = new CRCore(this).enable();
// Enregistre une commande basée sur le framework de CR-Core
getCommand("team").setExecutor(new TeamCommand(teamService));
getCommand("team").setTabCompleter(new TeamCommand(teamService));
// Listener custom sur les events team
getServer().getPluginManager().registerEvents(new MyTeamListener(), this);
// Table custom pour stocker des données spécifiques au jeu
core.getDatabase().table("my_kills")
.ifNotExists()
.column("player_id", ColumnType.UUID).primaryKey()
.column("kills", ColumnType.INTEGER).notNull().defaultValue("0")
.create();
}
@Override
public void onDisable() {
if (core != null) core.disable();
}
}
```
Exemple minimal de commande :
### Écouter les évènements
```java
public class TeamCommand extends BaseCommand {
public class MyTeamListener implements Listener {
public TeamCommand(TeamService service) {
super("team", "teams", "t");
description("Manage teams");
addSubCommand(new TeamCreateSub(service));
addSubCommand(new TeamInfoSub(service));
@EventHandler
public void onTeamCreate(TeamCreateEvent event) {
Team team = event.getTeam();
Bukkit.broadcastMessage("Nouvelle équipe : " + team.getName());
}
@EventHandler
public void onPlayerJoinTeam(PlayerJoinTeamEvent event) {
// Auto-join uniquement (chef qui ajoute = TeamMemberAddEvent)
}
@EventHandler
public void onScoreChange(TeamScoreChangeEvent event) {
// event.getScoreName(), event.getOldValue(), event.getNewValue()
}
}
```
public class TeamCreateSub extends SubCommand {
### Overrider une commande par défaut
private final TeamService service;
public TeamCreateSub(TeamService service) {
super("create", "c", "new");
this.service = service;
description("Create a new team");
permission("mygame.team.create");
playerOnly();
argument("name", ArgumentTypes.STRING);
argument("tag", ArgumentTypes.STRING);
argument("color", ArgumentTypes.enumOf(TeamColor.class));
```java
public class MyCreateCommand extends TeamCreateSubCommand {
public MyCreateCommand(TeamService service) {
super(service);
permission("mygame.team.create"); // permission custom
}
@Override
public CommandResult execute(CommandContext ctx) {
Player player = ctx.requirePlayer();
String name = ctx.get("name");
String tag = ctx.get("tag");
TeamColor color = ctx.get("color");
try {
Team team = service.createTeam(name, tag, color, player.getUniqueId());
return CommandResult.success("Équipe " + team.getName() + " créée !");
} catch (TeamException ex) {
return CommandResult.failure(ex.getMessage());
}
// logique custom
return super.execute(ctx);
}
}
// Dans onEnable() :
core.getCoreCommand().findSubCommand("team")
.ifPresent(team -> team.replaceSubCommand("create", new MyCreateCommand(core.getTeamService())));
```
Et dans le `plugin.yml` du plugin de jeu :
```yaml
name: MyGame
main: fr.luc.mygame.MyGamePlugin
version: 1.0
api-version: 1.16
commands:
team:
description: Manage teams
aliases: [teams, t]
```
## Override
Toute classe du noyau peut être étendue :
### Stocker / récupérer des données custom via SQLite
```java
public class LoggingTeamService extends TeamServiceImpl {
Database db = core.getDatabase();
public LoggingTeamService(TeamRepository repo) { super(repo); }
db.update("INSERT OR REPLACE INTO my_kills (player_id, kills) VALUES (?, ?)",
playerUuid, 42);
@Override
protected void onAfterCreate(Team team) {
getPlugin().getLogger().info("Team created: " + team.getName());
}
@Override
protected Team newTeam(UUID id, String name, String tag, TeamColor color, UUID leaderId) {
return new MyCustomTeam(id, name, tag, color, leaderId);
}
}
int kills = db.queryOne(
"SELECT kills FROM my_kills WHERE player_id = ?",
rs -> rs.getInt("kills"),
playerUuid
).orElse(0);
```
## Arborescence du projet
@@ -146,61 +150,112 @@ public class LoggingTeamService extends TeamServiceImpl {
CitesPlugin/ # dossier IntelliJ (renommer plus tard si voulu)
├── pom.xml
├── GEMINI.md
├── docs/ # source de vérité
├── docs/
│ ├── README.md
│ ├── setup.md
│ ├── features.md
│ ├── decisions.md
│ └── diagrams/
│ ├── team-class-diagram.puml
│ ├── team-create-sequence.puml
│ ├── team-create-activity.puml
│ └── command-class-diagram.puml
│ └── diagrams/*.puml
└── src/main/java/fr/luc/crcore/
├── common/ # abstractions partagées
│ ├── Identifiable.java # interface
│ ├── Named.java # interface
│ ├── ScoreHolder.java # interface (impl. par Team et PlayerProfile)
│ ├── AbstractEntity.java # abstract class
── Repository.java # interface
├── command/ # framework de commandes
── Command.java # interface
│ ├── AbstractCommand.java # base partagée
│ ├── BaseCommand.java # commande top-level (Bukkit-aware)
│ ├── SubCommand.java # sous-commande
├── CRCore.java # bootstrap orchestrator
├── CRCoreConfig.java # config (sqlite, command name, …)
├── common/
│ ├── Identifiable.java
│ ├── Named.java
── ScoreHolder.java # contrat partagé Team + PlayerProfile
│ ├── AbstractEntity.java
── Repository.java
├── database/ # wrapper SQLite
│ ├── Database.java
│ ├── TableBuilder.java
│ ├── ColumnType.java
│ ├── RowMapper.java
│ └── DatabaseException.java
├── command/ # framework
│ ├── Command.java (interface)
│ ├── AbstractCommand.java # base partagée, nested sub-commands
│ ├── BaseCommand.java # top-level Bukkit-aware
│ ├── SubCommand.java
│ ├── CommandContext.java
│ ├── CommandResult.java
│ ├── CommandException.java
│ ├── ArgumentType.java
│ ├── ArgumentTypes.java # STRING, INTEGER, BOOLEAN, ONLINE_PLAYER, enumOf, choice
── ArgumentDef.java # package-private
├── team/ # domaine team
│ ├── ArgumentTypes.java
── ArgumentDef.java # package-private
│ └── builtin/ # commandes prêtes à l'emploi
│ ├── CoreCommand.java # /core
│ └── team/
│ ├── TeamGroupSubCommand.java # /core team (container)
│ ├── TeamArgumentTypes.java # ArgumentType<Team> avec tab-complete
│ ├── TeamCreateSubCommand.java # /core team create
│ ├── TeamDeleteSubCommand.java # /core team delete
│ ├── TeamAddSubCommand.java # /core team add
│ ├── TeamRemoveSubCommand.java # /core team remove
│ ├── TeamJoinSubCommand.java # /core team join
│ ├── TeamLeaveSubCommand.java # /core team leave
│ ├── TeamInfoSubCommand.java # /core team info
│ ├── TeamListSubCommand.java # /core team list
│ ├── TeamTransferSubCommand.java # /core team transfer
│ ├── TeamVisibilitySubCommand.java # /core team visibility
│ ├── TeamScoreSubCommand.java # /core team score (admin)
│ ├── TeamTopSubCommand.java # /core team top
│ └── TeamSetSpawnSubCommand.java # /core team setspawn
├── team/
│ ├── Team.java
│ ├── TeamMember.java
│ ├── TeamRole.java # enum
│ ├── TeamColor.java # enum
│ ├── TeamVisibility.java # enum (PUBLIC / PRIVATE)
│ ├── TeamRanking.java # record (rank, team, score)
│ ├── TeamRepository.java # interface
│ ├── InMemoryTeamRepository.java # impl
│ ├── TeamService.java # interface
│ ├── TeamServiceImpl.java # impl avec hooks overridables
│ ├── TeamRole.java
│ ├── TeamColor.java
│ ├── TeamVisibility.java
│ ├── TeamRanking.java # record
│ ├── TeamRepository.java
│ ├── InMemoryTeamRepository.java
│ ├── SqliteTeamRepository.java
│ ├── TeamService.java
│ ├── TeamServiceImpl.java
│ ├── BukkitEventFiringTeamServiceImpl.java # impl par défaut
│ ├── TeamException.java
│ ├── TeamAlreadyExistsException.java
│ ├── TeamNotFoundException.java
── TeamAccessException.java # auto-join refusé
└── player/ # domaine player
├── PlayerProfile.java # entité, scores par joueur
├── PlayerRanking.java # record (rank, profile, score)
├── PlayerProfileRepository.java # interface
── TeamAccessException.java
│ └── event/ # Bukkit events team
├── TeamEvent.java # base
├── TeamCreateEvent.java
├── TeamDissolveEvent.java
│ ├── TeamMemberAddEvent.java
│ ├── TeamMemberRemoveEvent.java
│ ├── PlayerJoinTeamEvent.java
│ ├── TeamLeadershipTransferEvent.java
│ ├── TeamVisibilityChangeEvent.java
│ ├── TeamScoreChangeEvent.java
│ └── TeamSpawnPointChangeEvent.java
└── player/
├── PlayerProfile.java
├── PlayerRanking.java
├── PlayerProfileRepository.java
├── InMemoryPlayerProfileRepository.java
├── PlayerProfileService.java # interface
├── PlayerProfileServiceImpl.java # impl avec hooks overridables
├── SqlitePlayerProfileRepository.java
├── PlayerProfileService.java
├── PlayerProfileServiceImpl.java
├── BukkitEventFiringPlayerProfileServiceImpl.java
├── PlayerException.java
── PlayerProfileNotFoundException.java
── PlayerProfileNotFoundException.java
└── event/ # Bukkit events player
├── PlayerProfileEvent.java
├── PlayerProfileCreateEvent.java
├── PlayerProfileDeleteEvent.java
└── PlayerScoreChangeEvent.java
```
> **Note IntelliJ** : le dossier physique s'appelle encore `CitesPlugin/`. Pour
> le renommer en `CR-Core/`, fermer IntelliJ, renommer, rouvrir. Le `pom.xml`
> et les packages sont déjà à jour, le nom du dossier n'a aucun impact sur le
> build.
## Tables SQLite créées par CR-Core
Au premier `enable()`, les tables suivantes sont créées (en `IF NOT EXISTS`) :
- `crcore_teams` — métadonnées par équipe (id, name, tag, color, leader_id,
visibility, spawn_world/x/y/z/yaw/pitch)
- `crcore_team_members` — un membre = (team_id, player_id, role, joined_at)
- `crcore_team_scores` — (team_id, score_name, value)
- `crcore_player_profiles` — un profil = (id)
- `crcore_player_scores` — (profile_id, score_name, value)
Le préfixe `crcore_` évite les collisions avec les tables custom du plugin
de jeu.