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:
+31
-20
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user