4efaa5bbde
New feature fr.luc.crcore.features.moderation, opt-in via CRCoreConfig.setupModeration() (also enabled by setupAll()). Core abstractions: - ModerationState: full player snapshot (inv + armor + offhand, XP, health, food, location, gamemode, allowFlight/flying, walk/fly speed). Immutable, restoreTo(player) restores everything. - ModerationService interface + ModerationServiceImpl (with protected onAfterEnter/onAfterExit hooks) + BukkitEventFiringModerationServiceImpl (fires ModerationEnterEvent / ModerationExitEvent). - ModerationRepository interface + InMemoryModerationRepository (skeleton — SQLite impl with BukkitObjectOutputStream serialization planned). - ModeratorTool interface: getKey/getSlot(0..8)/buildIcon + onLeftClick/onRightClick/onInteractEntity. ModeratorToolRegistry preserves registration order, slot collision = replace. - Exceptions: ModerationException base + AlreadyActive + NotActive. - Events: ModerationEvent base + Enter + Exit. 5 skeleton tools in the hotbar: - slot 0: TeleportRandomPlayerTool (compass, right-click → tp random) - slot 1: InventorySpyTool (chest, right-click on player → open inv) - slot 2: FreezeTool (ice, right-click on player → toggle freeze) - slot 7: VanishToggleTool (ender eye, click → toggle vanish) - slot 8: ExitTool (barrier, click → exit mod mode) Slots 3-6 free for custom tools. ModerationListener routes interactions and locks hotbar: - PlayerInteractEvent → tool.onLeftClick / onRightClick (with cancel). - PlayerInteractEntityEvent → tool.onInteractEntity (with cancel). - PlayerDropItemEvent / PlayerSwapHandItemsEvent: cancel for mods. - InventoryClickEvent: cancel only when top inv is the mod's own inv (preserves InventorySpyTool's ability to manipulate target's inv). - PlayerJoinEvent: re-applies vanish for already-vanished mods. - PlayerQuitEvent: cleanup freeze state. - PlayerMoveEvent: cancel block-position changes for frozen players, keeping head rotation free. Mod mode lifecycle: - enter: snapshot + clear inv + populate hotbar + CREATIVE + allowFlight + vanish + ModerationEnterEvent. - exit: state.restoreTo(player) + unvanish + unfreeze + repo delete + ModerationExitEvent. /core admin (perm crcore.admin, player-only): toggle on/off. Messages moderation.enter.success / moderation.exit.success added to crcore-messages.yml. CRCoreConfig.setupModeration() + isModerationEnabled() flag. CRCore: buildModerationService() and registerDefaultModeratorTools() override points, moderation() / getModerationService() getters with IllegalStateException guard. Builds + registers ModerationListener at enable() when feature on. CoreCommand extended to take ModerationService; registers AdminToggleSubCommand only when service non-null. Skeleton limitations documented in features.md: - In-memory repo only (server crash = lost inv) — SQLite planned. - InventorySpyTool opens real inv (no read-only wrapping yet). - TeleportRandomPlayerTool is a placeholder for a future player-picker GUI. - No moderation.*.broadcast keys yet. - No /core admin <player> (self-toggle only). Docs: section 11 in features.md, decision logged in decisions.md (skeleton scope + rationale), setup.md snippet updated, new moderation-class-diagram.puml, README.md updated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1091 lines
43 KiB
Markdown
1091 lines
43 KiB
Markdown
# Domaines fonctionnels
|
|
|
|
CR-Core est une librairie. Le plugin de jeu downstream l'instancie en une
|
|
ligne via `new CRCore(this).enable()` dans son `onEnable()`, et tout est
|
|
branché : SQLite, services team + player, commandes `/core team ...`,
|
|
évènements Bukkit.
|
|
|
|
Architecture des domaines :
|
|
|
|
1. **Team** — équipes (membres, leader, visibilité, scores, classements, spawn)
|
|
2. **Player** — profils joueurs (scores nommés, classements individuels)
|
|
3. **Framework de commandes** — `BaseCommand` / `SubCommand` imbriqués
|
|
4. **Commandes built-in** — `/core team [create|delete|add|remove|join|leave|...]`
|
|
5. **Évènements Bukkit** — 9 events team + 3 events player
|
|
6. **Database** — wrapper SQLite + table builder pour les plugins downstream
|
|
7. **Bootstrap** — classe `CRCore` qui câble tout
|
|
|
|
---
|
|
|
|
## 1. Domaine Team
|
|
|
|
**Statut** : modèle + service implémenté (repository en mémoire). Overridable
|
|
étape par étape via hooks et factories.
|
|
|
|
### Définition d'une équipe (`Team`)
|
|
|
|
| Attribut | Type | Description |
|
|
|---|---|---|
|
|
| `id` | `UUID` | Identifiant interne unique, généré automatiquement. |
|
|
| `name` | `String` | Nom lisible. Unique (case-insensitive). |
|
|
| `tag` | `String` | Tag court (le « # »). Unique. Affiché entre `[# … ]`. |
|
|
| `color` | `TeamColor` | Couleur associée (enum). |
|
|
| `leaderId` | `UUID` *(nullable)* | Identifiant du joueur **chef d'équipe**. Peut être `null` (équipe leaderless — typique après création par admin). |
|
|
| `visibility` | `TeamVisibility` | `PUBLIC` (les joueurs peuvent rejoindre) ou `PRIVATE` (seul le chef ajoute). Défaut : `PRIVATE`. |
|
|
| `members` | `Set<TeamMember>` | Ensemble des membres (inclut le chef). |
|
|
|
|
### Membre (`TeamMember`)
|
|
|
|
| Attribut | Type | Description |
|
|
|---|---|---|
|
|
| `playerId` | `UUID` | UUID du joueur Bukkit (= `id` de l'entité). |
|
|
| `role` | `TeamRole` | `LEADER` ou `MEMBER`. |
|
|
| `joinedAt` | `Instant` | Date d'entrée. |
|
|
|
|
### Enums
|
|
|
|
- **`TeamRole`** : `LEADER`, `MEMBER`.
|
|
- **`TeamVisibility`** : `PUBLIC`, `PRIVATE`. Méthodes `isPublic()`, `isPrivate()`.
|
|
- **`TeamColor`** : 16 valeurs, chacune expose `ChatColor`, `DyeColor`,
|
|
`displayName`.
|
|
|
|
### Règles d'intégrité
|
|
|
|
1. Une équipe a **exactement un chef** à tout instant.
|
|
2. Un joueur appartient à **au plus une équipe** (au sein du registre du
|
|
plugin de jeu — chaque plugin a son propre registre).
|
|
3. `name` et `tag` sont uniques (case-insensitive) dans le registre.
|
|
4. Le chef ne peut pas être retiré sans `transferLeadership` préalable.
|
|
5. Une équipe `PRIVATE` ne peut être rejointe que via `addMember` (action du
|
|
chef) ; une équipe `PUBLIC` peut être rejointe via `joinTeam` (action du
|
|
joueur lui-même).
|
|
|
|
### Opérations (`TeamService`)
|
|
|
|
| Opération | Description |
|
|
|---|---|
|
|
| `createTeam(name, tag, color)` | Crée une équipe **leaderless** en `PRIVATE`. |
|
|
| `createTeam(name, tag, color, visibility)` | Crée une équipe **leaderless** avec la visibilité spécifiée. |
|
|
| `createTeam(name, tag, color, leaderId)` | Crée une équipe `PRIVATE` avec ce joueur comme chef. |
|
|
| `createTeam(name, tag, color, leaderId, visibility)` | Surcharge complète. `leaderId` peut être `null` (équivalent leaderless). |
|
|
| `dissolveTeam(teamId)` | Supprime l'équipe. |
|
|
| `addMember(teamId, playerId)` | **Action du chef** : ajoute un joueur comme `MEMBER` (marche en PUBLIC comme en PRIVATE). |
|
|
| `removeMember(teamId, playerId)` | Retire un joueur (interdit sur le chef). |
|
|
| `joinTeam(teamId, playerId)` | **Action du joueur** : auto-rejoindre une équipe `PUBLIC`. Lève `TeamAccessException` si la team est `PRIVATE` ou si le joueur est déjà dans une équipe. |
|
|
| `transferLeadership(teamId, newLeaderId)` | Transfert chef→chef strict : le nouveau chef doit être membre, et la team doit avoir un chef actuel. |
|
|
| `setLeader(teamId, newLeaderId)` | Plus permissif : fonctionne sur team leaderless **et** auto-ajoute le joueur s'il n'est pas membre. C'est l'opération admin. |
|
|
| `setVisibility(teamId, visibility)` | Change la visibilité (typiquement appelée par le chef). |
|
|
| `addScore(teamId, name, delta)` / `setScore` / `getScore` / `resetScore` / `resetAllScores` | Gestion des scores (voir section Scores). |
|
|
| `getRankingByScore(name)` / `getGlobalRanking()` / `getTopRankingByScore(name, n)` / `getTopGlobalRanking(n)` | Classements (voir section Classements). |
|
|
| `setSpawnPoint(teamId, loc)` / `clearSpawnPoint(teamId)` / `getSpawnPoint(teamId)` | Point de spawn (voir section Spawn). |
|
|
| `getTeam` / `getTeamByName` / `getTeamByTag` / `getTeamOfPlayer` | Recherches. |
|
|
| `getAllTeams()` | Toutes les équipes. |
|
|
|
|
### Scores
|
|
|
|
Chaque équipe porte une `Map<String, Integer>` de scores nommés. Le nom du
|
|
score est libre (`"kills"`, `"objectives"`, `"global"`, …). Un jeu qui n'a
|
|
besoin que d'un seul score peut utiliser un nom unique (typiquement
|
|
`"global"`).
|
|
|
|
| Opération sur `Team` | Description |
|
|
|---|---|
|
|
| `getScore(name)` | Valeur courante (0 si jamais set). |
|
|
| `hasScore(name)` | `true` si le score a été initialisé au moins une fois. |
|
|
| `getScores()` | Map immuable de tous les scores. |
|
|
| `getTotalScore()` | Somme de tous les scores (utilisée pour le classement global). |
|
|
| `addScore(name, delta)` | Incrémente (ou décrémente avec un delta négatif), renvoie la nouvelle valeur. |
|
|
| `setScore(name, value)` | Affecte une valeur absolue. |
|
|
| `resetScore(name)` | Supprime un score (revient à 0). |
|
|
| `resetAllScores()` | Vide tous les scores. |
|
|
|
|
Côté `TeamService` : mêmes opérations préfixées du `teamId` (`addScore(teamId,
|
|
name, delta)` etc.). Hook `onScoreChanged(team, name, oldValue, newValue)`
|
|
appelé uniquement quand la valeur change réellement.
|
|
|
|
### Classements (rankings)
|
|
|
|
Le service expose deux types de classements :
|
|
|
|
| Méthode | Description |
|
|
|---|---|
|
|
| `getRankingByScore(name)` | Classement par un score précis (descendant). |
|
|
| `getGlobalRanking()` | Classement par la somme des scores de chaque équipe. |
|
|
| `getTopRankingByScore(name, n)` | Top N par score. |
|
|
| `getTopGlobalRanking(n)` | Top N global. |
|
|
|
|
Le résultat est une `List<TeamRanking>` (classe immutable, accesseurs `rank()`/`team()`/`score()`) avec `rank` (1-based),
|
|
`team` et `score`. Tiebreaker : ordre alphabétique sur le nom de l'équipe
|
|
(insensible à la casse).
|
|
|
|
La méthode `protected rank(ToIntFunction<Team>)` est exposée pour permettre à
|
|
une sous-classe de créer ses propres classements (ex. score pondéré, ratio
|
|
kills/deaths, score borné). La factory `newRanking(rank, team, score)`
|
|
permet de retourner un type custom étendant `TeamRanking` (les records étant
|
|
final, on peut wrapper plutôt qu'étendre).
|
|
|
|
### Spawn point
|
|
|
|
Chaque équipe peut avoir un `Location` Bukkit comme point de spawn. Optionnel
|
|
(défaut : pas de spawn défini).
|
|
|
|
| Opération sur `Team` | Description |
|
|
|---|---|
|
|
| `getSpawnPoint()` | `Optional<Location>` (clonée — modifier l'objet retourné n'affecte pas l'équipe). |
|
|
| `hasSpawnPoint()` | `true` si un spawn a été défini. |
|
|
| `setSpawnPoint(location)` | Définit le spawn (cloné à l'entrée). `null` accepté = clear. |
|
|
| `clearSpawnPoint()` | Supprime le spawn. |
|
|
|
|
Côté `TeamService` : `setSpawnPoint(teamId, location)`,
|
|
`clearSpawnPoint(teamId)`, `getSpawnPoint(teamId)`. Hook
|
|
`onSpawnPointChanged(team, oldLocation, newLocation)`.
|
|
|
|
> **Persistance** : `Location` référence un `World` Bukkit, donc ce n'est pas
|
|
> trivialement sérialisable. Pour l'instant on stocke en mémoire ; quand on
|
|
> branchera une persistance fichier, on utilisera `Location.serialize()` /
|
|
> `deserialize()` de l'API Bukkit (`ConfigurationSerializable`).
|
|
|
|
### Hooks d'override (sur `TeamServiceImpl`)
|
|
|
|
| Hook | Quand |
|
|
|---|---|
|
|
| `newTeam(id, name, tag, color, leaderId, visibility)` | Factory — instancier une sous-classe de `Team`. |
|
|
| `newRanking(rank, team, score)` | Factory — wrapper / sous-classe de `TeamRanking`. |
|
|
| `rank(scoreFn)` | Logique de tri du classement (override pour pondération, tiebreaker custom, etc.). |
|
|
| `validateName(name)` | Avant création — règles custom sur le nom. |
|
|
| `validateTag(tag)` | Avant création — règles custom sur le tag. |
|
|
| `validateLeader(leaderId)` | Avant création — règles custom sur l'éligibilité du chef. |
|
|
| `validateJoinable(team, playerId)` | Avant `joinTeam` — règles custom. |
|
|
| `onBeforeSave(team)` | Juste avant le `repository.save`. |
|
|
| `onAfterCreate(team)` | Juste après une création réussie. |
|
|
| `onBeforeDissolve(team)` / `onAfterDissolve(team)` | Autour de la dissolution. |
|
|
| `onMemberAdded(team, member)` | Après ajout d'un membre (via `addMember` OU `joinTeam`). |
|
|
| `onMemberRemoved(team, playerId)` | Après retrait d'un membre. |
|
|
| `onPlayerJoined(team, member)` | Spécifique à `joinTeam` (en plus de `onMemberAdded`). |
|
|
| `onLeadershipTransferred(team, oldId, newId)` | Après transfert. |
|
|
| `onVisibilityChanged(team, oldV, newV)` | Après changement de visibilité. |
|
|
| `onScoreChanged(team, scoreName, oldV, newV)` | Après changement effectif d'un score. |
|
|
| `onSpawnPointChanged(team, oldLoc, newLoc)` | Après changement du point de spawn. |
|
|
|
|
Côté `Team`, factory `newMember(playerId, role)` pour utiliser un `TeamMember`
|
|
custom dans une sous-classe.
|
|
|
|
### Exceptions
|
|
|
|
- `TeamException` (base, `RuntimeException`).
|
|
- `TeamAlreadyExistsException` : nom, tag ou joueur déjà pris.
|
|
- `TeamNotFoundException` : équipe introuvable.
|
|
- `TeamAccessException` : auto-join refusé (team `PRIVATE`, joueur déjà dans
|
|
une équipe, ou règle custom dans `validateJoinable`).
|
|
|
|
### Persistance
|
|
|
|
`InMemoryTeamRepository` (Map<UUID, Team>) par défaut. Le contrat
|
|
`TeamRepository extends Repository<Team>` permet de brancher YAML / SQLite /
|
|
Postgres sans toucher au service.
|
|
|
|
### Diagrammes
|
|
|
|
- Classes : [team-class-diagram.puml](diagrams/team-class-diagram.puml)
|
|
- Séquence création : [team-create-sequence.puml](diagrams/team-create-sequence.puml)
|
|
- Séquence auto-join : [team-join-sequence.puml](diagrams/team-join-sequence.puml)
|
|
- Activité création : [team-create-activity.puml](diagrams/team-create-activity.puml)
|
|
|
|
---
|
|
|
|
## 2. Profils joueurs et scores individuels
|
|
|
|
**Statut** : modèle + service implémenté (en mémoire). Symétrique au domaine
|
|
Team pour tout ce qui touche aux scores et classements.
|
|
|
|
### Pourquoi un domaine séparé
|
|
|
|
Les scores joueur vivent en dehors de l'équipe : un joueur peut changer
|
|
d'équipe, en quitter une, en rejoindre une autre — son profil et ses scores
|
|
persistent. Le domaine `player` est indépendant du domaine `team` ; libre au
|
|
plugin de jeu de les combiner (ex. score d'équipe = somme des scores des
|
|
membres).
|
|
|
|
### `PlayerProfile`
|
|
|
|
| Attribut | Type | Description |
|
|
|---|---|---|
|
|
| `id` | `UUID` | UUID Bukkit du joueur (= `id` de l'entité). |
|
|
| `scores` | `Map<String, Integer>` | Scores nommés (`"kills"`, `"deaths"`, `"global"`, …). |
|
|
|
|
`PlayerProfile implements ScoreHolder` — la même interface que `Team`. Toutes
|
|
les méthodes de scoring (`getScore`, `addScore`, `setScore`, `resetScore`,
|
|
`resetAllScores`, `getTotalScore`, `getScores`, `hasScore`) sont identiques
|
|
à celles de `Team`.
|
|
|
|
### `PlayerProfileService`
|
|
|
|
| Opération | Description |
|
|
|---|---|
|
|
| `getOrCreateProfile(playerId)` | Retourne le profil existant ou en crée un. Utilisé en interne par les méthodes de scoring (auto-création à la première écriture). |
|
|
| `getProfile(playerId)` | `Optional<PlayerProfile>` sans création. |
|
|
| `deleteProfile(playerId)` | Supprime un profil. |
|
|
| `getAllProfiles()` | Tous les profils. |
|
|
| `addScore` / `setScore` / `getScore` / `resetScore` / `resetAllScores` | Identiques au service Team mais par `playerId`. |
|
|
| `getRankingByScore(name)` / `getGlobalRanking()` / `getTopRankingByScore(name, n)` / `getTopGlobalRanking(n)` | Classements de joueurs. |
|
|
|
|
### `PlayerRanking`
|
|
|
|
Classe immutable : `PlayerRanking(int rank, PlayerProfile profile, int score)` avec accesseurs `rank()`/`profile()`/`score()`.
|
|
Mêmes règles que `TeamRanking` (rank 1-based, tri descendant, tiebreaker par
|
|
UUID pour rester déterministe).
|
|
|
|
### Hooks d'override (sur `PlayerProfileServiceImpl`)
|
|
|
|
| Hook | Quand |
|
|
|---|---|
|
|
| `newProfile(playerId)` | Factory — instancier une sous-classe de `PlayerProfile`. |
|
|
| `newRanking(rank, profile, score)` | Factory — sous-classe de `PlayerRanking`. |
|
|
| `rank(scoreFn)` | Logique de tri (override pour pondération, tiebreaker custom, etc.). |
|
|
| `onProfileCreated(profile)` | Après création (lazy ou explicite). |
|
|
| `onProfileDeleted(profile)` | Après suppression. |
|
|
| `onScoreChanged(profile, name, oldV, newV)` | Après changement effectif d'un score. |
|
|
|
|
### Exceptions
|
|
|
|
- `PlayerException` (base, `RuntimeException`).
|
|
- `PlayerProfileNotFoundException` : profil introuvable (peu utilisé côté
|
|
service car la plupart des opérations auto-créent).
|
|
|
|
### Diagrammes
|
|
|
|
- Classes : [player-class-diagram.puml](diagrams/player-class-diagram.puml)
|
|
|
|
---
|
|
|
|
## 3. Framework de commandes
|
|
|
|
**Statut** : framework implémenté avec **sous-commandes imbriquées récursives**
|
|
(supporte `/core team create`, `/core team join`, etc.). Les commandes par
|
|
défaut sont fournies en section 4.
|
|
|
|
### Architecture
|
|
|
|
- **`Command`** (interface) — contrat partagé : `getName()`, `getAliases()`,
|
|
`getPermission()`, `isPlayerOnly()`, `getDescription()`, `execute(ctx)`,
|
|
`tabComplete(sender, args)`, `matches(label)`.
|
|
- **`AbstractCommand`** (abstract, implémente `Command`) — porte tous les
|
|
champs ET la table des sous-commandes (imbrication). Méthodes builder en
|
|
`protected` : `addAlias`, `permission`, `playerOnly`, `description`, `usage`,
|
|
`argument`, `optionalArgument`, `addSubCommand`. Routage récursif via
|
|
`dispatch(...)` et `tabComplete(...)`.
|
|
- **`BaseCommand extends AbstractCommand`** — implémente `CommandExecutor` et
|
|
`TabCompleter` de Bukkit, branche `onCommand` → `dispatch`. À utiliser pour
|
|
la racine d'un arbre (`/core`).
|
|
- **`SubCommand extends AbstractCommand`** — sous-commande ; peut être
|
|
feuille (override `execute`) ou groupe (appelle `addSubCommand` dans son
|
|
constructeur).
|
|
- **`replaceSubCommand(name, newSub)`** sur `AbstractCommand` — permet aux
|
|
plugins de jeu de remplacer une sous-commande par défaut par leur propre
|
|
implémentation (pattern standard d'override).
|
|
|
|
### Types d'arguments (`ArgumentTypes`)
|
|
|
|
| Constante / factory | Type produit | Tab-complete |
|
|
|---|---|---|
|
|
| `STRING` | `String` | (aucun) |
|
|
| `INTEGER` | `Integer` | (aucun) |
|
|
| `DOUBLE` | `Double` | (aucun) |
|
|
| `BOOLEAN` | `Boolean` | `true` / `false` |
|
|
| `ONLINE_PLAYER` | `Player` | joueurs connectés |
|
|
| `enumOf(Enum.class)` | l'enum | toutes les valeurs |
|
|
| `choice("a", "b", …)` | `String` | les choix |
|
|
|
|
Un `ArgumentType<T>` custom = une classe qui implémente `parse(String): T` et
|
|
optionnellement `suggestions(sender, partial): List<String>`.
|
|
|
|
### Résultats (`CommandResult`)
|
|
|
|
`SUCCESS`, `FAILURE`, `INVALID_USAGE`, `NO_PERMISSION`, `PLAYER_ONLY` — chacun
|
|
avec un message optionnel. Factories statiques `CommandResult.success(...)`,
|
|
`failure(...)`, `invalidUsage(...)`.
|
|
|
|
Rendus automatiquement par `BaseCommand.handleResult` avec un code couleur
|
|
standard (vert succès, rouge erreur). Override `handleResult` pour personnaliser.
|
|
|
|
### Contexte (`CommandContext`)
|
|
|
|
Wrapper passé à `execute()` :
|
|
|
|
- `getSender()`, `isPlayer()`, `getPlayer()`, `requirePlayer()`
|
|
- `get(name)` — récupère un argument parsé typé (`String name = ctx.get("name")`)
|
|
- `getOptional(name)` / `has(name)`
|
|
- `reply(message)` — raccourci `sender.sendMessage`
|
|
|
|
### Exemple complet
|
|
|
|
Voir [setup.md](setup.md#utilisation-depuis-un-plugin-de-jeu).
|
|
|
|
### Diagramme
|
|
|
|
- Classes : [command-class-diagram.puml](diagrams/command-class-diagram.puml)
|
|
|
|
---
|
|
|
|
## 4. Commandes built-in `/core team ...`
|
|
|
|
**Statut** : 14 sous-commandes prêtes à l'emploi, branchées par
|
|
`CRCore.enable()`. Chaque sous-commande vit dans
|
|
`fr.luc.crcore.command.builtin.team` et est substituable individuellement par
|
|
sous-classe ou via `replaceSubCommand`.
|
|
|
|
**Pas d'aliases courts** : les commandes ont leur nom long uniquement
|
|
(`/core team create` et pas `/core team c`).
|
|
|
|
**Modèle simplifié à 2 rôles** : toutes les opérations de gestion d'équipe
|
|
sont **admin** (perm requise + team passée en argument). Les opérations
|
|
joueur (`join`, `leave`, `info`, `list`, `top`) sont gated par permission
|
|
mais ne nécessitent pas le rôle chef. Le rôle `LEADER` reste présent dans
|
|
le modèle de données (utilisable par les game plugins via l'API) mais
|
|
n'accorde aucun privilège de commande pour l'instant.
|
|
|
|
### Arborescence
|
|
|
|
```
|
|
/core (CoreCommand)
|
|
└── team (TeamGroupSubCommand)
|
|
├── create <name> <tag> <color> [leader] [admin] créer (chef optionnel)
|
|
├── delete <team> [admin] dissoudre une équipe
|
|
├── setleader <team> <player> [admin] (re)assigner le chef
|
|
├── score <team> <name> <add|set> <value> [admin] modifier un score
|
|
├── add <team> <player> [admin] ajouter un joueur
|
|
├── remove <team> <player> [admin] retirer un joueur
|
|
├── transfer <team> <player> [admin] transfert chef→membre existant
|
|
├── visibility <team> <PUBLIC|PRIVATE> [admin] changer visibilité
|
|
├── setspawn <team> [admin] spawn à la position de l'admin
|
|
├── join <team> [joueur] rejoindre une PUBLIC
|
|
├── leave [joueur] quitter son équipe
|
|
├── info [team] [joueur] infos
|
|
├── list [joueur] toutes les équipes
|
|
└── top [score] [joueur] classement
|
|
```
|
|
|
|
### Permissions
|
|
|
|
Chaque sous-commande a une permission `crcore.team.<action>` :
|
|
|
|
| Niveau | Commandes |
|
|
|---|---|
|
|
| **Admin** | `create`, `delete`, `setleader`, `score`, `add`, `remove`, `transfer`, `visibility`, `setspawn` |
|
|
| **Joueur** | `join`, `leave`, `info`, `list`, `top` |
|
|
|
|
| Sous-commande | Permission |
|
|
|---|---|
|
|
| `create` | `crcore.team.create` |
|
|
| `delete` | `crcore.team.delete` |
|
|
| `setleader` | `crcore.team.setleader` |
|
|
| `score` | `crcore.team.score` |
|
|
| `add` | `crcore.team.add` |
|
|
| `remove` | `crcore.team.remove` |
|
|
| `transfer` | `crcore.team.transfer` |
|
|
| `visibility` | `crcore.team.visibility` |
|
|
| `setspawn` | `crcore.team.setspawn` |
|
|
| `join` | `crcore.team.join` |
|
|
| `leave` | `crcore.team.leave` |
|
|
| `info` | `crcore.team.info` |
|
|
| `list` | `crcore.team.list` |
|
|
| `top` | `crcore.team.top` |
|
|
|
|
Le plugin de jeu ou le serveur configure les défauts via LuckPerms / Bukkit
|
|
permissions plugin.
|
|
|
|
### Override d'une sous-commande par défaut
|
|
|
|
```java
|
|
// Option A : remplacer une feuille
|
|
core.getCoreCommand().findSubCommand("team")
|
|
.ifPresent(team -> team.replaceSubCommand("create",
|
|
new MyCustomTeamCreate(core.getTeamService())));
|
|
|
|
// Option B : sous-classer et override execute()
|
|
public class MyTeamCreate extends TeamCreateSubCommand {
|
|
public MyTeamCreate(TeamService service) { super(service); }
|
|
@Override public CommandResult execute(CommandContext ctx) {
|
|
// règles métier custom puis fallback super
|
|
return super.execute(ctx);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Diagramme
|
|
|
|
- Classes : [builtin-commands-diagram.puml](diagrams/builtin-commands-diagram.puml)
|
|
|
|
---
|
|
|
|
## 5. Évènements Bukkit
|
|
|
|
**Statut** : 12 évènements implémentés, tirés automatiquement par les services
|
|
par défaut (`BukkitEventFiringTeamServiceImpl` et `BukkitEventFiringPlayerProfileServiceImpl`).
|
|
|
|
Tous les évènements sont **post** (non-cancellable) — la validation se fait en
|
|
amont dans les services via les hooks `validate*`. Pour bloquer un comportement,
|
|
override le hook ou la sous-commande, pas l'évènement.
|
|
|
|
### Évènements Team (`fr.luc.crcore.team.event`)
|
|
|
|
| Évènement | Quand | Champs spécifiques |
|
|
|---|---|---|
|
|
| `TeamCreateEvent` | Après création + persist | — |
|
|
| `TeamDissolveEvent` | Après dissolution | — |
|
|
| `TeamMemberAddEvent` | Après ajout d'un membre (chef OU auto-join) | `getMember()` |
|
|
| `TeamMemberRemoveEvent` | Après retrait | `getPlayerId()` |
|
|
| `PlayerJoinTeamEvent` | Spécifique auto-join (joueur lui-même) | `getMember()` |
|
|
| `TeamLeadershipTransferEvent` | Après transfert | `getOldLeaderId()`, `getNewLeaderId()` |
|
|
| `TeamVisibilityChangeEvent` | Après changement effectif | `getOldVisibility()`, `getNewVisibility()` |
|
|
| `TeamScoreChangeEvent` | Après changement effectif d'un score | `getScoreName()`, `getOldValue()`, `getNewValue()`, `getDelta()` |
|
|
| `TeamSpawnPointChangeEvent` | Après changement du spawn | `getOldLocation()`, `getNewLocation()` (nullable) |
|
|
|
|
Tous étendent `TeamEvent` (porte la `Team`).
|
|
|
|
### Évènements Player (`fr.luc.crcore.player.event`)
|
|
|
|
| Évènement | Quand | Champs spécifiques |
|
|
|---|---|---|
|
|
| `PlayerProfileCreateEvent` | Après création (lazy ou explicite) | — |
|
|
| `PlayerProfileDeleteEvent` | Après suppression | — |
|
|
| `PlayerScoreChangeEvent` | Après changement effectif d'un score joueur | `getScoreName()`, `getOldValue()`, `getNewValue()`, `getDelta()` |
|
|
|
|
Tous étendent `PlayerProfileEvent` (porte le `PlayerProfile`).
|
|
|
|
### Usage
|
|
|
|
```java
|
|
@EventHandler
|
|
public void onTeamCreate(TeamCreateEvent e) {
|
|
Team t = e.getTeam();
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### Diagramme
|
|
|
|
- Classes : [events-diagram.puml](diagrams/events-diagram.puml)
|
|
|
|
---
|
|
|
|
## 6. Persistance SQLite (`fr.luc.crcore.database`)
|
|
|
|
**Statut** : wrapper minimal + table builder fluide. Repositories SQLite
|
|
write-through pour Team et PlayerProfile activés par défaut via `CRCore`.
|
|
|
|
### API
|
|
|
|
- **`Database`** (AutoCloseable) — 4 méthodes principales :
|
|
- `execute(sql, params...)` — DDL ou statement sans résultat
|
|
- `update(sql, params...)` — INSERT/UPDATE/DELETE, renvoie le nombre de lignes affectées
|
|
- `queryOne(sql, mapper, params...)` — au plus une ligne (Optional)
|
|
- `query(sql, mapper, params...)` — plusieurs lignes
|
|
- `inTransaction(Runnable)` — commit/rollback auto
|
|
- `table(name)` — démarre un `TableBuilder` fluide
|
|
- `tableExists(name)` — check
|
|
- **`TableBuilder`** — `.ifNotExists().column(name, type).primaryKey().notNull()...create()`
|
|
- **`ColumnType`** — enum : INTEGER, REAL, TEXT, BLOB, BOOLEAN, UUID
|
|
- **`RowMapper<T>`** — `T map(ResultSet rs) throws SQLException` (lambda-friendly)
|
|
- **`DatabaseException`** — runtime exception, wrap les `SQLException` JDBC
|
|
|
|
Les paramètres `Object...` sont liés via PreparedStatement (anti-injection),
|
|
avec conversion auto pour `UUID` (→ TEXT), `Enum<?>` (→ name() TEXT), `Boolean`
|
|
(→ 0/1).
|
|
|
|
### Tables internes CR-Core
|
|
|
|
Le préfixe `crcore_` évite les collisions :
|
|
|
|
- `crcore_teams`, `crcore_team_members`, `crcore_team_scores`
|
|
- `crcore_player_profiles`, `crcore_player_scores`
|
|
|
|
### Tables custom côté plugin de jeu
|
|
|
|
```java
|
|
Database db = core.getDatabase();
|
|
db.table("my_kills")
|
|
.ifNotExists()
|
|
.column("player_id", ColumnType.UUID).primaryKey()
|
|
.column("kills", ColumnType.INTEGER).notNull().defaultValue("0")
|
|
.create();
|
|
```
|
|
|
|
### Diagramme
|
|
|
|
- Classes : [database-diagram.puml](diagrams/database-diagram.puml)
|
|
|
|
---
|
|
|
|
## 7. Intégration PlaceholderAPI (optionnelle)
|
|
|
|
**Statut** : implémentée. Auto-détectée par `CRCore.enable()` — si le plugin
|
|
**PlaceholderAPI** est installé sur le serveur, les placeholders `%crcore_*%`
|
|
sont enregistrés automatiquement. Si PAPI est absent, la lib reste
|
|
fonctionnelle, juste sans placeholders.
|
|
|
|
### Placeholders Team
|
|
|
|
Renvoient vides si le joueur n'est dans aucune équipe.
|
|
|
|
| Placeholder | Renvoie | Exemple |
|
|
|---|---|---|
|
|
| `%crcore_team%` | récap formaté coloré | `§c[#WOLF] Wolves` |
|
|
| `%crcore_team_name%` | nom de l'équipe | `Wolves` |
|
|
| `%crcore_team_tag%` | tag court | `WOLF` |
|
|
| `%crcore_team_color%` | nom de la couleur | `Red` |
|
|
| `%crcore_team_color_chat%` | code couleur ChatColor | `§c` |
|
|
| `%crcore_team_size%` | nombre de membres | `5` |
|
|
| `%crcore_team_visibility%` | `PUBLIC` ou `PRIVATE` | `PRIVATE` |
|
|
| `%crcore_team_leader_name%` | nom du chef (vide si leaderless) | `Alice` |
|
|
| `%crcore_team_total_score%` | somme des scores de l'équipe | `42` |
|
|
| `%crcore_team_score_<name>%` | score nommé de l'équipe | `%crcore_team_score_kills%` → `12` |
|
|
|
|
### Placeholders Player
|
|
|
|
| Placeholder | Renvoie |
|
|
|---|---|
|
|
| `%crcore_player_score_<name>%` | score nommé du joueur (0 si pas set) |
|
|
| `%crcore_player_score_total%` | somme de tous les scores du joueur |
|
|
|
|
### Usage côté plugin de jeu / config
|
|
|
|
Pas d'action à faire côté plugin de jeu — la hook s'enregistre toute seule.
|
|
Les placeholders sont disponibles partout où PAPI les résout (scoreboard,
|
|
tablist, chat, hologrammes via DecentHolograms, etc.) :
|
|
|
|
```yaml
|
|
# Exemple de scoreboard config (FeatherBoard / Scoreboard plugin)
|
|
lines:
|
|
- "&aÉquipe : %crcore_team%"
|
|
- "&aChef : %crcore_team_leader_name%"
|
|
- "&aKills : %crcore_player_score_kills% (total équipe %crcore_team_score_kills%)"
|
|
```
|
|
|
|
### Override
|
|
|
|
`CRCore.registerPlaceholderHook()` est `protected`. Override dans une
|
|
sous-classe de `CRCore` pour ajouter ses propres placeholders ou désactiver
|
|
la hook.
|
|
|
|
---
|
|
|
|
## 8. Service de messages (`fr.luc.crcore.message`)
|
|
|
|
**Statut** : implémenté. Toutes les commandes built-in `/core team ...` passent
|
|
par ce service ; plus rien n'est hardcodé dans le code.
|
|
|
|
### Modèle « un seul fichier par plugin »
|
|
|
|
Au premier démarrage, CR-Core crée **un seul fichier** dans le dataFolder du
|
|
plugin de jeu :
|
|
|
|
```
|
|
<plugin-dataFolder>/<plugin-name-lowercase>-messages.yml
|
|
```
|
|
|
|
(par exemple `cites-messages.yml` si le plugin s'appelle `Cites`.) C'est *le*
|
|
fichier que l'admin du serveur édite. Pas de `crcore-messages.yml` séparé sur
|
|
disque — les defaults CR-Core vivent dans le jar et sont chargés en mémoire
|
|
comme couche de fallback.
|
|
|
|
### Deux couches en mémoire
|
|
|
|
| Ordre | Source | Mutabilité |
|
|
|---|---|---|
|
|
| 1 | `crcore-messages.yml` embarqué dans le jar CR-Core | Read-only, in-memory |
|
|
| 2 | `<plugin>-messages.yml` dans le dataFolder | Lecture du disque |
|
|
|
|
La couche 2 écrase la couche 1 sur les clés communes. Une clé manquante dans
|
|
le fichier user **retombe automatiquement** sur le default CR-Core. Si une
|
|
future release CR-Core ajoute une nouvelle clé, l'admin n'a rien à faire — ça
|
|
marche immédiatement.
|
|
|
|
### Création du fichier user au premier démarrage
|
|
|
|
L'ordre de priorité pour générer le starter file :
|
|
|
|
1. Si le plugin de jeu **bundle son propre** `<plugin-name-lowercase>-messages.yml`
|
|
dans ses ressources → c'est ce fichier qui devient le template (donc tu peux
|
|
pré-remplir avec tes overrides + tes messages perso au build).
|
|
2. Sinon → copie des defaults CR-Core comme starter (l'admin voit toutes les
|
|
clés CR-Core et peut les éditer).
|
|
|
|
### API `MessagesService`
|
|
|
|
```java
|
|
// Lecture avec placeholders nommés
|
|
core.messages().get("team.create.success",
|
|
"name", team.getName(),
|
|
"tag", team.getTag(),
|
|
"visibility", team.getVisibility().name());
|
|
// → "&aÉquipe Wolves [#WOLF] créée (PRIVATE, sans chef)." (codes & déjà traduits)
|
|
|
|
// Lecture brute (sans substitution ni couleur)
|
|
core.messages().raw("team.create.success");
|
|
|
|
// Ajout/override programmatique en mémoire (non persisté)
|
|
core.messages().set("mygame.welcome", "&aBienvenue {player} !");
|
|
|
|
// Charge un fichier YAML additionnel (en plus du fichier user principal)
|
|
core.messages().loadAdditional("my-extras.yml");
|
|
|
|
// Hot reload (relit le fichier user, garde les defaults en mémoire)
|
|
core.messages().reload();
|
|
|
|
// Toggle codes couleur
|
|
core.messages().setApplyColorCodes(true); // défaut
|
|
|
|
// Chemin du fichier user (informationnel)
|
|
File path = core.messages().getUserFile();
|
|
```
|
|
|
|
### Placeholders
|
|
|
|
Format `{name}` avec substitution via varargs paire-par-paire. Codes couleur
|
|
Bukkit `&a`, `&c`, `&7`, `&f`, etc. traduits automatiquement en `§…`.
|
|
|
|
### Clés manquantes
|
|
|
|
Si une clé n'existe ni dans le fichier user ni dans les defaults,
|
|
`messages.get(...)` renvoie `[missing: key]` pour rendre visible la clé qu'il
|
|
manque (debug facile).
|
|
|
|
### Override côté plugin de jeu
|
|
|
|
**Option 1 — Bundler son propre template** dans les ressources du plugin de
|
|
jeu, fichier nommé `<plugin-name-lowercase>-messages.yml`. À la première
|
|
exécution, ce fichier sert de starter dans le dataFolder.
|
|
|
|
**Option 2 — Éditer le fichier généré** après premier démarrage. Le YAML
|
|
contient déjà les clés CR-Core ; ajoute / modifie / supprime ce que tu veux.
|
|
|
|
**Option 3 — Override programmatique** :
|
|
```java
|
|
core.messages().set("team.create.success", "Custom format for {name}");
|
|
```
|
|
|
|
### Override de l'impl
|
|
|
|
`CRCore.buildMessagesService()` est `protected`. Sous-classe CRCore et
|
|
redéfinis-la pour une impl custom (ex. messages venant d'une base de données,
|
|
d'un microservice de traduction, etc.).
|
|
|
|
### Fichier `crcore-messages.yml` — référence
|
|
|
|
~25 clés organisées en sections : `common.*` (no-permission, player-only, …),
|
|
`team.create.*`, `team.delete.success`, `team.add.*`, etc. Voir le fichier
|
|
dans `src/main/resources/crcore-messages.yml` pour la liste complète avec les
|
|
placeholders documentés en commentaire.
|
|
|
|
### Diagramme
|
|
|
|
- Classes : [messages-class-diagram.puml](diagrams/messages-class-diagram.puml)
|
|
|
|
---
|
|
|
|
## 9. Service de broadcasts (`fr.luc.crcore.broadcast`)
|
|
|
|
**Statut** : implémenté. Un seul listener Bukkit interne route les 12
|
|
événements CR-Core vers le {@code BroadcastService} qui décide à qui
|
|
envoyer le message selon la config YAML.
|
|
|
|
### Séparation routes / templates
|
|
|
|
- **Routes** (« qui reçoit quoi ») → `<plugin>-broadcasts.yml`
|
|
- **Templates** (« quel texte ») → `<plugin>-messages.yml`, clés
|
|
`<eventKey>.broadcast` (ex. `team.create.broadcast`)
|
|
|
|
Les deux fichiers sont modifiables indépendamment. L'admin peut couper
|
|
tous les broadcasts en passant tout en `[NONE]` sans toucher aux templates,
|
|
ou inversement changer la formulation sans toucher aux routes.
|
|
|
|
### `BroadcastAudience` — qui reçoit
|
|
|
|
| Audience | Résolution |
|
|
|---|---|
|
|
| `NONE` | Personne (équivalent à liste vide). |
|
|
| `LEADER` | Le chef de l'équipe concernée (s'il est en ligne). |
|
|
| `TEAM` | Tous les membres en ligne de l'équipe concernée. |
|
|
| `ADMIN` | Joueurs en ligne ayant la perm `crcore.broadcast.admin`. |
|
|
| `ALL` | Tous les joueurs en ligne sur le serveur. |
|
|
|
|
Multi-cibles : une clé d'event mappe sur une **liste** d'audiences. Union
|
|
(pas de doublon : un joueur dans deux audiences reçoit un seul message).
|
|
|
|
### Le fichier `<plugin>-broadcasts.yml` — exemple
|
|
|
|
```yaml
|
|
team:
|
|
create: [ADMIN] # admins voient les créations
|
|
dissolve: [TEAM, ADMIN]
|
|
member:
|
|
add: [TEAM]
|
|
remove: [TEAM]
|
|
player:
|
|
join: [TEAM]
|
|
leadership:
|
|
transfer: [TEAM, ADMIN]
|
|
visibility:
|
|
change: [LEADER]
|
|
score:
|
|
change: [NONE] # noisy par défaut
|
|
spawn:
|
|
change: [LEADER]
|
|
|
|
player:
|
|
profile:
|
|
create: [NONE]
|
|
delete: [ADMIN]
|
|
score:
|
|
change: [NONE]
|
|
```
|
|
|
|
### Liste des `eventKey` (= mapping listener)
|
|
|
|
| Bukkit event | Clé broadcasts.yml | Clé messages.yml |
|
|
|---|---|---|
|
|
| `TeamCreateEvent` | `team.create` | `team.create.broadcast` |
|
|
| `TeamDissolveEvent` | `team.dissolve` | `team.dissolve.broadcast` |
|
|
| `TeamMemberAddEvent` | `team.member.add` | `team.member.add.broadcast` |
|
|
| `TeamMemberRemoveEvent` | `team.member.remove` | `team.member.remove.broadcast` |
|
|
| `PlayerJoinTeamEvent` | `team.player.join` | `team.player.join.broadcast` |
|
|
| `TeamLeadershipTransferEvent` | `team.leadership.transfer` | `team.leadership.transfer.broadcast` |
|
|
| `TeamVisibilityChangeEvent` | `team.visibility.change` | `team.visibility.change.broadcast` |
|
|
| `TeamScoreChangeEvent` | `team.score.change` | `team.score.change.broadcast` |
|
|
| `TeamSpawnPointChangeEvent` | `team.spawn.change` | `team.spawn.change.broadcast` |
|
|
| `PlayerProfileCreateEvent` | `player.profile.create` | `player.profile.create.broadcast` |
|
|
| `PlayerProfileDeleteEvent` | `player.profile.delete` | `player.profile.delete.broadcast` |
|
|
| `PlayerScoreChangeEvent` | `player.score.change` | `player.score.change.broadcast` |
|
|
|
|
### Placeholders injectés par le listener
|
|
|
|
Pour les events team, le contexte inclut toujours : `{name}`, `{team_name}`
|
|
(alias), `{tag}`, `{color}` (code couleur ChatColor), `{visibility}`.
|
|
Quand pertinent, en plus : `{player}` (nom du joueur impliqué),
|
|
`{new_leader}`, `{old_leader}`, `{old_visibility}`, `{new_visibility}`,
|
|
`{score_name}`, `{old_value}`, `{new_value}`, `{delta}`.
|
|
|
|
### API pour les game plugins
|
|
|
|
```java
|
|
// Broadcast custom depuis un game plugin
|
|
core.broadcasts().broadcast("mygame.round.start",
|
|
BroadcastContext.empty()
|
|
.with("round", String.valueOf(currentRound))
|
|
.with("map", mapName));
|
|
|
|
// Lecture des audiences configurées (debug)
|
|
List<BroadcastAudience> who = core.broadcasts().getAudiences("team.create");
|
|
|
|
// Hot reload
|
|
core.broadcasts().reload();
|
|
```
|
|
|
|
Pour ajouter ses propres events broadcast, le game plugin :
|
|
1. Ajoute la clé dans son `<plugin>-broadcasts.yml` (ex.
|
|
`mygame.round.start: [ALL]`)
|
|
2. Ajoute le template dans `<plugin>-messages.yml` (clé
|
|
`mygame.round.start.broadcast`)
|
|
3. Appelle `core.broadcasts().broadcast(...)` quand l'event survient
|
|
|
|
### Override de l'impl
|
|
|
|
`CRCore.buildBroadcastService(messages)` est `protected`. Sous-classe
|
|
{@code CRCore} pour fournir une impl alternative (base de données, queue
|
|
externe, etc.).
|
|
|
|
### Commande `/core reload`
|
|
|
|
Permission : `crcore.reload`. Recharge à la fois `messages` et `broadcasts`
|
|
depuis les fichiers user du dataFolder. Les defaults en jar ne bougent pas
|
|
(pas re-chargés).
|
|
|
|
### Diagramme
|
|
|
|
- Classes : [broadcasts-class-diagram.puml](diagrams/broadcasts-class-diagram.puml)
|
|
|
|
---
|
|
|
|
## 10. Paramètres d'équipe (`fr.luc.crcore.team.config`)
|
|
|
|
**Statut** : implémenté. 8 settings standards + GUI in-game + cascade
|
|
per-team → global → default.
|
|
|
|
### Modèle de résolution
|
|
|
|
```
|
|
1. hard default défini en code dans TeamSettings (constantes)
|
|
2. global config <plugin>-team-config.yml ← admin via GUI ou YAML
|
|
3. per-team override table SQLite crcore_team_settings ← admin via GUI
|
|
```
|
|
|
|
`config.get(team, setting)` cascade per-team → global → default.
|
|
`config.getGlobal(setting)` cascade global → default (skip per-team).
|
|
Toutes les valeurs retournées sont **non-null** grâce au default en bout
|
|
de chaîne.
|
|
|
|
### Settings standards (`TeamSettings`)
|
|
|
|
| Constante | Clé YAML/SQL | Type | Défaut |
|
|
|---|---|---|---|
|
|
| `FRIENDLY_FIRE` | `friendly_fire` | bool | `false` |
|
|
| `PVP_PROTECTION_SECONDS` | `pvp_protection_seconds` | int | `0` |
|
|
| `MAX_SIZE` | `max_size` | int | `0` (illimité) |
|
|
| `MIN_SIZE` | `min_size` | int | `0` |
|
|
| `RESPAWN_AT_TEAM_SPAWN` | `respawn_at_team_spawn` | bool | `true` |
|
|
| `TEAM_CHAT_ENABLED` | `team_chat_enabled` | bool | `true` |
|
|
| `SHOW_TAG_ABOVE_HEAD` | `show_tag_above_head` | bool | `true` |
|
|
| `TEAM_COLOR_IN_NAME` | `team_color_in_name` | bool | `true` |
|
|
|
|
CR-Core fournit les défauts ; **c'est au plugin de jeu d'appliquer ces
|
|
settings** dans sa logique (ex. écouter `EntityDamageByEntityEvent` et
|
|
checker `config.get(team, FRIENDLY_FIRE)` pour décider si le coup passe).
|
|
|
|
### API typée
|
|
|
|
```java
|
|
boolean ff = core.teamConfig().get(team, TeamSettings.FRIENDLY_FIRE);
|
|
int max = core.teamConfig().getGlobal(TeamSettings.MAX_SIZE);
|
|
core.teamConfig().setPerTeam(team, TeamSettings.FRIENDLY_FIRE, true);
|
|
core.teamConfig().resetPerTeam(team, TeamSettings.FRIENDLY_FIRE);
|
|
core.teamConfig().setGlobal(TeamSettings.MAX_SIZE, 8); // persiste le YAML
|
|
core.teamConfig().reload();
|
|
```
|
|
|
|
### Settings custom (game plugin)
|
|
|
|
Un game plugin peut enregistrer ses propres settings :
|
|
|
|
```java
|
|
public static final TeamSetting<Boolean> CITES_PVP_ROUND_END =
|
|
TeamSetting.ofBoolean("cites_pvp_round_end", false);
|
|
|
|
@Override public void onEnable() {
|
|
core = new CRCore(this).enable();
|
|
TeamSettings.register(CITES_PVP_ROUND_END);
|
|
}
|
|
```
|
|
|
|
→ La clé apparaîtra automatiquement dans les GUI globaux et per-team, et
|
|
sera persistée en SQLite + YAML comme les standards.
|
|
|
|
### Commande GUI
|
|
|
|
`/core team settings [team]` — player-only, ouvre l'interface graphique.
|
|
|
|
- Sans argument → **GUI globaux** (perm `crcore.team.settings.global`).
|
|
Modif → écrit dans `<plugin>-team-config.yml`.
|
|
- Avec argument → **GUI per-team** (perm `crcore.team.settings`). Modif →
|
|
écrit en SQLite (overrides). Bouton "Reset tous les overrides" pour
|
|
remettre toutes les valeurs au global.
|
|
|
|
### Mécaniques GUI
|
|
|
|
- **Inventaire 27 slots**, settings sur la ligne du milieu (slots 10..16).
|
|
- **Booléens** : lampe verte (ON) / grise (OFF), clic = toggle.
|
|
- **Entiers** : item livre.
|
|
- Clic gauche = +1, shift = +10
|
|
- Clic droit = -1, shift = -10
|
|
- Clamp à 0 minimum
|
|
- **Strings/Enums** : affichage seul (édition via YAML — pas dans la V1
|
|
du GUI).
|
|
- **Per-team** : indication visuelle "Override per-team actif" dans la
|
|
lore quand une valeur est différente du global.
|
|
- **Slot 22** : bouton Fermer.
|
|
- **Slot 18** (per-team uniquement) : bouton "Reset tous les overrides".
|
|
|
|
### Framework GUI réutilisable
|
|
|
|
Le module `fr.luc.crcore.gui` est **générique** — réutilisable pour tout
|
|
futur GUI CR-Core ou game plugin :
|
|
|
|
- `AbstractInventoryGui implements InventoryHolder` — base abstraite,
|
|
`rebuild()`, `setButton(slot, item, handler)`, `setDecoration(...)`,
|
|
`clearSlot(...)`, hook `onClose(...)`.
|
|
- `GuiClickHandler` (FunctionalInterface) — handler de clic par slot.
|
|
- `GuiListener` — un seul Listener Bukkit qui route les clics et les
|
|
fermetures vers le bon GUI via `inventory.getHolder()`.
|
|
- `GuiItems` — builder fluide `named(material, "&aTitre").lore(...).build()`,
|
|
filler décoratif gris.
|
|
|
|
Pour faire un GUI custom : `extends AbstractInventoryGui`, créer
|
|
l'inventaire dans le constructeur, override `rebuild()`. Le `GuiListener`
|
|
est déjà enregistré par `CRCore.enable()`.
|
|
|
|
### Diagrammes
|
|
|
|
- [team-config-class-diagram.puml](diagrams/team-config-class-diagram.puml)
|
|
- [gui-class-diagram.puml](diagrams/gui-class-diagram.puml)
|
|
|
|
---
|
|
|
|
## 11. Modération (`fr.luc.crcore.features.moderation`)
|
|
|
|
**Statut** : skeleton. Architecture complète, 5 outils squelette, vanish
|
|
+ freeze fonctionnels. Persistance SQLite et outils avancés à venir.
|
|
|
|
### Mod mode
|
|
|
|
`/core admin` (perm `crcore.admin`, player-only) toggle on/off le mod
|
|
mode pour l'exécutant :
|
|
|
|
- **À l'entrée** :
|
|
1. Snapshot complet du joueur (`ModerationState`) : inventory + armor
|
|
+ offhand, XP level + progress, health, food, location, gamemode,
|
|
allowFlight + flying, walk/fly speed.
|
|
2. Inventaire vidé, hotbar peuplée avec les outils du
|
|
`ModeratorToolRegistry`.
|
|
3. Passage en `CREATIVE` + `allowFlight=true`.
|
|
4. Vanish actif (caché de tous les joueurs en ligne).
|
|
5. Event Bukkit `ModerationEnterEvent` tiré.
|
|
- **À la sortie** : restauration intégrale du snapshot + retrait du
|
|
vanish + cleanup freeze + `ModerationExitEvent`.
|
|
|
|
### Outils squelette (hotbar)
|
|
|
|
| Slot | Outil | Action |
|
|
|---|---|---|
|
|
| 0 | `TeleportRandomPlayerTool` (compass) | clic droit → tp à un joueur aléatoire |
|
|
| 1 | `InventorySpyTool` (chest) | clic droit sur joueur → ouvre son inv |
|
|
| 2 | `FreezeTool` (ice) | clic droit sur joueur → toggle freeze |
|
|
| 7 | `VanishToggleTool` (ender eye) | clic → toggle vanish |
|
|
| 8 | `ExitTool` (barrier) | clic → exit mod mode |
|
|
|
|
Slots libres (3, 4, 5, 6) prêts pour des outils custom du game plugin.
|
|
|
|
### Ajouter un outil custom
|
|
|
|
```java
|
|
public class WarnTool implements ModeratorTool {
|
|
@Override public String getKey() { return "warn"; }
|
|
@Override public int getSlot() { return 4; }
|
|
@Override public ItemStack buildIcon() {
|
|
return GuiItems.named(Material.PAPER, "&eWarn").build();
|
|
}
|
|
@Override public void onInteractEntity(Player mod, Entity target) {
|
|
if (target instanceof Player) {
|
|
((Player) target).sendMessage("§eTu as reçu un warn !");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Côté game plugin onEnable() :
|
|
core.moderation().getToolRegistry().register(new WarnTool());
|
|
```
|
|
|
|
### Vanish
|
|
|
|
Le vanish utilise `Player.hidePlayer(plugin, vanished)` pour cacher de
|
|
tous les autres joueurs. Le `ModerationListener` re-applique
|
|
automatiquement le vanish aux joueurs qui rejoignent le serveur.
|
|
|
|
### Freeze
|
|
|
|
`moderation.freeze(uuid)` ajoute l'UUID à un `Set` en mémoire. Le
|
|
`ModerationListener` cancel les `PlayerMoveEvent` qui changent de bloc.
|
|
La rotation de la tête reste libre (UX). Le freeze est retiré
|
|
automatiquement à la déconnexion du joueur.
|
|
|
|
### `ModerationListener` — verrouillage hotbar
|
|
|
|
Tant qu'un joueur est en mod mode :
|
|
- Les drops d'items sont annulés.
|
|
- Le swap main/offhand est annulé.
|
|
- Les clics dans son propre inventaire sont annulés. Les clics dans un
|
|
inventaire ouvert par un outil (ex. `InventorySpy`) restent libres.
|
|
- Les `PlayerInteractEvent` sur la hotbar routent vers le tool du slot
|
|
tenu, avec annulation pour éviter toute interaction réelle avec le
|
|
monde.
|
|
|
|
### Events Bukkit
|
|
|
|
- `ModerationEnterEvent` — après snapshot + vanish + équipement
|
|
- `ModerationExitEvent` — après restore + retrait vanish
|
|
|
|
### Limites du skeleton — TODO
|
|
|
|
- **Persistance** : `InMemoryModerationRepository` uniquement. Si le
|
|
serveur crash pendant qu'un mod est en mod mode, son inventaire est
|
|
perdu. À ajouter : `SqliteModerationRepository` avec sérialisation
|
|
Bukkit (`BukkitObjectOutputStream` → base64) des `ItemStack[]`.
|
|
- **InventorySpyTool** : ouvre l'inventaire réel — toute modif est
|
|
appliquée. Pour de l'audit pur, wrap dans une `Inventory` cloné en
|
|
read-only.
|
|
- **TeleportRandomPlayerTool** : placeholder. Remplacer par un GUI
|
|
sélecteur de joueurs.
|
|
- **Pas de broadcasts** : pas encore d'entrées
|
|
`moderation.enter.broadcast` / `moderation.exit.broadcast`. Trivial à
|
|
ajouter au système broadcasts en suivant le pattern existant.
|
|
- **Pas d'autre toggle** que self — `/core admin <player>` n'existe pas
|
|
encore.
|
|
|
|
### Setup côté plugin de jeu
|
|
|
|
```java
|
|
this.core = new CRCore(this,
|
|
new CRCoreConfig().setupModeration()) // ou setupAll()
|
|
.enable();
|
|
|
|
// Ajouter un outil custom (optionnel)
|
|
core.moderation().getToolRegistry().register(new MyWarnTool());
|
|
```
|
|
|
|
### Diagramme
|
|
|
|
- [moderation-class-diagram.puml](diagrams/moderation-class-diagram.puml)
|
|
|
|
---
|
|
|
|
## 12. Bootstrap `CRCore`
|
|
|
|
**Statut** : implémenté. Point d'entrée unique pour les plugins de jeu.
|
|
|
|
### Usage
|
|
|
|
```java
|
|
public class MyGamePlugin extends JavaPlugin {
|
|
private CRCore core;
|
|
|
|
@Override
|
|
public void onEnable() {
|
|
core = new CRCore(this).enable();
|
|
}
|
|
|
|
@Override
|
|
public void onDisable() {
|
|
if (core != null) core.disable();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Configuration
|
|
|
|
`CRCoreConfig` en builder :
|
|
|
|
```java
|
|
new CRCore(this, new CRCoreConfig()
|
|
.withSqliteFile("mygame.db") // défaut : crcore.db
|
|
.withCommandName("game")) // défaut : core
|
|
.enable();
|
|
```
|
|
|
|
`withInMemoryStorage()` désactive SQLite (tests, ou contexte stateless).
|
|
|
|
### Override de la construction des services
|
|
|
|
Sous-classer `CRCore` et redéfinir :
|
|
- `buildTeamService(repo)` — pour utiliser une impl custom du service team
|
|
- `buildPlayerProfileService(repo)` — idem player
|
|
- `buildCoreCommand(...)` — pour ajouter des groupes top-level
|
|
|
|
### Diagramme
|
|
|
|
- Séquence : [bootstrap-sequence.puml](diagrams/bootstrap-sequence.puml)
|
|
|
|
---
|
|
|
|
## Backlog / idées de futurs domaines
|
|
|
|
_(à remplir — ex. inventaires partagés d'équipe, kits, gestion de rounds, …)_
|