# 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` | 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` 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` (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)` 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` (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) par défaut. Le contrat `TeamRepository extends Repository` permet de brancher YAML / SQLite / Postgres sans toucher au service. ### Diagrammes - Classes : [team-class-diagram.puml](diagrams/features/team/team-class-diagram.puml) - Séquence création : [team-create-sequence.puml](diagrams/features/team/team-create-sequence.puml) - Séquence auto-join : [team-join-sequence.puml](diagrams/features/team/team-join-sequence.puml) - Activité création : [team-create-activity.puml](diagrams/features/team/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` | 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` 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/features/player/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` custom = une classe qui implémente `parse(String): T` et optionnellement `suggestions(sender, partial): List`. ### 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/util/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 [leader] [admin] créer (chef optionnel) ├── delete [admin] dissoudre une équipe ├── setleader [admin] (re)assigner le chef ├── score [admin] modifier un score ├── add [admin] ajouter un joueur ├── remove [admin] retirer un joueur ├── transfer [admin] transfert chef→membre existant ├── visibility [admin] changer visibilité ├── setspawn [admin] spawn à la position de l'admin ├── join [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.` : | 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/features/team/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 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/util/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_%` | score nommé de l'équipe | `%crcore_team_score_kills%` → `12` | ### Placeholders Player | Placeholder | Renvoie | |---|---| | `%crcore_player_score_%` | 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 : ``` /-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 | `-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** `-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é `-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/util/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 ») → `-broadcasts.yml` - **Templates** (« quel texte ») → `-messages.yml`, clés `.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 `-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 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 `-broadcasts.yml` (ex. `mygame.round.start: [ALL]`) 2. Ajoute le template dans `-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/util/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 -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 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 `-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/features/team/team-config-class-diagram.puml) - [gui-class-diagram.puml](diagrams/util/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 ` 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/features/moderation/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, …)_