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:
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -8,41 +8,59 @@
|
|||||||
être lue depuis `docs/` et écrite dans `docs/`.
|
être lue depuis `docs/` et écrite dans `docs/`.
|
||||||
- Avant d'implémenter, vérifier ce qui est consigné dans `docs/`.
|
- Avant d'implémenter, vérifier ce qui est consigné dans `docs/`.
|
||||||
- Toute nouvelle information donnée par l'utilisateur va dans le fichier adapté :
|
- Toute nouvelle information donnée par l'utilisateur va dans le fichier adapté :
|
||||||
- `docs/features.md` — domaines fonctionnels (team, command, …)
|
- `docs/features.md` — domaines fonctionnels (team, player, commandes built-in, events, database)
|
||||||
- `docs/decisions.md` — décisions techniques / architecturales
|
- `docs/decisions.md` — décisions techniques / architecturales
|
||||||
- `docs/setup.md` — installation, build, intégration côté plugin de jeu
|
- `docs/setup.md` — installation, build, intégration côté plugin de jeu
|
||||||
- `docs/README.md` — vue d'ensemble et index
|
- `docs/README.md` — vue d'ensemble et index
|
||||||
- `docs/diagrams/*.puml` — diagrammes (classe / séquence / activité)
|
- `docs/diagrams/*.puml` — diagrammes (classe / séquence / activité)
|
||||||
- En cas de conflit code ↔ `docs/`, `docs/` fait foi : aligner le code, ou
|
- En cas de conflit code ↔ `docs/`, `docs/` fait foi.
|
||||||
mettre la doc à jour explicitement avec l'utilisateur.
|
|
||||||
|
|
||||||
## Nature du projet
|
## Nature du projet
|
||||||
|
|
||||||
**CR-Core** est une **librairie Java/Maven pure** — pas un plugin Bukkit. Elle
|
**CR-Core** est une **librairie Java/Maven** consommée par des plugins Bukkit
|
||||||
fournit les briques réutilisables (domaine team, framework de commandes,
|
("plugins de jeu"). Pas de `plugin.yml` côté core — c'est le plugin de jeu
|
||||||
abstractions communes) que chaque plugin de jeu (futur `CitesPlugin`, etc.)
|
qui en a un et qui instancie {@code CRCore} dans son {@code onEnable()}.
|
||||||
consomme en dépendance Maven.
|
|
||||||
|
|
||||||
- **Nom** : CR-Core (artifactId Maven : `CR-Core`)
|
- **Nom** : CR-Core (artifactId Maven : `CR-Core`)
|
||||||
- **Type** : librairie (`jar`) — pas de `plugin.yml`, pas de `JavaPlugin`
|
- **Type** : librairie (`jar`)
|
||||||
- **Cible runtime** : serveur Paper/Spigot 1.16.5 (le plugin de jeu downstream
|
- **Cible runtime** : serveur Paper/Spigot 1.16.5
|
||||||
est responsable du `plugin.yml` et de l'enregistrement des commandes)
|
|
||||||
- **Build** : Maven, Java 16
|
- **Build** : Maven, Java 16
|
||||||
- **Package racine** : `fr.luc.crcore`
|
- **Package racine** : `fr.luc.crcore`
|
||||||
|
- **SQLite** : `org.xerial:sqlite-jdbc` (compile scope)
|
||||||
|
|
||||||
|
## Domaines
|
||||||
|
|
||||||
|
- `fr.luc.crcore.common` — abstractions partagées (`Identifiable`, `Named`,
|
||||||
|
`ScoreHolder`, `AbstractEntity`, `Repository<T>`)
|
||||||
|
- `fr.luc.crcore.team` — équipes : visibilité (PUBLIC/PRIVATE), membres,
|
||||||
|
leader, scores nommés, classements, point de spawn. Sous-package `event/`
|
||||||
|
pour les évènements Bukkit.
|
||||||
|
- `fr.luc.crcore.player` — profils joueurs : scores nommés, classements
|
||||||
|
individuels. Sous-package `event/`.
|
||||||
|
- `fr.luc.crcore.command` — framework de commandes (nested sub-commands,
|
||||||
|
arguments typés, tab-complete). Sous-package `builtin/` pour les commandes
|
||||||
|
par défaut `/core team ...`.
|
||||||
|
- `fr.luc.crcore.database` — wrapper SQLite minimaliste (`Database`,
|
||||||
|
`TableBuilder`, `RowMapper`).
|
||||||
|
- `fr.luc.crcore.CRCore` — point d'entrée bootstrap (instancié en une ligne
|
||||||
|
par le plugin de jeu).
|
||||||
|
|
||||||
## Principe : simple par défaut, overridable partout
|
## Principe : simple par défaut, overridable partout
|
||||||
|
|
||||||
Toutes les classes du noyau sont conçues pour être étendues. Les services
|
Chaque service expose des **hooks `protected`** que les plugins de jeu peuvent
|
||||||
fournissent une implémentation "tout faite" (ex. `TeamServiceImpl`), mais chaque
|
overrider :
|
||||||
étape importante est exposée via une méthode `protected` (`newTeam`,
|
- factories (`newTeam`, `newRanking`, `newProfile`)
|
||||||
`validateName`, `onAfterCreate`, …) qu'une sous-classe peut overrider.
|
- validations (`validateName`, `validateJoinable`, …)
|
||||||
|
- post-hooks (`onAfterCreate`, `onMemberAdded`, `onScoreChanged`, …)
|
||||||
|
|
||||||
|
Chaque commande built-in (`TeamCreateSubCommand`, etc.) est elle aussi
|
||||||
|
substituable par nom via `replaceSubCommand(...)`.
|
||||||
|
|
||||||
**Règles** :
|
**Règles** :
|
||||||
- Pas de `final` sur les classes du noyau, sauf raison forte.
|
- Pas de `final` sur les classes du noyau.
|
||||||
- Méthodes-clés en `protected` (pas `private`) pour permettre l'override.
|
- Méthodes-clés en `protected` (pas `private`).
|
||||||
- Hooks `onBefore...` / `onAfter...` partout où ça a du sens.
|
- Hooks `onBefore...` / `onAfter...` partout où ça a du sens.
|
||||||
- Factories (`newTeam`, `newMember`, …) pour permettre de substituer des
|
- Factories pour permettre les sous-classes.
|
||||||
sous-classes sans réécrire le service.
|
|
||||||
|
|
||||||
## Workflow attendu
|
## Workflow attendu
|
||||||
|
|
||||||
@@ -54,6 +72,7 @@ fournissent une implémentation "tout faite" (ex. `TeamServiceImpl`), mais chaqu
|
|||||||
## Conventions de code
|
## Conventions de code
|
||||||
|
|
||||||
- Code (classes, méthodes, attributs, variables) en **anglais standard**.
|
- Code (classes, méthodes, attributs, variables) en **anglais standard**.
|
||||||
- Messages joueur et documentation en français.
|
- Messages joueur et documentation en **français**.
|
||||||
|
- JavaDoc en français sur les classes publiques et les méthodes non triviales.
|
||||||
- Séparation stricte : `interface`, `enum`, `abstract class`, `class` concrète,
|
- Séparation stricte : `interface`, `enum`, `abstract class`, `class` concrète,
|
||||||
`exception` — chacun dans son fichier.
|
`exception` — chacun dans son fichier.
|
||||||
|
|||||||
+31
-20
@@ -5,29 +5,35 @@ mécaniques, règles et spécifications du noyau sont consignées ici.
|
|||||||
|
|
||||||
## Objectif du projet
|
## Objectif du projet
|
||||||
|
|
||||||
**CR-Core** est une **librairie Maven** réutilisable pour construire des plugins
|
**CR-Core** est une **librairie Maven** réutilisable pour construire des
|
||||||
Minecraft. Elle fournit :
|
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`,
|
- **Abstractions communes** — `Identifiable`, `Named`, `ScoreHolder`,
|
||||||
`AbstractEntity`, `Repository<T>`) ;
|
`AbstractEntity`, `Repository<T>`.
|
||||||
- un **domaine équipes** clé en main (`Team`, `TeamMember`, `TeamService`,
|
- **Domaine Team** — équipes (nom, tag, couleur, chef, membres,
|
||||||
`TeamRepository`) — visibilité, scores, classements, spawn, overridable ;
|
visibilité PUBLIC/PRIVATE, scores nommés, classements, point de spawn),
|
||||||
- un **domaine profils joueurs** (`PlayerProfile`, `PlayerProfileService`) —
|
service overridable, exceptions dédiées.
|
||||||
scores nommés par joueur, classements individuels ;
|
- **Domaine Player** — profils joueurs (scores nommés, classements
|
||||||
- un **framework de commandes** (`BaseCommand`, `SubCommand`, `ArgumentType`,
|
individuels), service auto-créant les profils à la demande.
|
||||||
tab-completion intégrée).
|
- **Framework de commandes** — `BaseCommand` / `SubCommand` imbriqués,
|
||||||
|
arguments typés, tab-complétion, permissions, player-only.
|
||||||
Les plugins de jeu (futur `CitesPlugin`, BedWars, etc.) déclarent CR-Core en
|
- **Commandes par défaut** — `/core team [create|delete|add|remove|join|`
|
||||||
dépendance Maven `provided` (ou shadent la lib) et l'utilisent directement.
|
`leave|info|list|transfer|visibility|score|top|setspawn]` fonctionnelles
|
||||||
|
out-of-the-box, chacune substituable par sous-classe.
|
||||||
- Cible runtime : **Minecraft 1.16.5** (API Paper).
|
- **Évènements Bukkit** — 9 events team + 3 events player, à écouter avec
|
||||||
- Build : Maven, Java 16.
|
`@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
|
## Structure de la documentation
|
||||||
|
|
||||||
- `README.md` — Ce fichier. Vue d'ensemble et index.
|
- `README.md` — Ce fichier. Vue d'ensemble et index.
|
||||||
- `setup.md` — Build, intégration dans un plugin de jeu, exemple d'usage.
|
- `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).
|
- `decisions.md` — Journal des décisions importantes (ADR léger).
|
||||||
- `diagrams/` — Diagrammes PlantUML (`.puml`).
|
- `diagrams/` — Diagrammes PlantUML (`.puml`).
|
||||||
|
|
||||||
@@ -36,17 +42,22 @@ dépendance Maven `provided` (ou shadent la lib) et l'utilisent directement.
|
|||||||
| Fichier | Type | Sujet |
|
| Fichier | Type | Sujet |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| [team-class-diagram.puml](diagrams/team-class-diagram.puml) | Classe | Domaine Team + abstractions communes |
|
| [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-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 |
|
| [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) |
|
| [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 |
|
| [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
|
## Conventions
|
||||||
|
|
||||||
- Code : **anglais standard**, séparation stricte interfaces / enums / classes
|
- Code : **anglais standard**, séparation stricte interfaces / enums / classes
|
||||||
abstraites / classes concrètes / exceptions.
|
abstraites / classes concrètes / exceptions.
|
||||||
- Doc & messages joueur : **français**.
|
- Doc & messages joueur : **français**.
|
||||||
|
- JavaDoc en français sur les classes publiques et méthodes non triviales.
|
||||||
- Package racine : `fr.luc.crcore`.
|
- Package racine : `fr.luc.crcore`.
|
||||||
- Classes du noyau **non-`final`**, méthodes-clés `protected`, hooks
|
- Classes du noyau **non-`final`**, méthodes-clés `protected`, hooks
|
||||||
`onBefore…`/`onAfter…` et factories `new…` pour l'override.
|
`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
|
pour ~8 méthodes simples. Choix actuel : duplication contrôlée des
|
||||||
implémentations (Map + getters/setters), interface commune pour le
|
implémentations (Map + getters/setters), interface commune pour le
|
||||||
contrat.
|
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
|
@startuml command-class-diagram
|
||||||
title CR-Core — Command framework (class diagram)
|
title CR-Core — Command framework (class diagram, nested sub-commands)
|
||||||
|
|
||||||
skinparam classAttributeIconSize 0
|
skinparam classAttributeIconSize 0
|
||||||
hide empty members
|
hide empty members
|
||||||
@@ -13,7 +13,7 @@ package "fr.luc.crcore.command" {
|
|||||||
+ isPlayerOnly(): boolean
|
+ isPlayerOnly(): boolean
|
||||||
+ getDescription(): String
|
+ getDescription(): String
|
||||||
+ execute(ctx: CommandContext): CommandResult
|
+ execute(ctx: CommandContext): CommandResult
|
||||||
+ tabComplete(sender, argIndex, partial): List<String>
|
+ tabComplete(sender, args: String[]): List<String>
|
||||||
+ matches(label: String): boolean
|
+ matches(label: String): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,46 +25,41 @@ package "fr.luc.crcore.command" {
|
|||||||
- description: String
|
- description: String
|
||||||
- usage: String
|
- usage: String
|
||||||
- arguments: List<ArgumentDef>
|
- arguments: List<ArgumentDef>
|
||||||
# addAlias(...): void
|
- subCommandsByName: Map<String, SubCommand>
|
||||||
# permission(p): void
|
- subCommandsByAlias: Map<String, SubCommand>
|
||||||
# playerOnly(): void
|
--
|
||||||
# description(d): void
|
# addAlias(...) / permission / playerOnly / description / usage
|
||||||
# usage(u): void
|
# argument(name, type) / optionalArgument(name, type)
|
||||||
# argument(name, type): void
|
# addSubCommand(sub: SubCommand): void
|
||||||
# optionalArgument(name, type): void
|
--
|
||||||
# buildContext(sender, label, subArgs): CommandContext
|
+ findSubCommand(label): Optional<SubCommand>
|
||||||
+ getRequiredArgumentCount(): int
|
+ getSubCommands(): Collection<SubCommand>
|
||||||
+ getTotalArgumentCount(): int
|
+ replaceSubCommand(name, newSub): Optional<SubCommand>
|
||||||
+ getUsage(): String
|
+ 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 {
|
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
|
+ onCommand(sender, cmd, label, args): boolean
|
||||||
+ onTabComplete(sender, cmd, alias, args): List<String>
|
+ onTabComplete(sender, cmd, alias, args): List<String>
|
||||||
# checkAccess(sender, target): boolean
|
|
||||||
# handleResult(sender, result): void
|
# handleResult(sender, result): void
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class SubCommand {
|
abstract class SubCommand
|
||||||
+ {abstract} execute(ctx: CommandContext): CommandResult
|
|
||||||
}
|
|
||||||
|
|
||||||
class CommandContext {
|
class CommandContext {
|
||||||
- sender: CommandSender
|
- sender: CommandSender
|
||||||
- label: String
|
- label: String
|
||||||
- rawArgs: String[]
|
- rawArgs: String[]
|
||||||
- parsedArgs: Map<String, Object>
|
- parsedArgs: Map<String, Object>
|
||||||
+ getSender(): CommandSender
|
+ getSender / isPlayer / getPlayer / requirePlayer
|
||||||
+ isPlayer(): boolean
|
+ get(name): T
|
||||||
+ getPlayer(): Optional<Player>
|
|
||||||
+ requirePlayer(): Player
|
|
||||||
+ get(name: String): T
|
|
||||||
+ getOptional(name): Optional<T>
|
+ getOptional(name): Optional<T>
|
||||||
+ has(name): boolean
|
+ has(name): boolean
|
||||||
+ reply(msg): void
|
+ reply(msg): void
|
||||||
@@ -73,15 +68,7 @@ package "fr.luc.crcore.command" {
|
|||||||
class CommandResult {
|
class CommandResult {
|
||||||
- type: Type
|
- type: Type
|
||||||
- message: String
|
- message: String
|
||||||
+ getType(): Type
|
+ {static} success / failure / invalidUsage / noPermission / playerOnly
|
||||||
+ getMessage(): String
|
|
||||||
+ isSuccess(): boolean
|
|
||||||
+ {static} success(): CommandResult
|
|
||||||
+ {static} success(msg): CommandResult
|
|
||||||
+ {static} failure(msg): CommandResult
|
|
||||||
+ {static} invalidUsage(): CommandResult
|
|
||||||
+ {static} noPermission(): CommandResult
|
|
||||||
+ {static} playerOnly(): CommandResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum "CommandResult.Type" as ResultType {
|
enum "CommandResult.Type" as ResultType {
|
||||||
@@ -99,17 +86,13 @@ package "fr.luc.crcore.command" {
|
|||||||
+ suggestions(sender, partial): List<String>
|
+ suggestions(sender, partial): List<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArgumentTypes << (S, #FFC107) static >> {
|
class ArgumentTypes <<utility>> {
|
||||||
+ {static} STRING: ArgumentType<String>
|
+ STRING / INTEGER / DOUBLE / BOOLEAN / ONLINE_PLAYER
|
||||||
+ {static} INTEGER: ArgumentType<Integer>
|
+ enumOf(Class<E>): ArgumentType<E>
|
||||||
+ {static} DOUBLE: ArgumentType<Double>
|
+ choice(String...): ArgumentType<String>
|
||||||
+ {static} BOOLEAN: ArgumentType<Boolean>
|
|
||||||
+ {static} ONLINE_PLAYER: ArgumentType<Player>
|
|
||||||
+ {static} enumOf(type): ArgumentType<E>
|
|
||||||
+ {static} choice(choices): ArgumentType<String>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArgumentDef << (P, #BBBBBB) package-private >> {
|
class ArgumentDef <<package-private>> {
|
||||||
- name: String
|
- name: String
|
||||||
- type: ArgumentType<?>
|
- type: ArgumentType<?>
|
||||||
- required: boolean
|
- required: boolean
|
||||||
@@ -118,15 +101,24 @@ package "fr.luc.crcore.command" {
|
|||||||
AbstractCommand ..|> Command
|
AbstractCommand ..|> Command
|
||||||
BaseCommand --|> AbstractCommand
|
BaseCommand --|> AbstractCommand
|
||||||
SubCommand --|> 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
|
AbstractCommand "1" *-- "*" ArgumentDef : arguments
|
||||||
ArgumentDef --> ArgumentType
|
ArgumentDef --> ArgumentType
|
||||||
CommandResult +-- ResultType
|
CommandResult +-- ResultType
|
||||||
CommandException --|> RuntimeException
|
CommandException --|> RuntimeException
|
||||||
|
|
||||||
BaseCommand ..> CommandContext : creates
|
|
||||||
AbstractCommand ..> 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
|
@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
|
# Domaines fonctionnels
|
||||||
|
|
||||||
CR-Core est une librairie. Chaque domaine est autonome ; le plugin de jeu
|
CR-Core est une librairie. Le plugin de jeu downstream l'instancie en une
|
||||||
downstream pioche ce qu'il utilise.
|
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
|
## 3. Framework de commandes
|
||||||
|
|
||||||
**Statut** : framework implémenté. Pas de commande Team intégrée — c'est au
|
**Statut** : framework implémenté avec **sous-commandes imbriquées récursives**
|
||||||
plugin de jeu de définir ses commandes en utilisant les briques fournies.
|
(supporte `/core team create`, `/core team join`, etc.). Les commandes par
|
||||||
|
défaut sont fournies en section 4.
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
- **`Command`** (interface) — contrat partagé : `getName()`, `getAliases()`,
|
- **`Command`** (interface) — contrat partagé : `getName()`, `getAliases()`,
|
||||||
`getPermission()`, `isPlayerOnly()`, `getDescription()`, `execute(ctx)`,
|
`getPermission()`, `isPlayerOnly()`, `getDescription()`, `execute(ctx)`,
|
||||||
`tabComplete(sender, argIndex, partial)`, `matches(label)`.
|
`tabComplete(sender, args)`, `matches(label)`.
|
||||||
- **`AbstractCommand`** (abstract, implémente `Command`) — porte tous les
|
- **`AbstractCommand`** (abstract, implémente `Command`) — porte tous les
|
||||||
champs : nom, aliases, permission, player-only, description, usage,
|
champs ET la table des sous-commandes (imbrication). Méthodes builder en
|
||||||
arguments. Méthodes builder en `protected` : `addAlias`, `permission`,
|
`protected` : `addAlias`, `permission`, `playerOnly`, `description`, `usage`,
|
||||||
`playerOnly`, `description`, `usage`, `argument`, `optionalArgument`.
|
`argument`, `optionalArgument`, `addSubCommand`. Routage récursif via
|
||||||
- **`BaseCommand extends AbstractCommand`** — implémente aussi
|
`dispatch(...)` et `tabComplete(...)`.
|
||||||
`CommandExecutor` et `TabCompleter` de Bukkit. Conteneur de `SubCommand`,
|
- **`BaseCommand extends AbstractCommand`** — implémente `CommandExecutor` et
|
||||||
fait le routage `args[0]` → sous-commande, gère permissions, player-only,
|
`TabCompleter` de Bukkit, branche `onCommand` → `dispatch`. À utiliser pour
|
||||||
invalid usage, affichage de l'aide par défaut.
|
la racine d'un arbre (`/core`).
|
||||||
- **`SubCommand extends AbstractCommand`** — sous-commande sans logique
|
- **`SubCommand extends AbstractCommand`** — sous-commande ; peut être
|
||||||
Bukkit. La méthode abstraite `execute(CommandContext)` est à implémenter.
|
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`)
|
### 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
|
## 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
|
- **Type** : librairie Java (`jar`) — pas un plugin Bukkit
|
||||||
- **artifactId Maven** : `CR-Core`
|
- **artifactId Maven** : `CR-Core`
|
||||||
- **Build** : Maven
|
- **Build** : Maven, Java 16
|
||||||
- **Java** : 16
|
|
||||||
- **API serveur (provided)** : Paper 1.16.5
|
- **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`
|
- **Package racine** : `fr.luc.crcore`
|
||||||
|
|
||||||
## Dépôts Maven
|
## Dépôts Maven
|
||||||
@@ -22,12 +21,12 @@
|
|||||||
mvn clean install
|
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.
|
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
|
```xml
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -38,106 +37,111 @@ Dans le `pom.xml` du plugin de jeu :
|
|||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
> Scope `compile` (et non `provided`) **si** le plugin de jeu shade CR-Core dans
|
Le plugin de jeu doit **shader** CR-Core dans son jar final (avec
|
||||||
> son propre jar (recommandé pour de la pure librairie). Pensez à utiliser un
|
`maven-shade-plugin`) pour que sqlite-jdbc + le code du noyau soient bien
|
||||||
> `<relocation>` dans le `maven-shade-plugin` du plugin de jeu pour éviter les
|
embarqués sur le serveur.
|
||||||
> conflits si plusieurs plugins shadent CR-Core sur le même 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
|
```java
|
||||||
public final class MyGamePlugin extends JavaPlugin {
|
public class MyGamePlugin extends JavaPlugin {
|
||||||
|
|
||||||
private TeamService teamService;
|
private CRCore core;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
// Instancie le service team (chaque plugin a son propre registre)
|
// 1 ligne = SQLite + services + /core team ... opérationnels
|
||||||
this.teamService = new TeamServiceImpl(new InMemoryTeamRepository());
|
this.core = new CRCore(this).enable();
|
||||||
|
|
||||||
// Enregistre une commande basée sur le framework de CR-Core
|
// Listener custom sur les events team
|
||||||
getCommand("team").setExecutor(new TeamCommand(teamService));
|
getServer().getPluginManager().registerEvents(new MyTeamListener(), this);
|
||||||
getCommand("team").setTabCompleter(new TeamCommand(teamService));
|
|
||||||
|
// 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
|
```java
|
||||||
public class TeamCommand extends BaseCommand {
|
public class MyTeamListener implements Listener {
|
||||||
|
|
||||||
public TeamCommand(TeamService service) {
|
@EventHandler
|
||||||
super("team", "teams", "t");
|
public void onTeamCreate(TeamCreateEvent event) {
|
||||||
description("Manage teams");
|
Team team = event.getTeam();
|
||||||
addSubCommand(new TeamCreateSub(service));
|
Bukkit.broadcastMessage("Nouvelle équipe : " + team.getName());
|
||||||
addSubCommand(new TeamInfoSub(service));
|
}
|
||||||
|
|
||||||
|
@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;
|
```java
|
||||||
|
public class MyCreateCommand extends TeamCreateSubCommand {
|
||||||
public TeamCreateSub(TeamService service) {
|
public MyCreateCommand(TeamService service) {
|
||||||
super("create", "c", "new");
|
super(service);
|
||||||
this.service = service;
|
permission("mygame.team.create"); // permission custom
|
||||||
description("Create a new team");
|
|
||||||
permission("mygame.team.create");
|
|
||||||
playerOnly();
|
|
||||||
argument("name", ArgumentTypes.STRING);
|
|
||||||
argument("tag", ArgumentTypes.STRING);
|
|
||||||
argument("color", ArgumentTypes.enumOf(TeamColor.class));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandResult execute(CommandContext ctx) {
|
public CommandResult execute(CommandContext ctx) {
|
||||||
Player player = ctx.requirePlayer();
|
// logique custom
|
||||||
String name = ctx.get("name");
|
return super.execute(ctx);
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dans onEnable() :
|
||||||
|
core.getCoreCommand().findSubCommand("team")
|
||||||
|
.ifPresent(team -> team.replaceSubCommand("create", new MyCreateCommand(core.getTeamService())));
|
||||||
```
|
```
|
||||||
|
|
||||||
Et dans le `plugin.yml` du plugin de jeu :
|
### Stocker / récupérer des données custom via SQLite
|
||||||
|
|
||||||
```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 :
|
|
||||||
|
|
||||||
```java
|
```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
|
int kills = db.queryOne(
|
||||||
protected void onAfterCreate(Team team) {
|
"SELECT kills FROM my_kills WHERE player_id = ?",
|
||||||
getPlugin().getLogger().info("Team created: " + team.getName());
|
rs -> rs.getInt("kills"),
|
||||||
}
|
playerUuid
|
||||||
|
).orElse(0);
|
||||||
@Override
|
|
||||||
protected Team newTeam(UUID id, String name, String tag, TeamColor color, UUID leaderId) {
|
|
||||||
return new MyCustomTeam(id, name, tag, color, leaderId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Arborescence du projet
|
## Arborescence du projet
|
||||||
@@ -146,61 +150,112 @@ public class LoggingTeamService extends TeamServiceImpl {
|
|||||||
CitesPlugin/ # dossier IntelliJ (renommer plus tard si voulu)
|
CitesPlugin/ # dossier IntelliJ (renommer plus tard si voulu)
|
||||||
├── pom.xml
|
├── pom.xml
|
||||||
├── GEMINI.md
|
├── GEMINI.md
|
||||||
├── docs/ # source de vérité
|
├── docs/
|
||||||
│ ├── README.md
|
│ ├── README.md
|
||||||
│ ├── setup.md
|
│ ├── setup.md
|
||||||
│ ├── features.md
|
│ ├── features.md
|
||||||
│ ├── decisions.md
|
│ ├── decisions.md
|
||||||
│ └── diagrams/
|
│ └── diagrams/*.puml
|
||||||
│ ├── team-class-diagram.puml
|
|
||||||
│ ├── team-create-sequence.puml
|
|
||||||
│ ├── team-create-activity.puml
|
|
||||||
│ └── command-class-diagram.puml
|
|
||||||
└── src/main/java/fr/luc/crcore/
|
└── src/main/java/fr/luc/crcore/
|
||||||
├── common/ # abstractions partagées
|
├── CRCore.java # bootstrap orchestrator
|
||||||
│ ├── Identifiable.java # interface
|
├── CRCoreConfig.java # config (sqlite, command name, …)
|
||||||
│ ├── Named.java # interface
|
├── common/
|
||||||
│ ├── ScoreHolder.java # interface (impl. par Team et PlayerProfile)
|
│ ├── Identifiable.java
|
||||||
│ ├── AbstractEntity.java # abstract class
|
│ ├── Named.java
|
||||||
│ └── Repository.java # interface
|
│ ├── ScoreHolder.java # contrat partagé Team + PlayerProfile
|
||||||
├── command/ # framework de commandes
|
│ ├── AbstractEntity.java
|
||||||
│ ├── Command.java # interface
|
│ └── Repository.java
|
||||||
│ ├── AbstractCommand.java # base partagée
|
├── database/ # wrapper SQLite
|
||||||
│ ├── BaseCommand.java # commande top-level (Bukkit-aware)
|
│ ├── Database.java
|
||||||
│ ├── SubCommand.java # sous-commande
|
│ ├── 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
|
│ ├── CommandContext.java
|
||||||
│ ├── CommandResult.java
|
│ ├── CommandResult.java
|
||||||
│ ├── CommandException.java
|
│ ├── CommandException.java
|
||||||
│ ├── ArgumentType.java
|
│ ├── ArgumentType.java
|
||||||
│ ├── ArgumentTypes.java # STRING, INTEGER, BOOLEAN, ONLINE_PLAYER, enumOf, choice
|
│ ├── ArgumentTypes.java
|
||||||
│ └── ArgumentDef.java # package-private
|
│ ├── ArgumentDef.java # package-private
|
||||||
├── team/ # domaine team
|
│ └── 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
|
│ ├── Team.java
|
||||||
│ ├── TeamMember.java
|
│ ├── TeamMember.java
|
||||||
│ ├── TeamRole.java # enum
|
│ ├── TeamRole.java
|
||||||
│ ├── TeamColor.java # enum
|
│ ├── TeamColor.java
|
||||||
│ ├── TeamVisibility.java # enum (PUBLIC / PRIVATE)
|
│ ├── TeamVisibility.java
|
||||||
│ ├── TeamRanking.java # record (rank, team, score)
|
│ ├── TeamRanking.java # record
|
||||||
│ ├── TeamRepository.java # interface
|
│ ├── TeamRepository.java
|
||||||
│ ├── InMemoryTeamRepository.java # impl
|
│ ├── InMemoryTeamRepository.java
|
||||||
│ ├── TeamService.java # interface
|
│ ├── SqliteTeamRepository.java
|
||||||
│ ├── TeamServiceImpl.java # impl avec hooks overridables
|
│ ├── TeamService.java
|
||||||
|
│ ├── TeamServiceImpl.java
|
||||||
|
│ ├── BukkitEventFiringTeamServiceImpl.java # impl par défaut
|
||||||
│ ├── TeamException.java
|
│ ├── TeamException.java
|
||||||
│ ├── TeamAlreadyExistsException.java
|
│ ├── TeamAlreadyExistsException.java
|
||||||
│ ├── TeamNotFoundException.java
|
│ ├── TeamNotFoundException.java
|
||||||
│ └── TeamAccessException.java # auto-join refusé
|
│ ├── TeamAccessException.java
|
||||||
└── player/ # domaine player
|
│ └── event/ # Bukkit events team
|
||||||
├── PlayerProfile.java # entité, scores par joueur
|
│ ├── TeamEvent.java # base
|
||||||
├── PlayerRanking.java # record (rank, profile, score)
|
│ ├── TeamCreateEvent.java
|
||||||
├── PlayerProfileRepository.java # interface
|
│ ├── 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
|
├── InMemoryPlayerProfileRepository.java
|
||||||
├── PlayerProfileService.java # interface
|
├── SqlitePlayerProfileRepository.java
|
||||||
├── PlayerProfileServiceImpl.java # impl avec hooks overridables
|
├── PlayerProfileService.java
|
||||||
|
├── PlayerProfileServiceImpl.java
|
||||||
|
├── BukkitEventFiringPlayerProfileServiceImpl.java
|
||||||
├── PlayerException.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
|
## Tables SQLite créées par CR-Core
|
||||||
> 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
|
Au premier `enable()`, les tables suivantes sont créées (en `IF NOT EXISTS`) :
|
||||||
> build.
|
|
||||||
|
- `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.
|
||||||
|
|||||||
@@ -10,12 +10,13 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>CR-Core</name>
|
<name>CR-Core</name>
|
||||||
<description>Reusable core library for CR Minecraft game plugins (teams, commands, common abstractions).</description>
|
<description>Reusable core library for CR Minecraft game plugins (teams, players, scores, commands, events, SQLite persistence).</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>16</maven.compiler.source>
|
<maven.compiler.source>16</maven.compiler.source>
|
||||||
<maven.compiler.target>16</maven.compiler.target>
|
<maven.compiler.target>16</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<sqlite.version>3.45.3.0</sqlite.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<repositories>
|
<repositories>
|
||||||
@@ -40,6 +41,18 @@
|
|||||||
<version>1.16.5-R0.1-SNAPSHOT</version>
|
<version>1.16.5-R0.1-SNAPSHOT</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!--
|
||||||
|
SQLite JDBC driver. Scope compile so consumer plugins that depend on
|
||||||
|
CR-Core get it transitively. They are expected to shade it into
|
||||||
|
their final jar (or declare the dependency themselves) since Paper
|
||||||
|
does not bundle it.
|
||||||
|
-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xerial</groupId>
|
||||||
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
|
<version>${sqlite.version}</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package fr.luc.crcore;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.builtin.CoreCommand;
|
||||||
|
import fr.luc.crcore.database.Database;
|
||||||
|
import fr.luc.crcore.player.BukkitEventFiringPlayerProfileServiceImpl;
|
||||||
|
import fr.luc.crcore.player.InMemoryPlayerProfileRepository;
|
||||||
|
import fr.luc.crcore.player.PlayerProfileRepository;
|
||||||
|
import fr.luc.crcore.player.PlayerProfileService;
|
||||||
|
import fr.luc.crcore.player.SqlitePlayerProfileRepository;
|
||||||
|
import fr.luc.crcore.team.BukkitEventFiringTeamServiceImpl;
|
||||||
|
import fr.luc.crcore.team.InMemoryTeamRepository;
|
||||||
|
import fr.luc.crcore.team.SqliteTeamRepository;
|
||||||
|
import fr.luc.crcore.team.TeamRepository;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.command.PluginCommand;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Point d'entrée unique de CR-Core pour un plugin de jeu downstream.
|
||||||
|
*
|
||||||
|
* <p>Instanciée une fois dans {@code onEnable()}, branche en cascade :
|
||||||
|
* <ol>
|
||||||
|
* <li>la base SQLite (dans le dataFolder du plugin),</li>
|
||||||
|
* <li>les repositories (SQLite ou in-memory selon {@link CRCoreConfig}),</li>
|
||||||
|
* <li>les services team + player avec fire d'évènements Bukkit,</li>
|
||||||
|
* <li>la commande {@code /core} avec tous ses sous-commandes par défaut.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <h2>Utilisation minimale côté plugin de jeu</h2>
|
||||||
|
* <pre>{@code
|
||||||
|
* public class MyGamePlugin extends JavaPlugin {
|
||||||
|
*
|
||||||
|
* private CRCore core;
|
||||||
|
*
|
||||||
|
* @Override
|
||||||
|
* public void onEnable() {
|
||||||
|
* this.core = new CRCore(this).enable();
|
||||||
|
* // /core team create/delete/add/remove/join/leave/... est prêt
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @Override
|
||||||
|
* public void onDisable() {
|
||||||
|
* if (core != null) core.disable();
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Le plugin de jeu doit avoir déclaré la commande dans son {@code plugin.yml} :
|
||||||
|
* <pre>{@code
|
||||||
|
* commands:
|
||||||
|
* core:
|
||||||
|
* description: Commandes CR-Core
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <h2>Override</h2>
|
||||||
|
* Tout est accessible via les getters : {@link #getTeamService()},
|
||||||
|
* {@link #getCoreCommand()}, {@link #getDatabase()}, etc. Pour remplacer une
|
||||||
|
* sous-commande, voir {@link CoreCommand}. Pour remplacer un service complet,
|
||||||
|
* sous-classer {@code CRCore} et override {@link #buildTeamService}.
|
||||||
|
*/
|
||||||
|
public class CRCore {
|
||||||
|
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
private final CRCoreConfig config;
|
||||||
|
|
||||||
|
private Database database;
|
||||||
|
private TeamRepository teamRepository;
|
||||||
|
private TeamService teamService;
|
||||||
|
private PlayerProfileRepository playerProfileRepository;
|
||||||
|
private PlayerProfileService playerProfileService;
|
||||||
|
private CoreCommand coreCommand;
|
||||||
|
private boolean enabled = false;
|
||||||
|
|
||||||
|
/** Construit CR-Core avec la config par défaut (SQLite activée, commande "core"). */
|
||||||
|
public CRCore(JavaPlugin plugin) {
|
||||||
|
this(plugin, new CRCoreConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CRCore(JavaPlugin plugin, CRCoreConfig config) {
|
||||||
|
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||||
|
this.config = Objects.requireNonNull(config, "config");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Branche tout : ouvre la DB, instancie les services, enregistre la
|
||||||
|
* commande. Idempotent : un second appel est no-op.
|
||||||
|
*
|
||||||
|
* @return {@code this} pour chaîner.
|
||||||
|
*/
|
||||||
|
public CRCore enable() {
|
||||||
|
if (enabled) return this;
|
||||||
|
if (config.isSqliteEnabled()) {
|
||||||
|
File dbFile = new File(plugin.getDataFolder(), config.getSqliteFilename());
|
||||||
|
if (!dbFile.getParentFile().exists() && !dbFile.getParentFile().mkdirs()) {
|
||||||
|
plugin.getLogger().warning("Impossible de créer le dataFolder : " + dbFile.getParentFile());
|
||||||
|
}
|
||||||
|
this.database = new Database(dbFile);
|
||||||
|
this.teamRepository = new SqliteTeamRepository(database);
|
||||||
|
this.playerProfileRepository = new SqlitePlayerProfileRepository(database);
|
||||||
|
} else {
|
||||||
|
this.teamRepository = new InMemoryTeamRepository();
|
||||||
|
this.playerProfileRepository = new InMemoryPlayerProfileRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.teamService = buildTeamService(teamRepository);
|
||||||
|
this.playerProfileService = buildPlayerProfileService(playerProfileRepository);
|
||||||
|
|
||||||
|
this.coreCommand = buildCoreCommand(teamService, playerProfileService);
|
||||||
|
registerCommand();
|
||||||
|
|
||||||
|
plugin.getLogger().info("CR-Core activé.");
|
||||||
|
enabled = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Libère les ressources (ferme la DB notamment). Idempotent. */
|
||||||
|
public void disable() {
|
||||||
|
if (!enabled) return;
|
||||||
|
if (database != null) {
|
||||||
|
try {
|
||||||
|
database.close();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
plugin.getLogger().warning("Erreur en fermant la DB : " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.getLogger().info("CR-Core désactivé.");
|
||||||
|
enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Override points ----
|
||||||
|
|
||||||
|
/** Construit le {@link TeamService}. Override pour utiliser une impl custom. */
|
||||||
|
protected TeamService buildTeamService(TeamRepository repository) {
|
||||||
|
return new BukkitEventFiringTeamServiceImpl(plugin, repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Construit le {@link PlayerProfileService}. Override pour une impl custom. */
|
||||||
|
protected PlayerProfileService buildPlayerProfileService(PlayerProfileRepository repository) {
|
||||||
|
return new BukkitEventFiringPlayerProfileServiceImpl(plugin, repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Construit le {@link CoreCommand}. Override pour ajouter des groupes top-level. */
|
||||||
|
protected CoreCommand buildCoreCommand(TeamService teamService, PlayerProfileService playerProfileService) {
|
||||||
|
return new CoreCommand(teamService, playerProfileService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerCommand() {
|
||||||
|
PluginCommand cmd = plugin.getCommand(config.getCommandName());
|
||||||
|
if (cmd == null) {
|
||||||
|
plugin.getLogger().warning("Commande '" + config.getCommandName() +
|
||||||
|
"' absente du plugin.yml — /" + config.getCommandName() +
|
||||||
|
" ne sera pas reconnue.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cmd.setExecutor(coreCommand);
|
||||||
|
cmd.setTabCompleter(coreCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Getters ----
|
||||||
|
|
||||||
|
public JavaPlugin getPlugin() { return plugin; }
|
||||||
|
public CRCoreConfig getConfig() { return config; }
|
||||||
|
public Database getDatabase() { return database; }
|
||||||
|
public TeamRepository getTeamRepository() { return teamRepository; }
|
||||||
|
public TeamService getTeamService() { return teamService; }
|
||||||
|
public PlayerProfileRepository getPlayerProfileRepository() { return playerProfileRepository; }
|
||||||
|
public PlayerProfileService getPlayerProfileService() { return playerProfileService; }
|
||||||
|
public CoreCommand getCoreCommand() { return coreCommand; }
|
||||||
|
public boolean isEnabled() { return enabled; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package fr.luc.crcore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration de {@link CRCore} fournie au constructeur. API builder : on
|
||||||
|
* chaîne les {@code with...} pour modifier les valeurs par défaut.
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* new CRCore(this, new CRCoreConfig()
|
||||||
|
* .withSqliteFile("mydata.db")
|
||||||
|
* .withCommandName("game"))
|
||||||
|
* .enable();
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Valeurs par défaut :
|
||||||
|
* <ul>
|
||||||
|
* <li>SQLite activé, fichier {@code crcore.db} dans le dataFolder du plugin</li>
|
||||||
|
* <li>Commande Bukkit racine : {@code core}</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class CRCoreConfig {
|
||||||
|
|
||||||
|
private boolean sqliteEnabled = true;
|
||||||
|
private String sqliteFilename = "crcore.db";
|
||||||
|
private String commandName = "core";
|
||||||
|
|
||||||
|
/** Désactive SQLite — toutes les données vivent en mémoire (perdues au reload/stop). */
|
||||||
|
public CRCoreConfig withInMemoryStorage() {
|
||||||
|
this.sqliteEnabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Active SQLite et fixe le nom du fichier (relatif au dataFolder du plugin). */
|
||||||
|
public CRCoreConfig withSqliteFile(String filename) {
|
||||||
|
this.sqliteEnabled = true;
|
||||||
|
this.sqliteFilename = filename;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change le nom de la commande Bukkit racine. Doit matcher l'entrée du
|
||||||
|
* {@code commands:} dans le {@code plugin.yml} du plugin de jeu.
|
||||||
|
*/
|
||||||
|
public CRCoreConfig withCommandName(String commandName) {
|
||||||
|
this.commandName = commandName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSqliteEnabled() { return sqliteEnabled; }
|
||||||
|
public String getSqliteFilename() { return sqliteFilename; }
|
||||||
|
public String getCommandName() { return commandName; }
|
||||||
|
}
|
||||||
@@ -1,19 +1,49 @@
|
|||||||
package fr.luc.crcore.command;
|
package fr.luc.crcore.command;
|
||||||
|
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base partagée par {@link BaseCommand} (top-level Bukkit) et {@link SubCommand}
|
||||||
|
* (feuille ou groupe imbriqué). Porte tous les champs communs : nom, aliases,
|
||||||
|
* permission, player-only, description, usage, arguments typés, et un registre
|
||||||
|
* de sous-commandes imbriquées.
|
||||||
|
*
|
||||||
|
* <p>Une commande peut être :
|
||||||
|
* <ul>
|
||||||
|
* <li><b>feuille</b> : pas de sous-commandes, implémente {@link #execute}</li>
|
||||||
|
* <li><b>groupe</b> : sous-commandes via {@link #addSubCommand}, le routage
|
||||||
|
* est récursif. Si aucune sous-commande ne matche, {@link #execute} est
|
||||||
|
* appelé en fallback (par défaut : liste les sous-commandes).</li>
|
||||||
|
* <li><b>hybride</b> : groupe ET arguments propres — déconseillé, le
|
||||||
|
* routage donne priorité aux sous-commandes.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>L'override par les plugins de jeu se fait via {@link #replaceSubCommand}
|
||||||
|
* (remplacer une sous-commande par nom) ou en sous-classant et en redéfinissant
|
||||||
|
* {@link #execute}.
|
||||||
|
*/
|
||||||
public abstract class AbstractCommand implements Command {
|
public abstract class AbstractCommand implements Command {
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
private final List<String> aliases = new ArrayList<>();
|
private final List<String> aliases = new ArrayList<>();
|
||||||
private final List<ArgumentDef> arguments = new ArrayList<>();
|
private final List<ArgumentDef> arguments = new ArrayList<>();
|
||||||
|
private final Map<String, SubCommand> subCommandsByName = new LinkedHashMap<>();
|
||||||
|
private final Map<String, SubCommand> subCommandsByAlias = new HashMap<>();
|
||||||
|
|
||||||
private String permission;
|
private String permission;
|
||||||
private boolean playerOnly;
|
private boolean playerOnly;
|
||||||
private String description = "";
|
private String description = "";
|
||||||
@@ -26,65 +56,126 @@ public abstract class AbstractCommand implements Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
// ---- Command interface ----
|
||||||
public final String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override public final String getName() { return name; }
|
||||||
public final List<String> getAliases() {
|
@Override public final List<String> getAliases() { return Collections.unmodifiableList(aliases); }
|
||||||
return Collections.unmodifiableList(aliases);
|
@Override public final String getPermission() { return permission; }
|
||||||
}
|
@Override public final boolean isPlayerOnly() { return playerOnly; }
|
||||||
|
@Override public final String getDescription() { return description; }
|
||||||
@Override
|
|
||||||
public final String getPermission() {
|
|
||||||
return permission;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final boolean isPlayerOnly() {
|
|
||||||
return playerOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/** Usage explicite si défini via {@link #usage(String)}, sinon usage auto-construit. */
|
||||||
public final String getUsage() {
|
public final String getUsage() {
|
||||||
return usage != null ? usage : buildDefaultUsage();
|
return usage != null ? usage : buildDefaultUsage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Builders (à appeler dans le constructeur des sous-classes) ----
|
||||||
|
|
||||||
|
/** Ajoute un ou plusieurs alias. Les aliases sont case-insensitive. */
|
||||||
protected final void addAlias(String... aliases) {
|
protected final void addAlias(String... aliases) {
|
||||||
for (String alias : aliases) {
|
for (String alias : aliases) {
|
||||||
this.aliases.add(alias.toLowerCase());
|
this.aliases.add(alias.toLowerCase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Définit la permission Bukkit requise (ex. {@code "crcore.team.create"}). */
|
||||||
protected final void permission(String permission) {
|
protected final void permission(String permission) {
|
||||||
this.permission = permission;
|
this.permission = permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Restreint l'exécution aux joueurs (refus pour console). */
|
||||||
protected final void playerOnly() {
|
protected final void playerOnly() {
|
||||||
this.playerOnly = true;
|
this.playerOnly = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Description courte affichée dans l'aide. */
|
||||||
protected final void description(String description) {
|
protected final void description(String description) {
|
||||||
this.description = Objects.requireNonNullElse(description, "");
|
this.description = Objects.requireNonNullElse(description, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Usage explicite (sinon construit automatiquement à partir des arguments). */
|
||||||
protected final void usage(String usage) {
|
protected final void usage(String usage) {
|
||||||
this.usage = usage;
|
this.usage = usage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Déclare un argument positionnel obligatoire. */
|
||||||
protected final void argument(String name, ArgumentType<?> type) {
|
protected final void argument(String name, ArgumentType<?> type) {
|
||||||
arguments.add(new ArgumentDef(name, type, true));
|
arguments.add(new ArgumentDef(name, type, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Déclare un argument positionnel optionnel (peut être omis par l'utilisateur). */
|
||||||
protected final void optionalArgument(String name, ArgumentType<?> type) {
|
protected final void optionalArgument(String name, ArgumentType<?> type) {
|
||||||
arguments.add(new ArgumentDef(name, type, false));
|
arguments.add(new ArgumentDef(name, type, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Sub-command management ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre une sous-commande. Lève {@link IllegalStateException} si une
|
||||||
|
* sous-commande du même nom existe déjà (utiliser {@link #replaceSubCommand}
|
||||||
|
* pour overrider).
|
||||||
|
*/
|
||||||
|
protected final void addSubCommand(SubCommand sub) {
|
||||||
|
Objects.requireNonNull(sub, "sub");
|
||||||
|
if (subCommandsByName.containsKey(sub.getName())) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Sub-command '" + sub.getName() + "' already registered on '" + name + "'");
|
||||||
|
}
|
||||||
|
subCommandsByName.put(sub.getName(), sub);
|
||||||
|
for (String alias : sub.getAliases()) {
|
||||||
|
subCommandsByAlias.put(alias, sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remplace une sous-commande existante par son nom. Utilisé par les plugins
|
||||||
|
* de jeu pour overrider un comportement par défaut.
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* coreCommand.findSubCommand("team")
|
||||||
|
* .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc)));
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @return l'ancienne sous-commande remplacée, ou {@link Optional#empty()}
|
||||||
|
* si aucune n'existait sous ce nom.
|
||||||
|
*/
|
||||||
|
public final Optional<SubCommand> replaceSubCommand(String name, SubCommand replacement) {
|
||||||
|
Objects.requireNonNull(name, "name");
|
||||||
|
Objects.requireNonNull(replacement, "replacement");
|
||||||
|
String key = name.toLowerCase();
|
||||||
|
SubCommand old = subCommandsByName.remove(key);
|
||||||
|
if (old != null) {
|
||||||
|
for (String alias : old.getAliases()) {
|
||||||
|
subCommandsByAlias.remove(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subCommandsByName.put(replacement.getName(), replacement);
|
||||||
|
for (String alias : replacement.getAliases()) {
|
||||||
|
subCommandsByAlias.put(alias, replacement);
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(old);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recherche une sous-commande par nom ou par alias (case-insensitive). */
|
||||||
|
public final Optional<SubCommand> findSubCommand(String label) {
|
||||||
|
if (label == null) return Optional.empty();
|
||||||
|
String lc = label.toLowerCase();
|
||||||
|
SubCommand sub = subCommandsByName.get(lc);
|
||||||
|
if (sub != null) return Optional.of(sub);
|
||||||
|
return Optional.ofNullable(subCommandsByAlias.get(lc));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toutes les sous-commandes enregistrées, dans l'ordre d'insertion. */
|
||||||
|
public final Collection<SubCommand> getSubCommands() {
|
||||||
|
return Collections.unmodifiableCollection(subCommandsByName.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public final boolean hasSubCommands() {
|
||||||
|
return !subCommandsByName.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Argument introspection ----
|
||||||
|
|
||||||
public final int getRequiredArgumentCount() {
|
public final int getRequiredArgumentCount() {
|
||||||
return (int) arguments.stream().filter(ArgumentDef::isRequired).count();
|
return (int) arguments.stream().filter(ArgumentDef::isRequired).count();
|
||||||
}
|
}
|
||||||
@@ -97,6 +188,129 @@ public abstract class AbstractCommand implements Command {
|
|||||||
return arguments;
|
return arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Dispatch (routage récursif vers sous-commandes) ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Achemine l'exécution : si {@code args[0]} matche une sous-commande, on
|
||||||
|
* recurse dessus avec {@code args[1..]}. Sinon on appelle {@link #execute}
|
||||||
|
* de cette commande. Gère les checks permission / player-only.
|
||||||
|
*/
|
||||||
|
public final CommandResult dispatch(CommandSender sender, String label, String[] args) {
|
||||||
|
if (!checkAccess(sender)) {
|
||||||
|
return permission != null && !sender.hasPermission(permission)
|
||||||
|
? CommandResult.noPermission()
|
||||||
|
: CommandResult.playerOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length > 0) {
|
||||||
|
Optional<SubCommand> sub = findSubCommand(args[0]);
|
||||||
|
if (sub.isPresent()) {
|
||||||
|
String[] subArgs = Arrays.copyOfRange(args, 1, args.length);
|
||||||
|
return sub.get().dispatch(sender, label, subArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length < getRequiredArgumentCount()) {
|
||||||
|
return CommandResult.invalidUsage("Usage: " + buildDefaultUsage());
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandContext ctx;
|
||||||
|
try {
|
||||||
|
ctx = buildContext(sender, label, args);
|
||||||
|
} catch (CommandException ex) {
|
||||||
|
return CommandResult.failure(ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return execute(ctx);
|
||||||
|
} catch (CommandException ex) {
|
||||||
|
return CommandResult.failure(ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-complétion récursive : si {@code args} a une seule valeur, suggère
|
||||||
|
* les sous-commandes accessibles + les arguments propres. Sinon recurse
|
||||||
|
* vers la sous-commande qui matche.
|
||||||
|
*/
|
||||||
|
public final List<String> tabComplete(CommandSender sender, String[] args) {
|
||||||
|
if (args.length == 0) return Collections.emptyList();
|
||||||
|
|
||||||
|
if (args.length == 1) {
|
||||||
|
String partial = args[0].toLowerCase();
|
||||||
|
List<String> suggestions = new ArrayList<>();
|
||||||
|
// Sous-commandes (filtrées par permission)
|
||||||
|
for (SubCommand sub : subCommandsByName.values()) {
|
||||||
|
if (sub.getPermission() != null && !sender.hasPermission(sub.getPermission())) continue;
|
||||||
|
suggestions.add(sub.getName());
|
||||||
|
suggestions.addAll(sub.getAliases());
|
||||||
|
}
|
||||||
|
// Argument 0 si on est une feuille avec arguments
|
||||||
|
if (!arguments.isEmpty()) {
|
||||||
|
suggestions.addAll(arguments.get(0).getType().suggestions(sender, args[0]));
|
||||||
|
}
|
||||||
|
return suggestions.stream()
|
||||||
|
.filter(s -> s.toLowerCase().startsWith(partial))
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// args.length > 1 : route vers sous-commande si match
|
||||||
|
Optional<SubCommand> sub = findSubCommand(args[0]);
|
||||||
|
if (sub.isPresent()) {
|
||||||
|
if (sub.get().getPermission() != null && !sender.hasPermission(sub.get().getPermission())) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
String[] subArgs = Arrays.copyOfRange(args, 1, args.length);
|
||||||
|
return sub.get().tabComplete(sender, subArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pas de sous-commande : complète l'argument courant
|
||||||
|
int argIndex = args.length - 1;
|
||||||
|
if (argIndex < arguments.size()) {
|
||||||
|
return arguments.get(argIndex).getType().suggestions(sender, args[argIndex]);
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Execution (à overrider par les feuilles) ----
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logique métier de la commande. Override par les sous-classes.
|
||||||
|
*
|
||||||
|
* <p>Comportement par défaut :
|
||||||
|
* <ul>
|
||||||
|
* <li>Si cette commande a des sous-commandes → affiche la liste (aide).</li>
|
||||||
|
* <li>Sinon → renvoie {@link CommandResult#invalidUsage()}.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public CommandResult execute(CommandContext context) {
|
||||||
|
if (hasSubCommands()) {
|
||||||
|
return listSubCommands(context);
|
||||||
|
}
|
||||||
|
return CommandResult.invalidUsage("Usage: " + buildDefaultUsage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Override helpers ----
|
||||||
|
|
||||||
|
/** Default {@code execute} pour les groupes : affiche la liste des sous-commandes accessibles. */
|
||||||
|
protected CommandResult listSubCommands(CommandContext context) {
|
||||||
|
StringBuilder sb = new StringBuilder(ChatColor.YELLOW.toString())
|
||||||
|
.append("Commandes disponibles pour ")
|
||||||
|
.append(ChatColor.WHITE).append('/').append(context.getLabel());
|
||||||
|
for (SubCommand sub : subCommandsByName.values()) {
|
||||||
|
if (sub.getPermission() != null && !context.getSender().hasPermission(sub.getPermission())) continue;
|
||||||
|
sb.append('\n').append(ChatColor.GRAY).append(" - ")
|
||||||
|
.append(ChatColor.WHITE).append(sub.getName());
|
||||||
|
if (!sub.getDescription().isEmpty()) {
|
||||||
|
sb.append(ChatColor.GRAY).append(" — ").append(sub.getDescription());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.reply(sb.toString());
|
||||||
|
return CommandResult.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Construit un usage par défaut à partir du nom et des arguments déclarés. */
|
||||||
protected String buildDefaultUsage() {
|
protected String buildDefaultUsage() {
|
||||||
StringBuilder sb = new StringBuilder("/").append(name);
|
StringBuilder sb = new StringBuilder("/").append(name);
|
||||||
for (ArgumentDef def : arguments) {
|
for (ArgumentDef def : arguments) {
|
||||||
@@ -108,26 +322,26 @@ public abstract class AbstractCommand implements Command {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/** Check standard de permission + player-only. */
|
||||||
public List<String> tabComplete(CommandSender sender, int argIndex, String partial) {
|
protected boolean checkAccess(CommandSender sender) {
|
||||||
if (argIndex >= 0 && argIndex < arguments.size()) {
|
if (permission != null && !sender.hasPermission(permission)) return false;
|
||||||
return arguments.get(argIndex).getType().suggestions(sender, partial);
|
if (playerOnly && !(sender instanceof Player)) return false;
|
||||||
}
|
return true;
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected CommandContext buildContext(CommandSender sender, String label, String[] subArgs) {
|
/** Parse les arguments raw et construit le {@link CommandContext}. */
|
||||||
|
protected CommandContext buildContext(CommandSender sender, String label, String[] rawArgs) {
|
||||||
Map<String, Object> parsed = new LinkedHashMap<>();
|
Map<String, Object> parsed = new LinkedHashMap<>();
|
||||||
int max = Math.min(subArgs.length, arguments.size());
|
int max = Math.min(rawArgs.length, arguments.size());
|
||||||
for (int i = 0; i < max; i++) {
|
for (int i = 0; i < max; i++) {
|
||||||
ArgumentDef def = arguments.get(i);
|
ArgumentDef def = arguments.get(i);
|
||||||
try {
|
try {
|
||||||
Object value = def.getType().parse(subArgs[i]);
|
Object value = def.getType().parse(rawArgs[i]);
|
||||||
parsed.put(def.getName(), value);
|
parsed.put(def.getName(), value);
|
||||||
} catch (CommandException ex) {
|
} catch (CommandException ex) {
|
||||||
throw new CommandException("Argument '" + def.getName() + "': " + ex.getMessage());
|
throw new CommandException("Argument '" + def.getName() + "': " + ex.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new CommandContext(sender, label, subArgs, parsed);
|
return new CommandContext(sender, label, rawArgs, parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,172 +4,48 @@ import org.bukkit.ChatColor;
|
|||||||
import org.bukkit.command.CommandExecutor;
|
import org.bukkit.command.CommandExecutor;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.command.TabCompleter;
|
import org.bukkit.command.TabCompleter;
|
||||||
import org.bukkit.entity.Player;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commande top-level branchée sur Bukkit. À utiliser comme racine de l'arbre :
|
||||||
|
* <pre>{@code
|
||||||
|
* PluginCommand cmd = plugin.getCommand("core");
|
||||||
|
* cmd.setExecutor(new CoreCommand(...));
|
||||||
|
* cmd.setTabCompleter((CoreCommand) cmd.getExecutor());
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>{@code BaseCommand} se contente de relayer {@code onCommand} et
|
||||||
|
* {@code onTabComplete} vers {@link AbstractCommand#dispatch} et
|
||||||
|
* {@link AbstractCommand#tabComplete}. Toute la logique (routage récursif,
|
||||||
|
* permissions, player-only, parsing d'arguments) vit dans
|
||||||
|
* {@link AbstractCommand}.
|
||||||
|
*/
|
||||||
public abstract class BaseCommand extends AbstractCommand
|
public abstract class BaseCommand extends AbstractCommand
|
||||||
implements CommandExecutor, TabCompleter {
|
implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
private final Map<String, SubCommand> subCommandsByName = new LinkedHashMap<>();
|
|
||||||
private final Map<String, SubCommand> subCommandsByAlias = new HashMap<>();
|
|
||||||
|
|
||||||
protected BaseCommand(String name, String... aliases) {
|
protected BaseCommand(String name, String... aliases) {
|
||||||
super(name, aliases);
|
super(name, aliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final void addSubCommand(SubCommand sub) {
|
|
||||||
Objects.requireNonNull(sub, "sub");
|
|
||||||
if (subCommandsByName.containsKey(sub.getName())) {
|
|
||||||
throw new IllegalStateException("Sub-command already registered: " + sub.getName());
|
|
||||||
}
|
|
||||||
subCommandsByName.put(sub.getName(), sub);
|
|
||||||
for (String alias : sub.getAliases()) {
|
|
||||||
subCommandsByAlias.put(alias, sub);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Collection<SubCommand> getSubCommands() {
|
|
||||||
return Collections.unmodifiableCollection(subCommandsByName.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
public final Optional<SubCommand> findSubCommand(String label) {
|
|
||||||
if (label == null) return Optional.empty();
|
|
||||||
String lc = label.toLowerCase();
|
|
||||||
SubCommand sub = subCommandsByName.get(lc);
|
|
||||||
if (sub != null) return Optional.of(sub);
|
|
||||||
return Optional.ofNullable(subCommandsByAlias.get(lc));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when no sub-command matches the first argument. Override for custom
|
|
||||||
* fallback behavior. Default: lists available sub-commands.
|
|
||||||
*/
|
|
||||||
protected CommandResult execute(CommandContext context) {
|
|
||||||
if (subCommandsByName.isEmpty()) {
|
|
||||||
return CommandResult.invalidUsage("No action specified.");
|
|
||||||
}
|
|
||||||
StringBuilder sb = new StringBuilder(ChatColor.YELLOW + "Available sub-commands:");
|
|
||||||
for (SubCommand sub : subCommandsByName.values()) {
|
|
||||||
sb.append('\n').append(ChatColor.GRAY).append(" - ")
|
|
||||||
.append(ChatColor.WHITE).append(sub.getName());
|
|
||||||
if (!sub.getDescription().isEmpty()) {
|
|
||||||
sb.append(ChatColor.GRAY).append(" — ").append(sub.getDescription());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.reply(sb.toString());
|
|
||||||
return CommandResult.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final boolean onCommand(CommandSender sender, org.bukkit.command.Command command,
|
public final boolean onCommand(CommandSender sender, org.bukkit.command.Command command,
|
||||||
String label, String[] args) {
|
String label, String[] args) {
|
||||||
if (!checkAccess(sender, this)) {
|
CommandResult result = dispatch(sender, label, args);
|
||||||
return true;
|
handleResult(sender, result);
|
||||||
}
|
|
||||||
|
|
||||||
if (args.length == 0 || findSubCommand(args[0]).isEmpty()) {
|
|
||||||
CommandContext ctx = new CommandContext(sender, label, args, Collections.emptyMap());
|
|
||||||
try {
|
|
||||||
handleResult(sender, execute(ctx));
|
|
||||||
} catch (CommandException ex) {
|
|
||||||
sender.sendMessage(ChatColor.RED + ex.getMessage());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
SubCommand sub = findSubCommand(args[0]).orElseThrow();
|
|
||||||
if (!checkAccess(sender, sub)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] subArgs = Arrays.copyOfRange(args, 1, args.length);
|
|
||||||
if (subArgs.length < sub.getRequiredArgumentCount()) {
|
|
||||||
sender.sendMessage(ChatColor.RED + "Usage: " + buildUsageFor(label, sub));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandContext ctx;
|
|
||||||
try {
|
|
||||||
ctx = sub.buildContext(sender, label, subArgs);
|
|
||||||
} catch (CommandException ex) {
|
|
||||||
sender.sendMessage(ChatColor.RED + ex.getMessage());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
handleResult(sender, sub.execute(ctx));
|
|
||||||
} catch (CommandException ex) {
|
|
||||||
sender.sendMessage(ChatColor.RED + ex.getMessage());
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final List<String> onTabComplete(CommandSender sender, org.bukkit.command.Command command,
|
public final List<String> onTabComplete(CommandSender sender, org.bukkit.command.Command command,
|
||||||
String alias, String[] args) {
|
String alias, String[] args) {
|
||||||
if (args.length == 0) return Collections.emptyList();
|
return tabComplete(sender, args);
|
||||||
|
|
||||||
if (args.length == 1) {
|
|
||||||
String partial = args[0].toLowerCase();
|
|
||||||
List<String> names = new ArrayList<>();
|
|
||||||
for (SubCommand sub : subCommandsByName.values()) {
|
|
||||||
if (sub.getPermission() != null && !sender.hasPermission(sub.getPermission())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
names.add(sub.getName());
|
|
||||||
names.addAll(sub.getAliases());
|
|
||||||
}
|
|
||||||
return names.stream()
|
|
||||||
.filter(n -> n.startsWith(partial))
|
|
||||||
.distinct()
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<SubCommand> subOpt = findSubCommand(args[0]);
|
|
||||||
if (subOpt.isEmpty()) return Collections.emptyList();
|
|
||||||
SubCommand sub = subOpt.get();
|
|
||||||
if (sub.getPermission() != null && !sender.hasPermission(sub.getPermission())) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
int argIndex = args.length - 2;
|
|
||||||
String partial = args[args.length - 1];
|
|
||||||
return sub.tabComplete(sender, argIndex, partial);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean checkAccess(CommandSender sender, Command target) {
|
|
||||||
if (target.getPermission() != null && !sender.hasPermission(target.getPermission())) {
|
|
||||||
sender.sendMessage(ChatColor.RED + "You don't have permission.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (target.isPlayerOnly() && !(sender instanceof Player)) {
|
|
||||||
sender.sendMessage(ChatColor.RED + "Only players can use this command.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String buildUsageFor(String label, SubCommand sub) {
|
|
||||||
StringBuilder sb = new StringBuilder("/").append(label).append(' ').append(sub.getName());
|
|
||||||
for (ArgumentDef def : sub.getArgumentDefs()) {
|
|
||||||
sb.append(' ');
|
|
||||||
sb.append(def.isRequired() ? '<' : '[');
|
|
||||||
sb.append(def.getName());
|
|
||||||
sb.append(def.isRequired() ? '>' : ']');
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche le {@link CommandResult} à l'utilisateur. Override pour
|
||||||
|
* personnaliser le formatage (couleurs, locales, etc.).
|
||||||
|
*/
|
||||||
protected void handleResult(CommandSender sender, CommandResult result) {
|
protected void handleResult(CommandSender sender, CommandResult result) {
|
||||||
switch (result.getType()) {
|
switch (result.getType()) {
|
||||||
case SUCCESS -> {
|
case SUCCESS -> {
|
||||||
@@ -181,8 +57,8 @@ public abstract class BaseCommand extends AbstractCommand
|
|||||||
(result.getMessage() != null ? result.getMessage() : "Command failed."));
|
(result.getMessage() != null ? result.getMessage() : "Command failed."));
|
||||||
case INVALID_USAGE -> sender.sendMessage(ChatColor.RED +
|
case INVALID_USAGE -> sender.sendMessage(ChatColor.RED +
|
||||||
(result.getMessage() != null ? result.getMessage() : "Invalid usage."));
|
(result.getMessage() != null ? result.getMessage() : "Invalid usage."));
|
||||||
case NO_PERMISSION -> sender.sendMessage(ChatColor.RED + "You don't have permission.");
|
case NO_PERMISSION -> sender.sendMessage(ChatColor.RED + "Vous n'avez pas la permission.");
|
||||||
case PLAYER_ONLY -> sender.sendMessage(ChatColor.RED + "Only players can use this command.");
|
case PLAYER_ONLY -> sender.sendMessage(ChatColor.RED + "Seul un joueur peut utiliser cette commande.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import org.bukkit.command.CommandSender;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrat partagé par toutes les commandes du framework CR-Core
|
||||||
|
* ({@link BaseCommand} top-level et {@link SubCommand} imbriquées). Implémenté
|
||||||
|
* concrètement par {@link AbstractCommand}.
|
||||||
|
*/
|
||||||
public interface Command {
|
public interface Command {
|
||||||
|
|
||||||
String getName();
|
String getName();
|
||||||
@@ -16,10 +21,20 @@ public interface Command {
|
|||||||
|
|
||||||
String getDescription();
|
String getDescription();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logique d'exécution de la commande (cas feuille, ou fallback si aucune
|
||||||
|
* sous-commande ne matche).
|
||||||
|
*/
|
||||||
CommandResult execute(CommandContext context);
|
CommandResult execute(CommandContext context);
|
||||||
|
|
||||||
List<String> tabComplete(CommandSender sender, int argIndex, String partial);
|
/**
|
||||||
|
* Suggestions de tab-completion en fonction des arguments déjà tapés.
|
||||||
|
* {@code args} contient TOUS les arguments depuis ce niveau de commande
|
||||||
|
* (sans le nom de la commande elle-même).
|
||||||
|
*/
|
||||||
|
List<String> tabComplete(CommandSender sender, String[] args);
|
||||||
|
|
||||||
|
/** {@code true} si {@code label} match le nom ou un alias (case-insensitive). */
|
||||||
default boolean matches(String label) {
|
default boolean matches(String label) {
|
||||||
if (label == null) return false;
|
if (label == null) return false;
|
||||||
String lc = label.toLowerCase();
|
String lc = label.toLowerCase();
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
package fr.luc.crcore.command;
|
package fr.luc.crcore.command;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sous-commande imbriquée. Peut être :
|
||||||
|
* <ul>
|
||||||
|
* <li><b>feuille</b> — override {@link #execute(CommandContext)} avec la logique métier</li>
|
||||||
|
* <li><b>groupe</b> — appelle {@code addSubCommand(...)} dans son constructeur pour
|
||||||
|
* déléguer à des sous-sous-commandes (ex. {@code /core team create})</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Toute la machinerie de routage / parsing / tab-complete est héritée de
|
||||||
|
* {@link AbstractCommand}.
|
||||||
|
*/
|
||||||
public abstract class SubCommand extends AbstractCommand {
|
public abstract class SubCommand extends AbstractCommand {
|
||||||
|
|
||||||
protected SubCommand(String name, String... aliases) {
|
protected SubCommand(String name, String... aliases) {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package fr.luc.crcore.command.builtin;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.BaseCommand;
|
||||||
|
import fr.luc.crcore.command.builtin.team.TeamGroupSubCommand;
|
||||||
|
import fr.luc.crcore.player.PlayerProfileService;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commande racine {@code /core}. Container des groupes par défaut.
|
||||||
|
*
|
||||||
|
* <p>Branchée par {@code CRCore.enable()} sur la {@code PluginCommand "core"}
|
||||||
|
* du plugin de jeu (qui doit l'avoir déclarée dans son {@code plugin.yml}).
|
||||||
|
*
|
||||||
|
* <p>Sans arguments, affiche l'aide des groupes disponibles. Avec {@code team
|
||||||
|
* <action>}, route vers {@link TeamGroupSubCommand}.
|
||||||
|
*
|
||||||
|
* <h2>Override</h2>
|
||||||
|
* Pour remplacer un groupe entier :
|
||||||
|
* <pre>{@code
|
||||||
|
* core.getCoreCommand().replaceSubCommand("team", new MyTeamGroup(svc));
|
||||||
|
* }</pre>
|
||||||
|
* Pour remplacer une feuille :
|
||||||
|
* <pre>{@code
|
||||||
|
* core.getCoreCommand().findSubCommand("team")
|
||||||
|
* .ifPresent(t -> t.replaceSubCommand("create", new MyCreate(svc)));
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public class CoreCommand extends BaseCommand {
|
||||||
|
|
||||||
|
protected final TeamService teamService;
|
||||||
|
protected final PlayerProfileService playerProfileService;
|
||||||
|
|
||||||
|
public CoreCommand(TeamService teamService, PlayerProfileService playerProfileService) {
|
||||||
|
super("core", "cr", "crcore");
|
||||||
|
this.teamService = Objects.requireNonNull(teamService, "teamService");
|
||||||
|
this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService");
|
||||||
|
description("Commandes du noyau CR-Core");
|
||||||
|
registerDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Enregistre les groupes par défaut. Override pour ajouter / retirer des groupes. */
|
||||||
|
protected void registerDefaults() {
|
||||||
|
addSubCommand(new TeamGroupSubCommand(teamService));
|
||||||
|
// Futur : addSubCommand(new PlayerGroupSubCommand(playerProfileService));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.ArgumentTypes;
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team add <player>}
|
||||||
|
*
|
||||||
|
* <p>Le chef ajoute un joueur à son équipe. Marche que la team soit PUBLIC ou
|
||||||
|
* PRIVATE — c'est une action chef, pas un auto-join.
|
||||||
|
*/
|
||||||
|
public class TeamAddSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamAddSubCommand(TeamService service) {
|
||||||
|
super("add", "invite");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Ajouter un joueur à son équipe (chef uniquement)");
|
||||||
|
playerOnly();
|
||||||
|
argument("player", ArgumentTypes.ONLINE_PLAYER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Player executor = ctx.requirePlayer();
|
||||||
|
Player target = ctx.get("player");
|
||||||
|
|
||||||
|
Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
|
||||||
|
if (team == null) {
|
||||||
|
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||||
|
}
|
||||||
|
if (!team.getLeaderId().equals(executor.getUniqueId())) {
|
||||||
|
return CommandResult.failure("Seul le chef peut ajouter des membres.");
|
||||||
|
}
|
||||||
|
if (service.getTeamOfPlayer(target.getUniqueId()).isPresent()) {
|
||||||
|
return CommandResult.failure(target.getName() + " est déjà dans une équipe.");
|
||||||
|
}
|
||||||
|
service.addMember(team.getId(), target.getUniqueId());
|
||||||
|
return CommandResult.success(target.getName() + " ajouté à l'équipe " + team.getName() + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.ArgumentType;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types d'arguments spécifiques aux commandes team. Fournit un
|
||||||
|
* {@link ArgumentType} qui résout un nom d'équipe en {@link Team} et propose
|
||||||
|
* la tab-complétion des équipes existantes.
|
||||||
|
*/
|
||||||
|
public final class TeamArgumentTypes {
|
||||||
|
|
||||||
|
private TeamArgumentTypes() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résout un nom d'équipe en {@link Team} (case-insensitive) en interrogeant
|
||||||
|
* le {@link TeamService}. Suggère les noms d'équipes existantes en
|
||||||
|
* tab-completion.
|
||||||
|
*/
|
||||||
|
public static ArgumentType<Team> teamByName(TeamService service) {
|
||||||
|
Objects.requireNonNull(service, "service");
|
||||||
|
return new ArgumentType<>() {
|
||||||
|
@Override
|
||||||
|
public Team parse(String input) {
|
||||||
|
return service.getTeamByName(input).orElseThrow(() ->
|
||||||
|
new fr.luc.crcore.command.CommandException(
|
||||||
|
"Aucune équipe trouvée : " + input));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggestions(CommandSender sender, String partial) {
|
||||||
|
String lower = partial.toLowerCase();
|
||||||
|
return service.getAllTeams().stream()
|
||||||
|
.map(Team::getName)
|
||||||
|
.filter(n -> n.toLowerCase().startsWith(lower))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.ArgumentTypes;
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamColor;
|
||||||
|
import fr.luc.crcore.team.TeamException;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import fr.luc.crcore.team.TeamVisibility;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team create <name> <tag> <color> [visibility]}
|
||||||
|
*
|
||||||
|
* <p>Crée une équipe dont l'exécutant devient le chef. Visibilité par défaut :
|
||||||
|
* {@link TeamVisibility#PRIVATE}.
|
||||||
|
*/
|
||||||
|
public class TeamCreateSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamCreateSubCommand(TeamService service) {
|
||||||
|
super("create", "c", "new");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Créer une équipe");
|
||||||
|
permission("crcore.team.create");
|
||||||
|
playerOnly();
|
||||||
|
argument("name", ArgumentTypes.STRING);
|
||||||
|
argument("tag", ArgumentTypes.STRING);
|
||||||
|
argument("color", ArgumentTypes.enumOf(TeamColor.class));
|
||||||
|
optionalArgument("visibility", ArgumentTypes.enumOf(TeamVisibility.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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");
|
||||||
|
TeamVisibility visibility = ctx.<TeamVisibility>getOptional("visibility")
|
||||||
|
.orElse(TeamVisibility.PRIVATE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Team team = service.createTeam(name, tag, color, player.getUniqueId(), visibility);
|
||||||
|
return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag() + "] créée.");
|
||||||
|
} catch (TeamException ex) {
|
||||||
|
return CommandResult.failure(ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team delete}
|
||||||
|
*
|
||||||
|
* <p>Dissout l'équipe de l'exécutant. Réservé au chef. Pas d'argument :
|
||||||
|
* l'équipe ciblée est déduite du joueur.
|
||||||
|
*
|
||||||
|
* <p>Aliases : {@code disband}, {@code dissolve}.
|
||||||
|
*/
|
||||||
|
public class TeamDeleteSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamDeleteSubCommand(TeamService service) {
|
||||||
|
super("delete", "disband", "dissolve");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Dissoudre son équipe (chef uniquement)");
|
||||||
|
playerOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Player player = ctx.requirePlayer();
|
||||||
|
Team team = service.getTeamOfPlayer(player.getUniqueId())
|
||||||
|
.orElse(null);
|
||||||
|
if (team == null) {
|
||||||
|
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||||
|
}
|
||||||
|
if (!team.getLeaderId().equals(player.getUniqueId())) {
|
||||||
|
return CommandResult.failure("Seul le chef peut dissoudre l'équipe.");
|
||||||
|
}
|
||||||
|
service.dissolveTeam(team.getId());
|
||||||
|
return CommandResult.success("Équipe " + team.getName() + " dissoute.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groupe {@code /core team ...} : container de toutes les sous-commandes
|
||||||
|
* d'équipe par défaut.
|
||||||
|
*
|
||||||
|
* <p>Pour overrider une sous-commande, un plugin de jeu fait :
|
||||||
|
* <pre>{@code
|
||||||
|
* core.getCoreCommand().findSubCommand("team")
|
||||||
|
* .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc)));
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Ou sous-classe {@code TeamGroupSubCommand} et redéfinit son constructeur
|
||||||
|
* pour swap ce qu'il faut.
|
||||||
|
*/
|
||||||
|
public class TeamGroupSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamGroupSubCommand(TeamService service) {
|
||||||
|
super("team", "t");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Gestion des équipes");
|
||||||
|
registerDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enregistre toutes les sous-commandes par défaut. Override pour exclure
|
||||||
|
* ou ajouter des sous-commandes au lieu du jeu standard.
|
||||||
|
*/
|
||||||
|
protected void registerDefaults() {
|
||||||
|
addSubCommand(new TeamCreateSubCommand(service));
|
||||||
|
addSubCommand(new TeamDeleteSubCommand(service));
|
||||||
|
addSubCommand(new TeamAddSubCommand(service));
|
||||||
|
addSubCommand(new TeamRemoveSubCommand(service));
|
||||||
|
addSubCommand(new TeamJoinSubCommand(service));
|
||||||
|
addSubCommand(new TeamLeaveSubCommand(service));
|
||||||
|
addSubCommand(new TeamInfoSubCommand(service));
|
||||||
|
addSubCommand(new TeamListSubCommand(service));
|
||||||
|
addSubCommand(new TeamTransferSubCommand(service));
|
||||||
|
addSubCommand(new TeamVisibilitySubCommand(service));
|
||||||
|
addSubCommand(new TeamScoreSubCommand(service));
|
||||||
|
addSubCommand(new TeamTopSubCommand(service));
|
||||||
|
addSubCommand(new TeamSetSpawnSubCommand(service));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamMember;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team info [name]}
|
||||||
|
*
|
||||||
|
* <p>Affiche les infos d'une équipe. Si aucun nom n'est donné, affiche celle
|
||||||
|
* de l'exécutant.
|
||||||
|
*/
|
||||||
|
public class TeamInfoSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamInfoSubCommand(TeamService service) {
|
||||||
|
super("info", "i");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Afficher les infos d'une équipe");
|
||||||
|
optionalArgument("name", TeamArgumentTypes.teamByName(service));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Team team = ctx.<Team>getOptional("name").orElseGet(() -> {
|
||||||
|
if (ctx.isPlayer()) {
|
||||||
|
Player p = (Player) ctx.getSender();
|
||||||
|
return service.getTeamOfPlayer(p.getUniqueId()).orElse(null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (team == null) {
|
||||||
|
return CommandResult.failure("Aucune équipe spécifiée et vous n'êtes pas dans une équipe.");
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
ChatColor c = team.getColor().getChatColor();
|
||||||
|
sb.append(c).append("=== ").append(team.getName())
|
||||||
|
.append(" [#").append(team.getTag()).append("] ===\n");
|
||||||
|
sb.append(ChatColor.GRAY).append("Couleur : ").append(c).append(team.getColor().getDisplayName()).append('\n');
|
||||||
|
sb.append(ChatColor.GRAY).append("Visibilité : ").append(ChatColor.WHITE).append(team.getVisibility()).append('\n');
|
||||||
|
sb.append(ChatColor.GRAY).append("Membres (").append(team.size()).append(") : ").append(ChatColor.WHITE);
|
||||||
|
sb.append(team.getMembers().stream()
|
||||||
|
.map(m -> Bukkit.getOfflinePlayer(m.getPlayerId()).getName() +
|
||||||
|
(m.isLeader() ? "★" : ""))
|
||||||
|
.collect(Collectors.joining(", ")));
|
||||||
|
if (!team.getScores().isEmpty()) {
|
||||||
|
sb.append('\n').append(ChatColor.GRAY).append("Scores : ").append(ChatColor.WHITE)
|
||||||
|
.append(team.getScores().entrySet().stream()
|
||||||
|
.map(e -> e.getKey() + "=" + e.getValue())
|
||||||
|
.collect(Collectors.joining(", ")));
|
||||||
|
}
|
||||||
|
if (team.hasSpawnPoint()) {
|
||||||
|
team.getSpawnPoint().ifPresent(loc -> sb.append('\n').append(ChatColor.GRAY).append("Spawn : ")
|
||||||
|
.append(ChatColor.WHITE).append(loc.getWorld().getName()).append(' ')
|
||||||
|
.append((int) loc.getX()).append('/').append((int) loc.getY()).append('/').append((int) loc.getZ()));
|
||||||
|
}
|
||||||
|
ctx.reply(sb.toString());
|
||||||
|
return CommandResult.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamException;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team join <name>}
|
||||||
|
*
|
||||||
|
* <p>Auto-join sur une équipe PUBLIC. Lève une {@link TeamException} si la
|
||||||
|
* team est privée ou si le joueur est déjà dans une équipe (rendu en
|
||||||
|
* message d'erreur).
|
||||||
|
*/
|
||||||
|
public class TeamJoinSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamJoinSubCommand(TeamService service) {
|
||||||
|
super("join", "j");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Rejoindre une équipe publique");
|
||||||
|
playerOnly();
|
||||||
|
argument("name", TeamArgumentTypes.teamByName(service));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Player player = ctx.requirePlayer();
|
||||||
|
Team team = ctx.get("name");
|
||||||
|
try {
|
||||||
|
service.joinTeam(team.getId(), player.getUniqueId());
|
||||||
|
return CommandResult.success("Vous avez rejoint " + team.getName() + ".");
|
||||||
|
} catch (TeamException ex) {
|
||||||
|
return CommandResult.failure(ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team leave}
|
||||||
|
*
|
||||||
|
* <p>Le joueur quitte volontairement son équipe. Refusé pour le chef (il doit
|
||||||
|
* d'abord transférer le leadership ou dissoudre l'équipe).
|
||||||
|
*/
|
||||||
|
public class TeamLeaveSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamLeaveSubCommand(TeamService service) {
|
||||||
|
super("leave", "quit");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Quitter son équipe");
|
||||||
|
playerOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Player player = ctx.requirePlayer();
|
||||||
|
Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null);
|
||||||
|
if (team == null) {
|
||||||
|
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||||
|
}
|
||||||
|
if (team.getLeaderId().equals(player.getUniqueId())) {
|
||||||
|
return CommandResult.failure(
|
||||||
|
"Vous êtes le chef. Transférez le leadership avec /core team transfer <player>, ou dissolvez avec /core team delete.");
|
||||||
|
}
|
||||||
|
service.removeMember(team.getId(), player.getUniqueId());
|
||||||
|
return CommandResult.success("Vous avez quitté l'équipe " + team.getName() + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team list}
|
||||||
|
*
|
||||||
|
* <p>Affiche toutes les équipes existantes avec leur tag, nom, taille et
|
||||||
|
* visibilité.
|
||||||
|
*/
|
||||||
|
public class TeamListSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamListSubCommand(TeamService service) {
|
||||||
|
super("list", "ls");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Lister toutes les équipes");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Collection<Team> teams = service.getAllTeams();
|
||||||
|
if (teams.isEmpty()) {
|
||||||
|
return CommandResult.success("Aucune équipe pour le moment.");
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder(ChatColor.YELLOW + "Équipes (" + teams.size() + ") :");
|
||||||
|
for (Team team : teams) {
|
||||||
|
ChatColor c = team.getColor().getChatColor();
|
||||||
|
sb.append('\n').append(ChatColor.GRAY).append(" - ")
|
||||||
|
.append(c).append('[').append(team.getTag()).append("] ")
|
||||||
|
.append(team.getName())
|
||||||
|
.append(ChatColor.GRAY).append(" (").append(team.size()).append(" membres, ")
|
||||||
|
.append(team.getVisibility()).append(")");
|
||||||
|
}
|
||||||
|
ctx.reply(sb.toString());
|
||||||
|
return CommandResult.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.ArgumentTypes;
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team remove <player>}
|
||||||
|
*
|
||||||
|
* <p>Le chef retire un membre. Accepte les joueurs offline (utilise leur nom
|
||||||
|
* pour résoudre l'UUID via {@link Bukkit#getOfflinePlayer(String)}).
|
||||||
|
*/
|
||||||
|
public class TeamRemoveSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamRemoveSubCommand(TeamService service) {
|
||||||
|
super("remove", "kick", "expel");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Retirer un joueur de son équipe (chef uniquement)");
|
||||||
|
playerOnly();
|
||||||
|
argument("player", ArgumentTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Player executor = ctx.requirePlayer();
|
||||||
|
String targetName = ctx.get("player");
|
||||||
|
|
||||||
|
Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
|
||||||
|
if (team == null) {
|
||||||
|
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||||
|
}
|
||||||
|
if (!team.getLeaderId().equals(executor.getUniqueId())) {
|
||||||
|
return CommandResult.failure("Seul le chef peut retirer des membres.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
|
||||||
|
if (target.getUniqueId().equals(executor.getUniqueId())) {
|
||||||
|
return CommandResult.failure("Pour quitter l'équipe en tant que chef, transférez d'abord le leadership.");
|
||||||
|
}
|
||||||
|
if (!team.hasMember(target.getUniqueId())) {
|
||||||
|
return CommandResult.failure(targetName + " n'est pas dans votre équipe.");
|
||||||
|
}
|
||||||
|
service.removeMember(team.getId(), target.getUniqueId());
|
||||||
|
return CommandResult.success(targetName + " retiré de l'équipe.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.ArgumentTypes;
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team score <team> <name> <add|set> <value>}
|
||||||
|
*
|
||||||
|
* <p>Commande admin pour ajuster les scores d'une équipe à la main (debug,
|
||||||
|
* fix, init). Le gameplay normal pilote les scores via le service, pas via
|
||||||
|
* cette commande.
|
||||||
|
*
|
||||||
|
* <p>Restreinte par défaut à la permission {@code crcore.team.score.modify}.
|
||||||
|
*/
|
||||||
|
public class TeamScoreSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamScoreSubCommand(TeamService service) {
|
||||||
|
super("score");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("[Admin] Modifier le score d'une équipe");
|
||||||
|
permission("crcore.team.score.modify");
|
||||||
|
argument("team", TeamArgumentTypes.teamByName(service));
|
||||||
|
argument("name", ArgumentTypes.STRING);
|
||||||
|
argument("op", ArgumentTypes.choice("add", "set"));
|
||||||
|
argument("value", ArgumentTypes.INTEGER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Team team = ctx.get("team");
|
||||||
|
String name = ctx.get("name");
|
||||||
|
String op = ctx.get("op");
|
||||||
|
int value = ctx.get("value");
|
||||||
|
|
||||||
|
int result = switch (op) {
|
||||||
|
case "add" -> service.addScore(team.getId(), name, value);
|
||||||
|
case "set" -> service.setScore(team.getId(), name, value);
|
||||||
|
default -> throw new IllegalStateException("unreachable: " + op);
|
||||||
|
};
|
||||||
|
return CommandResult.success("Score " + name + " de " + team.getName() + " = " + result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team setspawn}
|
||||||
|
*
|
||||||
|
* <p>Définit le point de spawn de l'équipe à la position courante du chef.
|
||||||
|
*/
|
||||||
|
public class TeamSetSpawnSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamSetSpawnSubCommand(TeamService service) {
|
||||||
|
super("setspawn", "spawn");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Définir le point de spawn de l'équipe (chef uniquement)");
|
||||||
|
playerOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Player player = ctx.requirePlayer();
|
||||||
|
Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null);
|
||||||
|
if (team == null) {
|
||||||
|
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||||
|
}
|
||||||
|
if (!team.getLeaderId().equals(player.getUniqueId())) {
|
||||||
|
return CommandResult.failure("Seul le chef peut définir le spawn.");
|
||||||
|
}
|
||||||
|
service.setSpawnPoint(team.getId(), player.getLocation());
|
||||||
|
return CommandResult.success("Spawn de l'équipe défini à votre position.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.ArgumentTypes;
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.TeamRanking;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.ChatColor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team top [score]}
|
||||||
|
*
|
||||||
|
* <p>Affiche le classement des équipes. Sans argument, classement global
|
||||||
|
* (somme de tous les scores). Avec un nom de score, classement sur ce score
|
||||||
|
* précis.
|
||||||
|
*/
|
||||||
|
public class TeamTopSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
protected final int limit;
|
||||||
|
|
||||||
|
public TeamTopSubCommand(TeamService service) {
|
||||||
|
this(service, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TeamTopSubCommand(TeamService service, int limit) {
|
||||||
|
super("top", "ranking", "leaderboard");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
this.limit = limit;
|
||||||
|
description("Classement des équipes");
|
||||||
|
optionalArgument("score", ArgumentTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
String scoreName = ctx.<String>getOptional("score").orElse(null);
|
||||||
|
List<TeamRanking> ranking = scoreName == null
|
||||||
|
? service.getTopGlobalRanking(limit)
|
||||||
|
: service.getTopRankingByScore(scoreName, limit);
|
||||||
|
|
||||||
|
if (ranking.isEmpty()) {
|
||||||
|
return CommandResult.success("Aucune équipe à classer.");
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder(ChatColor.YELLOW + "Top " + ranking.size() +
|
||||||
|
(scoreName == null ? " (global) :" : " (" + scoreName + ") :"));
|
||||||
|
for (TeamRanking r : ranking) {
|
||||||
|
sb.append('\n').append(ChatColor.GRAY).append(" ").append(r.rank()).append(". ")
|
||||||
|
.append(r.team().getColor().getChatColor()).append(r.team().getName())
|
||||||
|
.append(ChatColor.GRAY).append(" — ").append(ChatColor.WHITE).append(r.score());
|
||||||
|
}
|
||||||
|
ctx.reply(sb.toString());
|
||||||
|
return CommandResult.success();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.ArgumentTypes;
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team transfer <player>}
|
||||||
|
*
|
||||||
|
* <p>Le chef transmet son rôle à un autre membre existant de son équipe.
|
||||||
|
*/
|
||||||
|
public class TeamTransferSubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamTransferSubCommand(TeamService service) {
|
||||||
|
super("transfer");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Transférer le rôle de chef à un autre membre");
|
||||||
|
playerOnly();
|
||||||
|
argument("player", ArgumentTypes.STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Player executor = ctx.requirePlayer();
|
||||||
|
String targetName = ctx.get("player");
|
||||||
|
Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
|
||||||
|
if (team == null) {
|
||||||
|
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||||
|
}
|
||||||
|
if (!team.getLeaderId().equals(executor.getUniqueId())) {
|
||||||
|
return CommandResult.failure("Seul le chef peut transférer le leadership.");
|
||||||
|
}
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
|
||||||
|
if (!team.hasMember(target.getUniqueId())) {
|
||||||
|
return CommandResult.failure(targetName + " n'est pas dans votre équipe.");
|
||||||
|
}
|
||||||
|
service.transferLeadership(team.getId(), target.getUniqueId());
|
||||||
|
return CommandResult.success("Leadership transféré à " + targetName + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package fr.luc.crcore.command.builtin.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.command.ArgumentTypes;
|
||||||
|
import fr.luc.crcore.command.CommandContext;
|
||||||
|
import fr.luc.crcore.command.CommandResult;
|
||||||
|
import fr.luc.crcore.command.SubCommand;
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamService;
|
||||||
|
import fr.luc.crcore.team.TeamVisibility;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code /core team visibility <PUBLIC|PRIVATE>}
|
||||||
|
*
|
||||||
|
* <p>Le chef change la visibilité de son équipe. PUBLIC = les autres joueurs
|
||||||
|
* peuvent rejoindre avec {@code /core team join}.
|
||||||
|
*/
|
||||||
|
public class TeamVisibilitySubCommand extends SubCommand {
|
||||||
|
|
||||||
|
protected final TeamService service;
|
||||||
|
|
||||||
|
public TeamVisibilitySubCommand(TeamService service) {
|
||||||
|
super("visibility", "vis");
|
||||||
|
this.service = Objects.requireNonNull(service, "service");
|
||||||
|
description("Changer la visibilité de son équipe");
|
||||||
|
playerOnly();
|
||||||
|
argument("visibility", ArgumentTypes.enumOf(TeamVisibility.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandResult execute(CommandContext ctx) {
|
||||||
|
Player player = ctx.requirePlayer();
|
||||||
|
TeamVisibility visibility = ctx.get("visibility");
|
||||||
|
Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null);
|
||||||
|
if (team == null) {
|
||||||
|
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||||
|
}
|
||||||
|
if (!team.getLeaderId().equals(player.getUniqueId())) {
|
||||||
|
return CommandResult.failure("Seul le chef peut changer la visibilité.");
|
||||||
|
}
|
||||||
|
service.setVisibility(team.getId(), visibility);
|
||||||
|
return CommandResult.success("Visibilité réglée sur " + visibility + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,17 @@ import java.util.Collection;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrat CRUD générique pour tout aggregate {@link Identifiable}. Permet de
|
||||||
|
* brancher différents backends (in-memory, SQLite, …) sans toucher au code
|
||||||
|
* service.
|
||||||
|
*
|
||||||
|
* <p>Implémentations CR-Core par défaut :
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code InMemoryTeamRepository} / {@code SqliteTeamRepository}</li>
|
||||||
|
* <li>{@code InMemoryPlayerProfileRepository} / {@code SqlitePlayerProfileRepository}</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
public interface Repository<T extends Identifiable> {
|
public interface Repository<T extends Identifiable> {
|
||||||
|
|
||||||
T save(T entity);
|
T save(T entity);
|
||||||
|
|||||||
@@ -2,6 +2,18 @@ package fr.luc.crcore.common;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrat partagé par tout ce qui porte des scores nommés. Implémenté par
|
||||||
|
* {@link fr.luc.crcore.team.Team} et {@link fr.luc.crcore.player.PlayerProfile}.
|
||||||
|
*
|
||||||
|
* <p>Les scores sont identifiés par un nom libre (ex. {@code "kills"},
|
||||||
|
* {@code "objectives"}, {@code "global"}) et stockés comme entiers. Un jeu
|
||||||
|
* mono-score peut conventionnellement utiliser {@code "global"}.
|
||||||
|
*
|
||||||
|
* <p>{@link #getScore(String)} renvoie 0 pour un score jamais initialisé
|
||||||
|
* (utile pour {@code addScore("kills", 1)} sans set préalable). Pour
|
||||||
|
* distinguer "jamais set" et "set à 0", utiliser {@link #hasScore(String)}.
|
||||||
|
*/
|
||||||
public interface ScoreHolder {
|
public interface ScoreHolder {
|
||||||
|
|
||||||
int getScore(String scoreName);
|
int getScore(String scoreName);
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package fr.luc.crcore.database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types de colonnes supportés par {@link TableBuilder}, chacun mappé sur un
|
||||||
|
* type natif SQLite. Volontairement réduit : SQLite est faiblement typé, ces
|
||||||
|
* 6 types couvrent largement les besoins d'un plugin Minecraft.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #INTEGER} — entiers 32/64 bits</li>
|
||||||
|
* <li>{@link #REAL} — flottants (double)</li>
|
||||||
|
* <li>{@link #TEXT} — chaînes UTF-8</li>
|
||||||
|
* <li>{@link #BLOB} — données binaires brutes</li>
|
||||||
|
* <li>{@link #BOOLEAN} — stocké comme INTEGER (0/1) côté SQLite</li>
|
||||||
|
* <li>{@link #UUID} — stocké comme TEXT (forme canonique 36 caractères)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public enum ColumnType {
|
||||||
|
|
||||||
|
INTEGER("INTEGER"),
|
||||||
|
REAL("REAL"),
|
||||||
|
TEXT("TEXT"),
|
||||||
|
BLOB("BLOB"),
|
||||||
|
BOOLEAN("INTEGER"),
|
||||||
|
UUID("TEXT");
|
||||||
|
|
||||||
|
private final String sqlType;
|
||||||
|
|
||||||
|
ColumnType(String sqlType) {
|
||||||
|
this.sqlType = sqlType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nom SQL natif côté SQLite. */
|
||||||
|
public String getSqlType() {
|
||||||
|
return sqlType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package fr.luc.crcore.database;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Façade SQLite minimaliste pour CR-Core et les plugins de jeu downstream.
|
||||||
|
*
|
||||||
|
* <p>Ouvre une connexion JDBC vers un fichier SQLite et expose 4 méthodes pour
|
||||||
|
* couvrir 95 % des besoins :
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #execute(String, Object...)} — DDL ou DML qui ne renvoie rien</li>
|
||||||
|
* <li>{@link #update(String, Object...)} — INSERT/UPDATE/DELETE, renvoie le nombre de lignes affectées</li>
|
||||||
|
* <li>{@link #queryOne(String, RowMapper, Object...)} — SELECT renvoyant au plus une ligne</li>
|
||||||
|
* <li>{@link #query(String, RowMapper, Object...)} — SELECT renvoyant plusieurs lignes</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Les paramètres ({@code Object...}) sont liés via PreparedStatement (donc
|
||||||
|
* pas d'injection SQL possible). Les {@link UUID} sont automatiquement
|
||||||
|
* convertis en {@code TEXT}.
|
||||||
|
*
|
||||||
|
* <p>Pour créer une table de manière fluide, voir {@link #table(String)}.
|
||||||
|
*
|
||||||
|
* <p>Cette classe n'est pas thread-safe. Un plugin Bukkit accède en général
|
||||||
|
* à la DB depuis le main thread ; pour de l'async, synchroniser explicitement
|
||||||
|
* ou ouvrir plusieurs instances.
|
||||||
|
*/
|
||||||
|
public class Database implements AutoCloseable {
|
||||||
|
|
||||||
|
private final Connection connection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ouvre (ou crée) un fichier SQLite. Le dossier parent doit exister.
|
||||||
|
*
|
||||||
|
* @throws DatabaseException si le driver JDBC SQLite est absent du
|
||||||
|
* classpath, ou si l'ouverture du fichier échoue.
|
||||||
|
*/
|
||||||
|
public Database(File file) {
|
||||||
|
Objects.requireNonNull(file, "file");
|
||||||
|
try {
|
||||||
|
// Force le chargement du driver (utile sur certains classloaders Bukkit).
|
||||||
|
Class.forName("org.sqlite.JDBC");
|
||||||
|
String url = "jdbc:sqlite:" + file.getAbsolutePath();
|
||||||
|
this.connection = DriverManager.getConnection(url);
|
||||||
|
// Active les foreign keys (désactivées par défaut sur SQLite).
|
||||||
|
execute("PRAGMA foreign_keys = ON");
|
||||||
|
} catch (ClassNotFoundException ex) {
|
||||||
|
throw new DatabaseException(
|
||||||
|
"SQLite JDBC driver not found on classpath (org.sqlite.JDBC).", ex);
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DatabaseException("Failed to open SQLite database: " + file, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Connexion JDBC sous-jacente, pour les cas avancés (transactions custom, etc.). */
|
||||||
|
public Connection getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarre la création d'une table de manière fluide.
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* db.table("foo")
|
||||||
|
* .ifNotExists()
|
||||||
|
* .column("id", ColumnType.UUID).primaryKey()
|
||||||
|
* .column("name", ColumnType.TEXT).notNull()
|
||||||
|
* .create();
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public TableBuilder table(String name) {
|
||||||
|
return new TableBuilder(this, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vérifie l'existence d'une table dans la base. */
|
||||||
|
public boolean tableExists(String name) {
|
||||||
|
Objects.requireNonNull(name, "name");
|
||||||
|
return queryOne(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
||||||
|
rs -> rs.getString(1),
|
||||||
|
name
|
||||||
|
).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécute un statement SQL (DDL ou autre) sans collecter de résultat. Pour
|
||||||
|
* les INSERT/UPDATE/DELETE préférer {@link #update}.
|
||||||
|
*/
|
||||||
|
public void execute(String sql, Object... params) {
|
||||||
|
try (PreparedStatement stmt = prepare(sql, params)) {
|
||||||
|
stmt.execute();
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DatabaseException("execute() failed: " + sql, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécute un INSERT/UPDATE/DELETE et renvoie le nombre de lignes affectées.
|
||||||
|
*/
|
||||||
|
public int update(String sql, Object... params) {
|
||||||
|
try (PreparedStatement stmt = prepare(sql, params)) {
|
||||||
|
return stmt.executeUpdate();
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DatabaseException("update() failed: " + sql, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécute un SELECT et renvoie au plus une ligne. {@link Optional#empty()}
|
||||||
|
* si aucune ligne. Lève {@link DatabaseException} si plusieurs lignes
|
||||||
|
* matchent — penser à mettre {@code LIMIT 1} ou un {@code WHERE} unique.
|
||||||
|
*/
|
||||||
|
public <T> Optional<T> queryOne(String sql, RowMapper<T> mapper, Object... params) {
|
||||||
|
Objects.requireNonNull(mapper, "mapper");
|
||||||
|
try (PreparedStatement stmt = prepare(sql, params);
|
||||||
|
ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (!rs.next()) return Optional.empty();
|
||||||
|
T value = mapper.map(rs);
|
||||||
|
if (rs.next()) {
|
||||||
|
throw new DatabaseException("queryOne() returned more than one row: " + sql);
|
||||||
|
}
|
||||||
|
return Optional.ofNullable(value);
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DatabaseException("queryOne() failed: " + sql, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exécute un SELECT et renvoie la liste de toutes les lignes mappées. */
|
||||||
|
public <T> List<T> query(String sql, RowMapper<T> mapper, Object... params) {
|
||||||
|
Objects.requireNonNull(mapper, "mapper");
|
||||||
|
List<T> results = new ArrayList<>();
|
||||||
|
try (PreparedStatement stmt = prepare(sql, params);
|
||||||
|
ResultSet rs = stmt.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
results.add(mapper.map(rs));
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DatabaseException("query() failed: " + sql, ex);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécute un bloc dans une transaction. Commit automatique si le bloc se
|
||||||
|
* termine sans exception, rollback sinon. Pour des opérations multiples
|
||||||
|
* qui doivent être atomiques (ex. créer une équipe + ses membres).
|
||||||
|
*/
|
||||||
|
public void inTransaction(Runnable block) {
|
||||||
|
Objects.requireNonNull(block, "block");
|
||||||
|
try {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
try {
|
||||||
|
block.run();
|
||||||
|
connection.commit();
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
connection.rollback();
|
||||||
|
throw ex;
|
||||||
|
} finally {
|
||||||
|
connection.setAutoCommit(true);
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DatabaseException("Transaction failed", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
if (connection != null && !connection.isClosed()) {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DatabaseException("Failed to close database", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Internal ----
|
||||||
|
|
||||||
|
private PreparedStatement prepare(String sql, Object[] params) throws SQLException {
|
||||||
|
PreparedStatement stmt = connection.prepareStatement(sql);
|
||||||
|
if (params != null) {
|
||||||
|
for (int i = 0; i < params.length; i++) {
|
||||||
|
stmt.setObject(i + 1, normalize(params[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convertit les valeurs Java non-natives SQL en types utilisables par JDBC. */
|
||||||
|
private static Object normalize(Object value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value instanceof UUID uuid) return uuid.toString();
|
||||||
|
if (value instanceof Enum<?> e) return e.name();
|
||||||
|
if (value instanceof Boolean b) return b ? 1 : 0;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package fr.luc.crcore.database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception levée pour toute erreur de persistance (ouverture de connexion,
|
||||||
|
* exécution SQL, mapping de résultat). Toujours basée sur une {@link Throwable}
|
||||||
|
* d'origine (généralement {@link java.sql.SQLException}) accessible via
|
||||||
|
* {@link #getCause()}.
|
||||||
|
*/
|
||||||
|
public class DatabaseException extends RuntimeException {
|
||||||
|
|
||||||
|
public DatabaseException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatabaseException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package fr.luc.crcore.database;
|
||||||
|
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit une ligne d'un {@link ResultSet} en un objet Java.
|
||||||
|
*
|
||||||
|
* <p>Utilisé par {@link Database#query} et {@link Database#queryOne}. Le mapper
|
||||||
|
* <b>ne doit pas</b> appeler {@code rs.next()} : c'est {@link Database} qui
|
||||||
|
* itère.
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* RowMapper<String> nameMapper = rs -> rs.getString("name");
|
||||||
|
* List<String> names = db.query("SELECT name FROM teams", nameMapper);
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface RowMapper<T> {
|
||||||
|
|
||||||
|
T map(ResultSet rs) throws SQLException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package fr.luc.crcore.database;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder fluide pour créer une table SQL en quelques lignes. Obtenu via
|
||||||
|
* {@link Database#table(String)}.
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* db.table("my_scores")
|
||||||
|
* .ifNotExists()
|
||||||
|
* .column("player_id", ColumnType.UUID).primaryKey()
|
||||||
|
* .column("score", ColumnType.INTEGER).notNull().defaultValue("0")
|
||||||
|
* .column("updated_at", ColumnType.INTEGER).notNull()
|
||||||
|
* .create();
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Ne supporte pas (volontairement) les FOREIGN KEY ni les contraintes
|
||||||
|
* multi-colonnes pour rester simple. Pour ces cas, utiliser
|
||||||
|
* {@link Database#execute(String, Object...)} avec un {@code CREATE TABLE}
|
||||||
|
* brut.
|
||||||
|
*/
|
||||||
|
public class TableBuilder {
|
||||||
|
|
||||||
|
private final Database database;
|
||||||
|
private final String name;
|
||||||
|
private final List<ColumnDef> columns = new ArrayList<>();
|
||||||
|
private boolean ifNotExists = false;
|
||||||
|
|
||||||
|
TableBuilder(Database database, String name) {
|
||||||
|
this.database = Objects.requireNonNull(database, "database");
|
||||||
|
this.name = Objects.requireNonNull(name, "name");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ajoute {@code IF NOT EXISTS} à la création (idempotent). */
|
||||||
|
public TableBuilder ifNotExists() {
|
||||||
|
this.ifNotExists = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarre la définition d'une colonne. Renvoie un {@link ColumnDef} sur
|
||||||
|
* lequel on chaîne {@code .primaryKey()}, {@code .notNull()}, etc.
|
||||||
|
*/
|
||||||
|
public ColumnDef column(String name, ColumnType type) {
|
||||||
|
ColumnDef def = new ColumnDef(this, name, type);
|
||||||
|
columns.add(def);
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exécute le {@code CREATE TABLE}. Lève {@link DatabaseException} en cas d'échec. */
|
||||||
|
public void create() {
|
||||||
|
if (columns.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Cannot create table '" + name + "' with no columns.");
|
||||||
|
}
|
||||||
|
StringBuilder sql = new StringBuilder("CREATE TABLE ");
|
||||||
|
if (ifNotExists) sql.append("IF NOT EXISTS ");
|
||||||
|
sql.append('\"').append(name).append("\" (");
|
||||||
|
for (int i = 0; i < columns.size(); i++) {
|
||||||
|
if (i > 0) sql.append(", ");
|
||||||
|
sql.append(columns.get(i).toSql());
|
||||||
|
}
|
||||||
|
sql.append(')');
|
||||||
|
database.execute(sql.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définition d'une colonne en cours de construction. Toutes les méthodes
|
||||||
|
* renvoient {@code this} (ou le {@link TableBuilder} pour continuer la
|
||||||
|
* définition d'une autre colonne).
|
||||||
|
*/
|
||||||
|
public static class ColumnDef {
|
||||||
|
|
||||||
|
private final TableBuilder parent;
|
||||||
|
private final String name;
|
||||||
|
private final ColumnType type;
|
||||||
|
private boolean primaryKey = false;
|
||||||
|
private boolean notNull = false;
|
||||||
|
private boolean unique = false;
|
||||||
|
private String defaultValue = null;
|
||||||
|
|
||||||
|
ColumnDef(TableBuilder parent, String name, ColumnType type) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.name = Objects.requireNonNull(name, "name");
|
||||||
|
this.type = Objects.requireNonNull(type, "type");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColumnDef primaryKey() { this.primaryKey = true; return this; }
|
||||||
|
public ColumnDef notNull() { this.notNull = true; return this; }
|
||||||
|
public ColumnDef unique() { this.unique = true; return this; }
|
||||||
|
|
||||||
|
/** Valeur par défaut en clause {@code DEFAULT}. Le texte est inséré tel quel — penser à quoter les String. */
|
||||||
|
public ColumnDef defaultValue(String sqlExpression) {
|
||||||
|
this.defaultValue = sqlExpression;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Démarre une nouvelle colonne (raccourci pour {@code .build().column(...)}). */
|
||||||
|
public ColumnDef column(String name, ColumnType type) {
|
||||||
|
return parent.column(name, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Termine la définition et lance le {@code CREATE TABLE}. */
|
||||||
|
public void create() {
|
||||||
|
parent.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
String toSql() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append('\"').append(name).append("\" ").append(type.getSqlType());
|
||||||
|
if (primaryKey) sb.append(" PRIMARY KEY");
|
||||||
|
if (notNull) sb.append(" NOT NULL");
|
||||||
|
if (unique) sb.append(" UNIQUE");
|
||||||
|
if (defaultValue != null) sb.append(" DEFAULT ").append(defaultValue);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package fr.luc.crcore.player;
|
||||||
|
|
||||||
|
import fr.luc.crcore.player.event.PlayerProfileCreateEvent;
|
||||||
|
import fr.luc.crcore.player.event.PlayerProfileDeleteEvent;
|
||||||
|
import fr.luc.crcore.player.event.PlayerScoreChangeEvent;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variante de {@link PlayerProfileServiceImpl} qui tire des évènements Bukkit
|
||||||
|
* via les hooks {@code on...}. Utilisée par défaut par {@code CRCore}.
|
||||||
|
*/
|
||||||
|
public class BukkitEventFiringPlayerProfileServiceImpl extends PlayerProfileServiceImpl {
|
||||||
|
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
|
||||||
|
public BukkitEventFiringPlayerProfileServiceImpl(JavaPlugin plugin, PlayerProfileRepository repository) {
|
||||||
|
super(repository);
|
||||||
|
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JavaPlugin getPlugin() {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProfileCreated(PlayerProfile profile) {
|
||||||
|
Bukkit.getPluginManager().callEvent(new PlayerProfileCreateEvent(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProfileDeleted(PlayerProfile profile) {
|
||||||
|
Bukkit.getPluginManager().callEvent(new PlayerProfileDeleteEvent(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onScoreChanged(PlayerProfile profile, String scoreName, int oldValue, int newValue) {
|
||||||
|
Bukkit.getPluginManager().callEvent(
|
||||||
|
new PlayerScoreChangeEvent(profile, scoreName, oldValue, newValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,14 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profil persistant d'un joueur. Identifié par l'UUID Bukkit du joueur,
|
||||||
|
* porte ses scores nommés.
|
||||||
|
*
|
||||||
|
* <p>Indépendant du domaine team : un joueur peut entrer / quitter / changer
|
||||||
|
* d'équipe sans toucher à son profil. Géré par {@link PlayerProfileService}
|
||||||
|
* qui auto-crée le profil à la première écriture de score.
|
||||||
|
*/
|
||||||
public class PlayerProfile extends AbstractEntity implements ScoreHolder {
|
public class PlayerProfile extends AbstractEntity implements ScoreHolder {
|
||||||
|
|
||||||
private final Map<String, Integer> scores;
|
private final Map<String, Integer> scores;
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import java.util.Optional;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Façade pour les profils joueurs : lifecycle, scores, classements.
|
||||||
|
*
|
||||||
|
* <p>Auto-création : {@link #addScore}, {@link #setScore} créent
|
||||||
|
* automatiquement le profil s'il n'existe pas (via {@link #getOrCreateProfile}).
|
||||||
|
* Pas besoin d'initialiser explicitement.
|
||||||
|
*/
|
||||||
public interface PlayerProfileService {
|
public interface PlayerProfileService {
|
||||||
|
|
||||||
PlayerProfile getOrCreateProfile(UUID playerId);
|
PlayerProfile getOrCreateProfile(UUID playerId);
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package fr.luc.crcore.player;
|
||||||
|
|
||||||
|
import fr.luc.crcore.database.ColumnType;
|
||||||
|
import fr.luc.crcore.database.Database;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implémentation {@link PlayerProfileRepository} adossée à SQLite.
|
||||||
|
*
|
||||||
|
* <p>Mêmes principes que {@link fr.luc.crcore.team.SqliteTeamRepository} :
|
||||||
|
* cache mémoire en write-through, schéma créé à l'init, état rechargé depuis
|
||||||
|
* la DB au constructeur.
|
||||||
|
*
|
||||||
|
* <p>Schéma (2 tables) :
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code crcore_player_profiles} — une ligne par joueur</li>
|
||||||
|
* <li>{@code crcore_player_scores} — une ligne par (joueur, score nommé)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class SqlitePlayerProfileRepository extends InMemoryPlayerProfileRepository {
|
||||||
|
|
||||||
|
private static final String TABLE_PROFILES = "crcore_player_profiles";
|
||||||
|
private static final String TABLE_SCORES = "crcore_player_scores";
|
||||||
|
|
||||||
|
private final Database db;
|
||||||
|
|
||||||
|
public SqlitePlayerProfileRepository(Database db) {
|
||||||
|
this.db = Objects.requireNonNull(db, "db");
|
||||||
|
ensureSchema();
|
||||||
|
loadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureSchema() {
|
||||||
|
db.table(TABLE_PROFILES).ifNotExists()
|
||||||
|
.column("id", ColumnType.UUID).primaryKey()
|
||||||
|
.create();
|
||||||
|
|
||||||
|
db.table(TABLE_SCORES).ifNotExists()
|
||||||
|
.column("profile_id", ColumnType.UUID).notNull()
|
||||||
|
.column("score_name", ColumnType.TEXT).notNull()
|
||||||
|
.column("value", ColumnType.INTEGER).notNull()
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadAll() {
|
||||||
|
var ids = db.query(
|
||||||
|
"SELECT id FROM " + TABLE_PROFILES,
|
||||||
|
rs -> UUID.fromString(rs.getString("id"))
|
||||||
|
);
|
||||||
|
for (UUID id : ids) {
|
||||||
|
PlayerProfile profile = new PlayerProfile(id);
|
||||||
|
db.query(
|
||||||
|
"SELECT score_name, value FROM " + TABLE_SCORES + " WHERE profile_id = ?",
|
||||||
|
rs -> {
|
||||||
|
profile.setScore(rs.getString("score_name"), rs.getInt("value"));
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
id
|
||||||
|
);
|
||||||
|
super.save(profile); // injecte dans le cache hérité sans repasser par notre override
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PlayerProfile save(PlayerProfile profile) {
|
||||||
|
super.save(profile);
|
||||||
|
persist(profile);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean delete(UUID id) {
|
||||||
|
boolean removed = super.delete(id);
|
||||||
|
if (removed) {
|
||||||
|
db.update("DELETE FROM " + TABLE_SCORES + " WHERE profile_id = ?", id);
|
||||||
|
db.update("DELETE FROM " + TABLE_PROFILES + " WHERE id = ?", id);
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persist(PlayerProfile profile) {
|
||||||
|
db.update(
|
||||||
|
"INSERT OR REPLACE INTO " + TABLE_PROFILES + " (id) VALUES (?)",
|
||||||
|
profile.getId()
|
||||||
|
);
|
||||||
|
db.update("DELETE FROM " + TABLE_SCORES + " WHERE profile_id = ?", profile.getId());
|
||||||
|
profile.getScores().forEach((name, value) ->
|
||||||
|
db.update(
|
||||||
|
"INSERT INTO " + TABLE_SCORES + " (profile_id, score_name, value) VALUES (?, ?, ?)",
|
||||||
|
profile.getId(), name, value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package fr.luc.crcore.player.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.player.PlayerProfile;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
/** Déclenché juste après la création d'un profil (lazy ou explicite). */
|
||||||
|
public class PlayerProfileCreateEvent extends PlayerProfileEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
public PlayerProfileCreateEvent(PlayerProfile profile) {
|
||||||
|
super(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package fr.luc.crcore.player.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.player.PlayerProfile;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
/** Déclenché juste après la suppression d'un profil. */
|
||||||
|
public class PlayerProfileDeleteEvent extends PlayerProfileEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
public PlayerProfileDeleteEvent(PlayerProfile profile) {
|
||||||
|
super(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package fr.luc.crcore.player.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.player.PlayerProfile;
|
||||||
|
import org.bukkit.event.Event;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base abstraite pour tous les évènements liés à un {@link PlayerProfile}.
|
||||||
|
*
|
||||||
|
* <p>Chaque sous-classe concrète doit fournir sa propre {@code HandlerList}
|
||||||
|
* statique (contrainte Bukkit).
|
||||||
|
*/
|
||||||
|
public abstract class PlayerProfileEvent extends Event {
|
||||||
|
|
||||||
|
private final PlayerProfile profile;
|
||||||
|
|
||||||
|
protected PlayerProfileEvent(PlayerProfile profile) {
|
||||||
|
this.profile = Objects.requireNonNull(profile, "profile");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Le profil concerné. */
|
||||||
|
public PlayerProfile getProfile() {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package fr.luc.crcore.player.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.player.PlayerProfile;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenché après changement effectif d'un score joueur. {@link #getScoreName()}
|
||||||
|
* donne le nom du score touché.
|
||||||
|
*/
|
||||||
|
public class PlayerScoreChangeEvent extends PlayerProfileEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
private final String scoreName;
|
||||||
|
private final int oldValue;
|
||||||
|
private final int newValue;
|
||||||
|
|
||||||
|
public PlayerScoreChangeEvent(PlayerProfile profile, String scoreName, int oldValue, int newValue) {
|
||||||
|
super(profile);
|
||||||
|
this.scoreName = Objects.requireNonNull(scoreName, "scoreName");
|
||||||
|
this.oldValue = oldValue;
|
||||||
|
this.newValue = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getScoreName() { return scoreName; }
|
||||||
|
public int getOldValue() { return oldValue; }
|
||||||
|
public int getNewValue() { return newValue; }
|
||||||
|
public int getDelta() { return newValue - oldValue; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package fr.luc.crcore.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.event.PlayerJoinTeamEvent;
|
||||||
|
import fr.luc.crcore.team.event.TeamCreateEvent;
|
||||||
|
import fr.luc.crcore.team.event.TeamDissolveEvent;
|
||||||
|
import fr.luc.crcore.team.event.TeamLeadershipTransferEvent;
|
||||||
|
import fr.luc.crcore.team.event.TeamMemberAddEvent;
|
||||||
|
import fr.luc.crcore.team.event.TeamMemberRemoveEvent;
|
||||||
|
import fr.luc.crcore.team.event.TeamScoreChangeEvent;
|
||||||
|
import fr.luc.crcore.team.event.TeamSpawnPointChangeEvent;
|
||||||
|
import fr.luc.crcore.team.event.TeamVisibilityChangeEvent;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variante de {@link TeamServiceImpl} qui, en plus de la logique métier,
|
||||||
|
* <b>tire des évènements Bukkit</b> via les hooks {@code on...} hérités.
|
||||||
|
*
|
||||||
|
* <p>C'est cette implémentation qu'utilise {@code CRCore} par défaut. Les
|
||||||
|
* plugins de jeu peuvent toujours sous-classer pour ajouter d'autres effets
|
||||||
|
* (logs, scoreboard sync, etc.) en overridant les mêmes hooks et en appelant
|
||||||
|
* {@code super}.
|
||||||
|
*/
|
||||||
|
public class BukkitEventFiringTeamServiceImpl extends TeamServiceImpl {
|
||||||
|
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
|
||||||
|
public BukkitEventFiringTeamServiceImpl(JavaPlugin plugin, TeamRepository repository) {
|
||||||
|
super(repository);
|
||||||
|
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JavaPlugin getPlugin() {
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onAfterCreate(Team team) {
|
||||||
|
Bukkit.getPluginManager().callEvent(new TeamCreateEvent(team));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onAfterDissolve(Team team) {
|
||||||
|
Bukkit.getPluginManager().callEvent(new TeamDissolveEvent(team));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMemberAdded(Team team, TeamMember member) {
|
||||||
|
Bukkit.getPluginManager().callEvent(new TeamMemberAddEvent(team, member));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMemberRemoved(Team team, UUID playerId) {
|
||||||
|
Bukkit.getPluginManager().callEvent(new TeamMemberRemoveEvent(team, playerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPlayerJoined(Team team, TeamMember member) {
|
||||||
|
Bukkit.getPluginManager().callEvent(new PlayerJoinTeamEvent(team, member));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLeadershipTransferred(Team team, UUID oldLeaderId, UUID newLeaderId) {
|
||||||
|
Bukkit.getPluginManager().callEvent(
|
||||||
|
new TeamLeadershipTransferEvent(team, oldLeaderId, newLeaderId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onVisibilityChanged(Team team, TeamVisibility oldValue, TeamVisibility newValue) {
|
||||||
|
Bukkit.getPluginManager().callEvent(
|
||||||
|
new TeamVisibilityChangeEvent(team, oldValue, newValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onScoreChanged(Team team, String scoreName, int oldValue, int newValue) {
|
||||||
|
Bukkit.getPluginManager().callEvent(
|
||||||
|
new TeamScoreChangeEvent(team, scoreName, oldValue, newValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSpawnPointChanged(Team team, Location oldLocation, Location newLocation) {
|
||||||
|
Bukkit.getPluginManager().callEvent(
|
||||||
|
new TeamSpawnPointChangeEvent(team, oldLocation, newLocation));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package fr.luc.crcore.team;
|
||||||
|
|
||||||
|
import fr.luc.crcore.database.ColumnType;
|
||||||
|
import fr.luc.crcore.database.Database;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implémentation {@link TeamRepository} adossée à SQLite.
|
||||||
|
*
|
||||||
|
* <p>Stratégie : <b>write-through cache</b>. On hérite de
|
||||||
|
* {@link InMemoryTeamRepository} pour conserver les requêtes rapides
|
||||||
|
* (findAll, findByName, etc. en mémoire), et on overrride {@link #save} et
|
||||||
|
* {@link #delete} pour persister synchroniquement vers SQLite. Le constructeur
|
||||||
|
* crée les tables si nécessaire et recharge l'état complet depuis la DB.
|
||||||
|
*
|
||||||
|
* <p>Schéma (3 tables) :
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code crcore_teams} — une ligne par équipe (champs scalaires)</li>
|
||||||
|
* <li>{@code crcore_team_members} — une ligne par membre</li>
|
||||||
|
* <li>{@code crcore_team_scores} — une ligne par (équipe, score nommé)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Sur {@link #save}, on supprime puis ré-insère membres et scores de
|
||||||
|
* l'équipe (approche simple et robuste pour des volumes faibles d'event).
|
||||||
|
*/
|
||||||
|
public class SqliteTeamRepository extends InMemoryTeamRepository {
|
||||||
|
|
||||||
|
private static final String TABLE_TEAMS = "crcore_teams";
|
||||||
|
private static final String TABLE_MEMBERS = "crcore_team_members";
|
||||||
|
private static final String TABLE_SCORES = "crcore_team_scores";
|
||||||
|
|
||||||
|
private final Database db;
|
||||||
|
|
||||||
|
public SqliteTeamRepository(Database db) {
|
||||||
|
this.db = Objects.requireNonNull(db, "db");
|
||||||
|
ensureSchema();
|
||||||
|
loadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureSchema() {
|
||||||
|
db.table(TABLE_TEAMS).ifNotExists()
|
||||||
|
.column("id", ColumnType.UUID).primaryKey()
|
||||||
|
.column("name", ColumnType.TEXT).notNull().unique()
|
||||||
|
.column("tag", ColumnType.TEXT).notNull().unique()
|
||||||
|
.column("color", ColumnType.TEXT).notNull()
|
||||||
|
.column("leader_id", ColumnType.UUID).notNull()
|
||||||
|
.column("visibility", ColumnType.TEXT).notNull()
|
||||||
|
.column("spawn_world", ColumnType.TEXT)
|
||||||
|
.column("spawn_x", ColumnType.REAL)
|
||||||
|
.column("spawn_y", ColumnType.REAL)
|
||||||
|
.column("spawn_z", ColumnType.REAL)
|
||||||
|
.column("spawn_yaw", ColumnType.REAL)
|
||||||
|
.column("spawn_pitch", ColumnType.REAL)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
db.table(TABLE_MEMBERS).ifNotExists()
|
||||||
|
.column("team_id", ColumnType.UUID).notNull()
|
||||||
|
.column("player_id", ColumnType.UUID).notNull()
|
||||||
|
.column("role", ColumnType.TEXT).notNull()
|
||||||
|
.column("joined_at", ColumnType.INTEGER).notNull()
|
||||||
|
.create();
|
||||||
|
// Index logique (team_id, player_id) — pas créé explicitement, SQLite gère via lookups.
|
||||||
|
|
||||||
|
db.table(TABLE_SCORES).ifNotExists()
|
||||||
|
.column("team_id", ColumnType.UUID).notNull()
|
||||||
|
.column("score_name", ColumnType.TEXT).notNull()
|
||||||
|
.column("value", ColumnType.INTEGER).notNull()
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Recharge tous les Teams depuis la DB dans le cache mémoire hérité. */
|
||||||
|
private void loadAll() {
|
||||||
|
// On query toutes les équipes en flat puis on ré-hydrate.
|
||||||
|
var teamRows = db.query(
|
||||||
|
"SELECT id, name, tag, color, leader_id, visibility, " +
|
||||||
|
"spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch " +
|
||||||
|
"FROM " + TABLE_TEAMS,
|
||||||
|
rs -> new TeamRow(
|
||||||
|
UUID.fromString(rs.getString("id")),
|
||||||
|
rs.getString("name"),
|
||||||
|
rs.getString("tag"),
|
||||||
|
TeamColor.valueOf(rs.getString("color")),
|
||||||
|
UUID.fromString(rs.getString("leader_id")),
|
||||||
|
TeamVisibility.valueOf(rs.getString("visibility")),
|
||||||
|
rs.getString("spawn_world"),
|
||||||
|
(Double) rs.getObject("spawn_x"),
|
||||||
|
(Double) rs.getObject("spawn_y"),
|
||||||
|
(Double) rs.getObject("spawn_z"),
|
||||||
|
(Float) (rs.getObject("spawn_yaw") == null ? null : (float) rs.getDouble("spawn_yaw")),
|
||||||
|
(Float) (rs.getObject("spawn_pitch") == null ? null : (float) rs.getDouble("spawn_pitch"))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (TeamRow row : teamRows) {
|
||||||
|
Team team = new Team(row.id, row.name, row.tag, row.color, row.leaderId, row.visibility);
|
||||||
|
// Membres
|
||||||
|
var members = db.query(
|
||||||
|
"SELECT player_id, role, joined_at FROM " + TABLE_MEMBERS + " WHERE team_id = ?",
|
||||||
|
rs -> new MemberRow(
|
||||||
|
UUID.fromString(rs.getString("player_id")),
|
||||||
|
TeamRole.valueOf(rs.getString("role")),
|
||||||
|
Instant.ofEpochMilli(rs.getLong("joined_at"))
|
||||||
|
),
|
||||||
|
row.id
|
||||||
|
);
|
||||||
|
// Le leader est ajouté par le constructeur de Team avec role LEADER.
|
||||||
|
// On ajoute les autres membres manuellement via addMember (qui les marque MEMBER).
|
||||||
|
for (MemberRow m : members) {
|
||||||
|
if (!m.playerId.equals(row.leaderId)) {
|
||||||
|
team.addMember(m.playerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Scores
|
||||||
|
db.query(
|
||||||
|
"SELECT score_name, value FROM " + TABLE_SCORES + " WHERE team_id = ?",
|
||||||
|
rs -> {
|
||||||
|
team.setScore(rs.getString("score_name"), rs.getInt("value"));
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
row.id
|
||||||
|
);
|
||||||
|
// Spawn point — différé : nécessite un World qui n'est pas forcément chargé
|
||||||
|
// au moment du load. Le serveur charge les worlds avant les plugins normalement,
|
||||||
|
// mais on est défensif.
|
||||||
|
if (row.spawnWorld != null && row.spawnX != null) {
|
||||||
|
var world = org.bukkit.Bukkit.getWorld(row.spawnWorld);
|
||||||
|
if (world != null) {
|
||||||
|
org.bukkit.Location loc = new org.bukkit.Location(
|
||||||
|
world, row.spawnX, row.spawnY, row.spawnZ);
|
||||||
|
if (row.spawnYaw != null) loc.setYaw(row.spawnYaw);
|
||||||
|
if (row.spawnPitch != null) loc.setPitch(row.spawnPitch);
|
||||||
|
team.setSpawnPoint(loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Inject dans le cache mémoire hérité (super.save persiste à nouveau — on évite ça).
|
||||||
|
super.save(team);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Team save(Team team) {
|
||||||
|
super.save(team); // met à jour le cache mémoire
|
||||||
|
persist(team);
|
||||||
|
return team;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean delete(UUID id) {
|
||||||
|
boolean removed = super.delete(id);
|
||||||
|
if (removed) {
|
||||||
|
db.update("DELETE FROM " + TABLE_SCORES + " WHERE team_id = ?", id);
|
||||||
|
db.update("DELETE FROM " + TABLE_MEMBERS + " WHERE team_id = ?", id);
|
||||||
|
db.update("DELETE FROM " + TABLE_TEAMS + " WHERE id = ?", id);
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persist(Team team) {
|
||||||
|
var spawn = team.getSpawnPoint();
|
||||||
|
String spawnWorld = spawn.map(l -> l.getWorld().getName()).orElse(null);
|
||||||
|
Double spawnX = spawn.map(org.bukkit.Location::getX).orElse(null);
|
||||||
|
Double spawnY = spawn.map(org.bukkit.Location::getY).orElse(null);
|
||||||
|
Double spawnZ = spawn.map(org.bukkit.Location::getZ).orElse(null);
|
||||||
|
Float spawnYaw = spawn.map(org.bukkit.Location::getYaw).orElse(null);
|
||||||
|
Float spawnPitch = spawn.map(org.bukkit.Location::getPitch).orElse(null);
|
||||||
|
|
||||||
|
// INSERT OR REPLACE = upsert simple sur SQLite (la PK gère la collision).
|
||||||
|
db.update(
|
||||||
|
"INSERT OR REPLACE INTO " + TABLE_TEAMS +
|
||||||
|
" (id, name, tag, color, leader_id, visibility, " +
|
||||||
|
" spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch) " +
|
||||||
|
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
team.getId(), team.getName(), team.getTag(), team.getColor(),
|
||||||
|
team.getLeaderId(), team.getVisibility(),
|
||||||
|
spawnWorld, spawnX, spawnY, spawnZ, spawnYaw, spawnPitch
|
||||||
|
);
|
||||||
|
|
||||||
|
// Approche simple : on remplace en bloc les membres et les scores.
|
||||||
|
db.update("DELETE FROM " + TABLE_MEMBERS + " WHERE team_id = ?", team.getId());
|
||||||
|
for (TeamMember m : team.getMembers()) {
|
||||||
|
db.update(
|
||||||
|
"INSERT INTO " + TABLE_MEMBERS +
|
||||||
|
" (team_id, player_id, role, joined_at) VALUES (?, ?, ?, ?)",
|
||||||
|
team.getId(), m.getPlayerId(), m.getRole(), m.getJoinedAt().toEpochMilli()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update("DELETE FROM " + TABLE_SCORES + " WHERE team_id = ?", team.getId());
|
||||||
|
team.getScores().forEach((name, value) ->
|
||||||
|
db.update(
|
||||||
|
"INSERT INTO " + TABLE_SCORES + " (team_id, score_name, value) VALUES (?, ?, ?)",
|
||||||
|
team.getId(), name, value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tuples internes pour le load.
|
||||||
|
private record TeamRow(
|
||||||
|
UUID id, String name, String tag, TeamColor color,
|
||||||
|
UUID leaderId, TeamVisibility visibility,
|
||||||
|
String spawnWorld, Double spawnX, Double spawnY, Double spawnZ,
|
||||||
|
Float spawnYaw, Float spawnPitch
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private record MemberRow(UUID playerId, TeamRole role, Instant joinedAt) {}
|
||||||
|
}
|
||||||
@@ -14,6 +14,21 @@ import java.util.Optional;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Représente une équipe de joueurs. Aggregate mutable : ajout / retrait de
|
||||||
|
* membres, transfert de leadership, mise à jour de visibilité, des scores
|
||||||
|
* nommés, et du point de spawn passent par les méthodes de cette classe.
|
||||||
|
*
|
||||||
|
* <p>L'identité d'une équipe est son UUID ({@link #getId()}). Deux Team avec
|
||||||
|
* le même UUID sont égales, quel que soit le reste de leur état.
|
||||||
|
*
|
||||||
|
* <p>Implémente {@link Named} (a un nom) et {@link ScoreHolder} (porte des
|
||||||
|
* scores nommés). Hérite de {@link AbstractEntity} pour l'identité.
|
||||||
|
*
|
||||||
|
* <p>Toutes les modifications passent normalement par le {@link TeamService},
|
||||||
|
* qui orchestre persistance + hooks + évènements Bukkit. Modifier une instance
|
||||||
|
* directement (ex. {@code team.addMember(...)}) court-circuite la persistance.
|
||||||
|
*/
|
||||||
public class Team extends AbstractEntity implements Named, ScoreHolder {
|
public class Team extends AbstractEntity implements Named, ScoreHolder {
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|||||||
@@ -8,6 +8,19 @@ import java.util.Optional;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Façade pour toutes les opérations sur les équipes : lifecycle (create /
|
||||||
|
* dissolve), membres, scores, classements, point de spawn, visibilité.
|
||||||
|
*
|
||||||
|
* <p>Toute logique d'écriture passe par le service (jamais directement sur
|
||||||
|
* {@link Team}) — il garantit l'unicité nom/tag, déclenche les hooks
|
||||||
|
* d'override et tire les évènements Bukkit (via la sous-classe par défaut
|
||||||
|
* {@code BukkitEventFiringTeamServiceImpl}).
|
||||||
|
*
|
||||||
|
* <p>L'implémentation par défaut est {@link TeamServiceImpl} avec ses ~12
|
||||||
|
* hooks {@code protected} surchargeables (factories {@code newTeam},
|
||||||
|
* {@code newRanking}, et hooks {@code on...} autour de chaque opération).
|
||||||
|
*/
|
||||||
public interface TeamService {
|
public interface TeamService {
|
||||||
|
|
||||||
// ---- Lifecycle ----
|
// ---- Lifecycle ----
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package fr.luc.crcore.team.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamMember;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenché spécifiquement quand un joueur rejoint une équipe <b>par sa propre
|
||||||
|
* action</b> (auto-join sur une équipe PUBLIC via {@code TeamService.joinTeam}).
|
||||||
|
*
|
||||||
|
* <p>Dans ce cas, {@link TeamMemberAddEvent} est <i>aussi</i> tiré juste avant.
|
||||||
|
* Pour réagir uniquement aux auto-joins, écouter ce {@code PlayerJoinTeamEvent}.
|
||||||
|
*/
|
||||||
|
public class PlayerJoinTeamEvent extends TeamEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
private final TeamMember member;
|
||||||
|
|
||||||
|
public PlayerJoinTeamEvent(Team team, TeamMember member) {
|
||||||
|
super(team);
|
||||||
|
this.member = Objects.requireNonNull(member, "member");
|
||||||
|
}
|
||||||
|
|
||||||
|
public TeamMember getMember() {
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package fr.luc.crcore.team.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenché juste après qu'une équipe a été créée et persistée. Non annulable
|
||||||
|
* (la validation est faite côté service avant création).
|
||||||
|
*/
|
||||||
|
public class TeamCreateEvent extends TeamEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
public TeamCreateEvent(Team team) {
|
||||||
|
super(team);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package fr.luc.crcore.team.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
/** Déclenché juste après la dissolution d'une équipe. */
|
||||||
|
public class TeamDissolveEvent extends TeamEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
public TeamDissolveEvent(Team team) {
|
||||||
|
super(team);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package fr.luc.crcore.team.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import org.bukkit.event.Event;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base abstraite pour tous les évènements Bukkit liés à une équipe. Porte
|
||||||
|
* toujours la {@link Team} concernée.
|
||||||
|
*
|
||||||
|
* <p>Chaque sous-classe concrète doit fournir sa propre {@code HandlerList}
|
||||||
|
* statique (contrainte Bukkit — pas de moyen de partager via héritage).
|
||||||
|
*/
|
||||||
|
public abstract class TeamEvent extends Event {
|
||||||
|
|
||||||
|
private final Team team;
|
||||||
|
|
||||||
|
protected TeamEvent(Team team) {
|
||||||
|
this.team = Objects.requireNonNull(team, "team");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** L'équipe concernée par l'évènement. */
|
||||||
|
public Team getTeam() {
|
||||||
|
return team;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package fr.luc.crcore.team.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/** Déclenché après un transfert de leadership. {@link #getOldLeaderId()} et {@link #getNewLeaderId()} renvoient les UUID des deux joueurs. */
|
||||||
|
public class TeamLeadershipTransferEvent extends TeamEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
private final UUID oldLeaderId;
|
||||||
|
private final UUID newLeaderId;
|
||||||
|
|
||||||
|
public TeamLeadershipTransferEvent(Team team, UUID oldLeaderId, UUID newLeaderId) {
|
||||||
|
super(team);
|
||||||
|
this.oldLeaderId = Objects.requireNonNull(oldLeaderId, "oldLeaderId");
|
||||||
|
this.newLeaderId = Objects.requireNonNull(newLeaderId, "newLeaderId");
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getOldLeaderId() { return oldLeaderId; }
|
||||||
|
public UUID getNewLeaderId() { return newLeaderId; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package fr.luc.crcore.team.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamMember;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenché quand un membre est ajouté à une équipe — par action du chef
|
||||||
|
* ({@code TeamService.addMember}) OU par auto-join du joueur
|
||||||
|
* ({@code TeamService.joinTeam}).
|
||||||
|
*
|
||||||
|
* <p>Pour distinguer les deux cas, écouter aussi {@link PlayerJoinTeamEvent}
|
||||||
|
* qui n'est tiré que dans le second cas.
|
||||||
|
*/
|
||||||
|
public class TeamMemberAddEvent extends TeamEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
private final TeamMember member;
|
||||||
|
|
||||||
|
public TeamMemberAddEvent(Team team, TeamMember member) {
|
||||||
|
super(team);
|
||||||
|
this.member = Objects.requireNonNull(member, "member");
|
||||||
|
}
|
||||||
|
|
||||||
|
public TeamMember getMember() {
|
||||||
|
return member;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package fr.luc.crcore.team.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/** Déclenché après le retrait d'un membre d'une équipe (action chef ou départ volontaire). */
|
||||||
|
public class TeamMemberRemoveEvent extends TeamEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
private final UUID playerId;
|
||||||
|
|
||||||
|
public TeamMemberRemoveEvent(Team team, UUID playerId) {
|
||||||
|
super(team);
|
||||||
|
this.playerId = Objects.requireNonNull(playerId, "playerId");
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getPlayerId() {
|
||||||
|
return playerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package fr.luc.crcore.team.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenché après changement effectif d'un score d'équipe (uniquement si la
|
||||||
|
* valeur change). {@link #getScoreName()} donne le nom du score touché.
|
||||||
|
*/
|
||||||
|
public class TeamScoreChangeEvent extends TeamEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
private final String scoreName;
|
||||||
|
private final int oldValue;
|
||||||
|
private final int newValue;
|
||||||
|
|
||||||
|
public TeamScoreChangeEvent(Team team, String scoreName, int oldValue, int newValue) {
|
||||||
|
super(team);
|
||||||
|
this.scoreName = Objects.requireNonNull(scoreName, "scoreName");
|
||||||
|
this.oldValue = oldValue;
|
||||||
|
this.newValue = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getScoreName() { return scoreName; }
|
||||||
|
public int getOldValue() { return oldValue; }
|
||||||
|
public int getNewValue() { return newValue; }
|
||||||
|
public int getDelta() { return newValue - oldValue; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package fr.luc.crcore.team.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenché après changement du point de spawn d'une équipe. {@code oldLocation}
|
||||||
|
* et {@code newLocation} peuvent être {@code null} (clear / setup initial).
|
||||||
|
*/
|
||||||
|
public class TeamSpawnPointChangeEvent extends TeamEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
private final Location oldLocation;
|
||||||
|
private final Location newLocation;
|
||||||
|
|
||||||
|
public TeamSpawnPointChangeEvent(Team team, Location oldLocation, Location newLocation) {
|
||||||
|
super(team);
|
||||||
|
this.oldLocation = oldLocation;
|
||||||
|
this.newLocation = newLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** L'ancien spawn, ou {@code null} s'il n'y en avait pas. */
|
||||||
|
public Location getOldLocation() { return oldLocation; }
|
||||||
|
|
||||||
|
/** Le nouveau spawn, ou {@code null} si on a fait un clear. */
|
||||||
|
public Location getNewLocation() { return newLocation; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package fr.luc.crcore.team.event;
|
||||||
|
|
||||||
|
import fr.luc.crcore.team.Team;
|
||||||
|
import fr.luc.crcore.team.TeamVisibility;
|
||||||
|
import org.bukkit.event.HandlerList;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/** Déclenché après changement effectif de visibilité PUBLIC ↔ PRIVATE. */
|
||||||
|
public class TeamVisibilityChangeEvent extends TeamEvent {
|
||||||
|
|
||||||
|
private static final HandlerList HANDLERS = new HandlerList();
|
||||||
|
|
||||||
|
private final TeamVisibility oldVisibility;
|
||||||
|
private final TeamVisibility newVisibility;
|
||||||
|
|
||||||
|
public TeamVisibilityChangeEvent(Team team, TeamVisibility oldVisibility, TeamVisibility newVisibility) {
|
||||||
|
super(team);
|
||||||
|
this.oldVisibility = Objects.requireNonNull(oldVisibility, "oldVisibility");
|
||||||
|
this.newVisibility = Objects.requireNonNull(newVisibility, "newVisibility");
|
||||||
|
}
|
||||||
|
|
||||||
|
public TeamVisibility getOldVisibility() { return oldVisibility; }
|
||||||
|
public TeamVisibility getNewVisibility() { return newVisibility; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HandlerList getHandlers() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HandlerList getHandlerList() {
|
||||||
|
return HANDLERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user