feat: admin/chef/player permission model + leaderless teams + setleader
Commands: - Drop all short aliases (c/i/t/j/vis/disband/...) — long names only. - Every /core team <action> now has a crcore.team.<action> permission. - Three-tier model: * Admin (perm only): create, delete, setleader, score * Chef (perm + chef-check in execute): add, remove, transfer, visibility, setspawn * Player (perm): join, leave, info, list, top - delete now takes <team> as argument (admin); no more chef-disband shortcut. New /core team setleader <team> <player>: - Admin assigns or replaces a team's leader. - More permissive than transfer: target may not yet be a member (auto-add), and works on leaderless teams. Leaderless teams: - Team.leaderId is now nullable. - getLeaderId() and getLeader() return Optional<...>. - hasLeader() and isLeader(UUID) helpers added. - New constructors Team(id, name, tag, color [, visibility]) for leaderless. - TeamService.createTeam overloads without leaderId. - TeamService.setLeader(teamId, playerId): assigns/replaces leader (auto-adds as member if needed). Fires TeamLeadershipTransferEvent with optional old. - TeamLeadershipTransferEvent.getOldLeaderId() returns Optional<UUID>. - SqliteTeamRepository: leader_id column no longer NOT NULL. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -323,6 +323,66 @@ Format léger : une décision = un titre + contexte + choix + raison.
|
||||
même fichier SQLite (par défaut `<dataFolder>/crcore.db`) ; le préfixe
|
||||
isole proprement.
|
||||
|
||||
## 2026-06-09 — Refonte permissions + modèle admin/chef/joueur
|
||||
|
||||
- **Choix** : chaque sous-commande `/core team <action>` a sa propre permission
|
||||
`crcore.team.<action>`. Trois niveaux fonctionnels :
|
||||
- **Admin** (permission seule, cible une team par argument) : `create`,
|
||||
`delete`, `setleader`, `score`.
|
||||
- **Chef** (permission + check chef dans `execute()`) : `add`, `remove`,
|
||||
`transfer`, `visibility`, `setspawn`.
|
||||
- **Joueur** (permission seule, défaut « tout le monde » côté LuckPerms si
|
||||
voulu) : `join`, `leave`, `info`, `list`, `top`.
|
||||
- **Aliases courts supprimés** : `c` (create), `i` (info), `t` (team), `j`
|
||||
(join), `vis` (visibility), `disband`/`dissolve` (delete), `kick`/`expel`
|
||||
(remove), etc. Plus que les noms longs. Raison : réduire la friction
|
||||
d'apprentissage et la confusion (les game plugins ont leurs propres noms,
|
||||
l'aliasing devient un bruit).
|
||||
- **`delete` devient admin** : `/core team delete <team>` (au lieu de
|
||||
l'ancien `/core team delete` qui ciblait l'équipe du chef). Cohérent avec
|
||||
`create` qui est aussi admin.
|
||||
|
||||
## 2026-06-09 — Team peut être leaderless
|
||||
|
||||
- **Choix** : `Team.leaderId` devient nullable. `getLeaderId()` renvoie
|
||||
`Optional<UUID>`, `getLeader()` renvoie `Optional<TeamMember>`. Nouveau
|
||||
`hasLeader()` et `isLeader(UUID)` pour les checks.
|
||||
- **Raison** : le modèle admin requiert qu'on puisse créer une équipe sans
|
||||
chef et l'assigner ensuite via {@code setLeader}. Avant, créer une équipe
|
||||
imposait de connaître l'UUID du chef.
|
||||
- **Constructeurs ajoutés** :
|
||||
- `new Team(id, name, tag, color)` — leaderless, PRIVATE
|
||||
- `new Team(id, name, tag, color, visibility)` — leaderless avec visibilité
|
||||
- Les constructeurs avec leaderId acceptent maintenant `null`.
|
||||
- **`Team.setLeader(playerId)`** : assigne un chef à n'importe quel moment.
|
||||
Si la team a déjà un chef, il est démis en MEMBER. Si le nouveau n'est
|
||||
pas membre, il est auto-ajouté.
|
||||
- **`Team.transferLeadership(playerId)`** : conserve sa sémantique stricte
|
||||
(chef→chef, membre déjà existant). Lève `IllegalStateException` si la team
|
||||
est leaderless. Utilisé par la commande `/core team transfer` (chef).
|
||||
- **`TeamLeadershipTransferEvent.getOldLeaderId()`** renvoie maintenant
|
||||
`Optional<UUID>` (vide si la team était leaderless avant l'opération).
|
||||
- **Schéma SQLite** : la colonne `crcore_teams.leader_id` n'a plus la
|
||||
contrainte `NOT NULL`. Migration automatique sur nouvelle base — pour les
|
||||
bases existantes, ALTER TABLE manuel ou suppression du fichier
|
||||
(les bases d'event sont jetables).
|
||||
|
||||
## 2026-06-09 — Nouvelle commande `/core team setleader`
|
||||
|
||||
- **Choix** : ajout de `TeamSetLeaderSubCommand` (`/core team setleader
|
||||
<team> <player>`). Permission `crcore.team.setleader`. Délègue à
|
||||
`TeamService.setLeader(...)`.
|
||||
- **Différence avec `/core team transfer`** :
|
||||
- `transfer` : action **chef**, cible son équipe, le nouveau chef doit déjà
|
||||
être membre.
|
||||
- `setleader` : action **admin**, cible n'importe quelle équipe, le nouveau
|
||||
chef peut être non-membre (auto-ajouté).
|
||||
- **Use cases couverts** :
|
||||
- Admin assigne un chef à une équipe leaderless fraîchement créée.
|
||||
- Admin remplace le chef d'une équipe (le membre target est déjà dans
|
||||
l'équipe ou pas — peu importe).
|
||||
- Admin "promote member up to leader" (cas explicitement demandé).
|
||||
|
||||
## 2026-06-09 — Bascule Java 16 → Java 11 (révision)
|
||||
|
||||
- **Révision** de la décision "Java 16" du 2026-06-08.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@startuml builtin-commands-diagram
|
||||
title CR-Core — Default /core team commands
|
||||
title CR-Core — Default /core team commands (admin / chef / joueur)
|
||||
|
||||
skinparam classAttributeIconSize 0
|
||||
hide empty members
|
||||
@@ -13,7 +13,6 @@ package "fr.luc.crcore.command.builtin" {
|
||||
|
||||
class CoreCommand {
|
||||
+ CoreCommand(teamSvc, playerSvc)
|
||||
# registerDefaults(): void
|
||||
}
|
||||
CoreCommand --|> BaseCommand
|
||||
|
||||
@@ -29,44 +28,93 @@ package "fr.luc.crcore.command.builtin" {
|
||||
+ {static} teamByName(service): ArgumentType<Team>
|
||||
}
|
||||
|
||||
class TeamCreateSubCommand {
|
||||
+ execute(ctx): CommandResult
|
||||
' === ADMIN commands (permission seule) ===
|
||||
package "admin" <<Rectangle>> {
|
||||
class TeamCreateSubCommand {
|
||||
perm: crcore.team.create
|
||||
args: name, tag, color, [leader]
|
||||
}
|
||||
class TeamDeleteSubCommand {
|
||||
perm: crcore.team.delete
|
||||
args: <team>
|
||||
}
|
||||
class TeamSetLeaderSubCommand {
|
||||
perm: crcore.team.setleader
|
||||
args: <team> <player>
|
||||
}
|
||||
class TeamScoreSubCommand {
|
||||
perm: crcore.team.score
|
||||
args: <team> <name> <add|set> <value>
|
||||
}
|
||||
}
|
||||
|
||||
' === CHEF commands (permission + check chef) ===
|
||||
package "chef" <<Rectangle>> {
|
||||
class TeamAddSubCommand {
|
||||
perm: crcore.team.add
|
||||
args: <player>
|
||||
}
|
||||
class TeamRemoveSubCommand {
|
||||
perm: crcore.team.remove
|
||||
args: <player>
|
||||
}
|
||||
class TeamTransferSubCommand {
|
||||
perm: crcore.team.transfer
|
||||
args: <player>
|
||||
}
|
||||
class TeamVisibilitySubCommand {
|
||||
perm: crcore.team.visibility
|
||||
args: <vis>
|
||||
}
|
||||
class TeamSetSpawnSubCommand {
|
||||
perm: crcore.team.setspawn
|
||||
}
|
||||
}
|
||||
|
||||
' === PLAYER commands ===
|
||||
package "player" <<Rectangle>> {
|
||||
class TeamJoinSubCommand {
|
||||
perm: crcore.team.join
|
||||
args: <team>
|
||||
}
|
||||
class TeamLeaveSubCommand {
|
||||
perm: crcore.team.leave
|
||||
}
|
||||
class TeamInfoSubCommand {
|
||||
perm: crcore.team.info
|
||||
args: [team]
|
||||
}
|
||||
class TeamListSubCommand {
|
||||
perm: crcore.team.list
|
||||
}
|
||||
class TeamTopSubCommand {
|
||||
perm: crcore.team.top
|
||||
args: [score]
|
||||
}
|
||||
}
|
||||
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
|
||||
TeamSetLeaderSubCommand --|> SubCommand
|
||||
TeamScoreSubCommand --|> SubCommand
|
||||
TeamAddSubCommand --|> SubCommand
|
||||
TeamRemoveSubCommand --|> SubCommand
|
||||
TeamTransferSubCommand --|> SubCommand
|
||||
TeamVisibilitySubCommand --|> SubCommand
|
||||
TeamSetSpawnSubCommand --|> 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
|
||||
TeamGroupSubCommand "1" *-- "14" SubCommand : contains
|
||||
}
|
||||
}
|
||||
|
||||
note right of CoreCommand
|
||||
Le plugin de jeu downstream
|
||||
remplace une feuille avec :
|
||||
note bottom of TeamGroupSubCommand
|
||||
Override d'une feuille :
|
||||
core.getCoreCommand()
|
||||
.findSubCommand("team")
|
||||
.replaceSubCommand("create",
|
||||
|
||||
@@ -101,17 +101,24 @@ package "fr.luc.crcore.team" {
|
||||
- name: String
|
||||
- tag: String
|
||||
- color: TeamColor
|
||||
- leaderId: UUID
|
||||
- leaderId: UUID *(nullable)*
|
||||
- visibility: TeamVisibility
|
||||
- members: Set<TeamMember>
|
||||
- scores: Map<String, Integer>
|
||||
- spawnPoint: Location
|
||||
--
|
||||
+ Team(id, name, tag, color) ' leaderless, PRIVATE
|
||||
+ Team(id, name, tag, color, visibility) ' leaderless
|
||||
+ Team(id, name, tag, color, leaderId) ' with leader, PRIVATE
|
||||
+ Team(id, name, tag, color, leaderId, visibility)
|
||||
--
|
||||
+ getName(): String
|
||||
+ getTag(): String
|
||||
+ getColor(): TeamColor
|
||||
+ getLeaderId(): UUID
|
||||
+ getLeader(): TeamMember
|
||||
+ getLeaderId(): Optional<UUID>
|
||||
+ getLeader(): Optional<TeamMember>
|
||||
+ hasLeader(): boolean
|
||||
+ isLeader(playerId): boolean
|
||||
+ getVisibility(): TeamVisibility
|
||||
+ setVisibility(v): void
|
||||
+ isPublic(): boolean
|
||||
@@ -121,7 +128,8 @@ package "fr.luc.crcore.team" {
|
||||
+ size(): int
|
||||
+ addMember(playerId): TeamMember
|
||||
+ removeMember(playerId): boolean
|
||||
+ transferLeadership(newLeaderId): void
|
||||
+ transferLeadership(newLeaderId): void ' strict: chef→chef
|
||||
+ setLeader(newLeaderId): void ' permissive: assign
|
||||
--
|
||||
+ getScore(name): int
|
||||
+ hasScore(name): boolean
|
||||
@@ -157,12 +165,16 @@ package "fr.luc.crcore.team" {
|
||||
}
|
||||
|
||||
interface TeamService {
|
||||
+ createTeam(name, tag, color, leaderId, [visibility]): Team
|
||||
+ createTeam(name, tag, color): Team ' leaderless, PRIVATE
|
||||
+ createTeam(name, tag, color, visibility): Team ' leaderless
|
||||
+ createTeam(name, tag, color, leaderId): Team
|
||||
+ createTeam(name, tag, color, leaderId, visibility): Team
|
||||
+ dissolveTeam(teamId): boolean
|
||||
+ addMember(teamId, playerId): boolean
|
||||
+ removeMember(teamId, playerId): boolean
|
||||
+ joinTeam(teamId, playerId): boolean
|
||||
+ transferLeadership(teamId, newLeaderId): boolean
|
||||
+ transferLeadership(teamId, newLeaderId): boolean ' strict: chef→chef
|
||||
+ setLeader(teamId, newLeaderId): boolean ' permissive (admin)
|
||||
+ setVisibility(teamId, visibility): void
|
||||
--
|
||||
+ addScore(teamId, name, delta): int
|
||||
|
||||
+52
-38
@@ -30,7 +30,7 @@ Architecture des domaines :
|
||||
| `name` | `String` | Nom lisible. Unique (case-insensitive). |
|
||||
| `tag` | `String` | Tag court (le « # »). Unique. Affiché entre `[# … ]`. |
|
||||
| `color` | `TeamColor` | Couleur associée (enum). |
|
||||
| `leaderId` | `UUID` | Identifiant du joueur **chef d'équipe**. |
|
||||
| `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). |
|
||||
|
||||
@@ -64,13 +64,16 @@ Architecture des domaines :
|
||||
|
||||
| Opération | Description |
|
||||
|---|---|
|
||||
| `createTeam(name, tag, color, leaderId)` | Crée une équipe `PRIVATE` avec ce joueur comme chef. Échoue si nom/tag/joueur déjà pris. |
|
||||
| `createTeam(name, tag, color, leaderId, visibility)` | Surcharge : permet de créer directement en `PUBLIC` ou `PRIVATE`. |
|
||||
| `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)` | Le nouveau chef doit déjà être membre. |
|
||||
| `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). |
|
||||
@@ -326,54 +329,65 @@ 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
|
||||
**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`). Les aliases ont été retirés
|
||||
pour réduire la friction de découverte et la confusion.
|
||||
|
||||
### 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
|
||||
/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 <player> [chef] ajouter à son équipe
|
||||
├── remove <player> [chef] retirer de son équipe
|
||||
├── transfer <player> [chef] transférer leadership
|
||||
├── visibility <PUBLIC|PRIVATE> [chef] changer visibilité
|
||||
├── setspawn [chef] définir le spawn
|
||||
├── join <team> [joueur] rejoindre PUBLIC
|
||||
├── leave [joueur] quitter son équipe
|
||||
├── info [team] [joueur] infos
|
||||
├── list [joueur] toutes les équipes
|
||||
└── top [score] [joueur] classement
|
||||
```
|
||||
|
||||
### Aliases supplémentaires (équivalents Bukkit)
|
||||
### Permissions
|
||||
|
||||
| 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` |
|
||||
Chaque sous-commande a une permission `crcore.team.<action>`. Modèle à 3 niveaux :
|
||||
|
||||
### Permissions par défaut
|
||||
| Niveau | Commandes | Comportement |
|
||||
|---|---|---|
|
||||
| **Admin** | `create`, `delete`, `setleader`, `score` | Permission seule (pas de check chef). Cible une team via argument. |
|
||||
| **Chef** | `add`, `remove`, `transfer`, `visibility`, `setspawn` | Permission **ET** check chef de sa propre équipe en plus. Cible la team de l'exécutant. |
|
||||
| **Joueur** | `join`, `leave`, `info`, `list`, `top` | Permission seule (à granter par défaut côté LuckPerms si on veut que tout le monde y ait accès). |
|
||||
|
||||
| Sous-commande | Permission |
|
||||
|---|---|
|
||||
| `create` | `crcore.team.create` |
|
||||
| `score` | `crcore.team.score.modify` (admin) |
|
||||
| autres | aucune (gated par appartenance / rôle de chef) |
|
||||
| `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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user