From a94bc56a5bcbd80317b8b33e7d3d95cd64f6ad84 Mon Sep 17 00:00:00 2001 From: Antone Barbaud Date: Wed, 10 Jun 2026 11:16:34 +0200 Subject: [PATCH] feat: configurable broadcasts + /core reload New fr.luc.crcore.broadcast module: - BroadcastAudience enum (NONE, LEADER, TEAM, ADMIN, ALL). - BroadcastContext (fluent: team + involvedPlayerId + placeholders). - BroadcastService interface + YamlBroadcastService impl. - CRCoreBroadcastListener (Bukkit listener) wires the 12 native events (9 team + 3 player) to broadcasts.broadcast(eventKey, ctx). Same single-per-plugin file pattern as messages: /-broadcasts.yml. Defaults bundled at resources/crcore-broadcasts.yml, copied on first boot (game plugin's own resource of the same name takes priority as the template). In-memory fallback so new CR-Core keys work without admin edit. Routes (who) vs templates (what) are separated: broadcasts.yml lists audiences per eventKey, messages.yml contains the templates under keys .broadcast. Admin can change either independently. 12 new *.broadcast keys added to crcore-messages.yml with sensible French defaults and color codes. Listener injects standard placeholders (name, team_name, tag, color, visibility, player, new_leader, old/new_value, etc.). ADMIN audience resolved via crcore.broadcast.admin permission. Multi- audiences via YAML list (e.g., [TEAM, ADMIN]); union of resolved players, no duplicate. New /core reload subcommand (permission crcore.reload) hot-reloads both messages and broadcasts from disk without restart. CRCore: protected buildBroadcastService() override point, getter broadcasts(), wire of CRCoreBroadcastListener at enable(). CoreCommand constructor extended to take BroadcastService, registers the reload subcommand. Docs/features.md: new section 9 "Service de broadcasts". docs/setup.md: updated to mention both YAML files. decisions.md logs the routes-vs- templates split, the audience model, and the reload semantics. New diagram broadcasts-class-diagram.puml. README updated. Co-Authored-By: Claude Opus 4.7 --- docs/README.md | 7 + docs/decisions.md | 36 +++ docs/diagrams/broadcasts-class-diagram.puml | 102 +++++++ docs/features.md | 125 +++++++- docs/setup.md | 18 ++ src/main/java/fr/luc/crcore/CRCore.java | 22 +- .../crcore/broadcast/BroadcastAudience.java | 26 ++ .../crcore/broadcast/BroadcastContext.java | 84 ++++++ .../crcore/broadcast/BroadcastService.java | 59 ++++ .../broadcast/CRCoreBroadcastListener.java | 190 +++++++++++++ .../broadcast/impl/YamlBroadcastService.java | 266 ++++++++++++++++++ .../crcore/command/builtin/CoreCommand.java | 27 +- .../command/builtin/CoreReloadSubCommand.java | 39 +++ src/main/resources/crcore-broadcasts.yml | 62 ++++ src/main/resources/crcore-messages.yml | 19 ++ 15 files changed, 1060 insertions(+), 22 deletions(-) create mode 100644 docs/diagrams/broadcasts-class-diagram.puml create mode 100644 src/main/java/fr/luc/crcore/broadcast/BroadcastAudience.java create mode 100644 src/main/java/fr/luc/crcore/broadcast/BroadcastContext.java create mode 100644 src/main/java/fr/luc/crcore/broadcast/BroadcastService.java create mode 100644 src/main/java/fr/luc/crcore/broadcast/CRCoreBroadcastListener.java create mode 100644 src/main/java/fr/luc/crcore/broadcast/impl/YamlBroadcastService.java create mode 100644 src/main/java/fr/luc/crcore/command/builtin/CoreReloadSubCommand.java create mode 100644 src/main/resources/crcore-broadcasts.yml diff --git a/docs/README.md b/docs/README.md index 0a1fc32..cf3811d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,6 +30,12 @@ d'initialisation côté plugin de jeu : YAML `-messages.yml` dans le dataFolder du plugin de jeu, avec defaults CR-Core en fallback. L'admin édite un seul fichier, placeholders nommés, codes couleur `&` natifs. +- **Broadcasts configurables** — `BroadcastService` route chaque event + CR-Core vers une liste d'audiences (`NONE`, `LEADER`, `TEAM`, `ADMIN`, + `ALL`) définie dans `-broadcasts.yml`. Séparation routes / + templates. Un listener interne wire les 12 events natifs ; les game + plugins peuvent broadcast leurs propres events. `/core reload` recharge + les deux fichiers à chaud. - **Bootstrap unique** — `new CRCore(this).enable()` dans le `onEnable()` du plugin de jeu, et tout est branché. @@ -55,6 +61,7 @@ d'initialisation côté plugin de jeu : | [events-diagram.puml](diagrams/events-diagram.puml) | Classe | Évènements Bukkit team + player | | [database-diagram.puml](diagrams/database-diagram.puml) | Classe | Wrapper SQLite + table builder | | [messages-class-diagram.puml](diagrams/messages-class-diagram.puml) | Classe | Service de messages YAML | +| [broadcasts-class-diagram.puml](diagrams/broadcasts-class-diagram.puml) | Classe | Service de broadcasts YAML + listener | | [bootstrap-sequence.puml](diagrams/bootstrap-sequence.puml) | Séquence | `CRCore.enable()` côté plugin de jeu | ## Conventions diff --git a/docs/decisions.md b/docs/decisions.md index 48f70e8..825032f 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -367,6 +367,42 @@ Format léger : une décision = un titre + contexte + choix + raison. bases existantes, ALTER TABLE manuel ou suppression du fichier (les bases d'event sont jetables). +## 2026-06-10 — Système de broadcasts configurables + `/core reload` + +- **Choix** : nouveau module `fr.luc.crcore.broadcast` avec + `BroadcastService` + `BroadcastAudience` enum + `BroadcastContext` data + class + `YamlBroadcastService` impl. Un listener Bukkit interne + (`CRCoreBroadcastListener`) écoute les 12 events CR-Core et les traduit + en appels `broadcast(eventKey, ctx)`. +- **Modèle « un seul fichier par plugin »** identique à messages : + `/-broadcasts.yml`. Defaults + bundlés dans le jar à `crcore-broadcasts.yml`, copiés au premier boot + (avec priorité au template du plugin de jeu sous le même nom s'il en + fournit un). +- **Séparation routes / templates** : + - **Routes** = qui reçoit quoi = `-broadcasts.yml` (liste + d'audiences par event) + - **Templates** = quel texte = `-messages.yml` (clés + `.broadcast`) + - L'admin peut modifier l'un sans toucher à l'autre. Modulaire. +- **5 audiences** : `NONE`, `LEADER`, `TEAM`, `ADMIN`, `ALL`. + Multi-cibles via liste, union sans doublon. +- **Permission ADMIN** : `crcore.broadcast.admin` (granular, + configurable côté LuckPerms). +- **Listener Bukkit interne** : `CRCoreBroadcastListener` est instancié + et enregistré dans `CRCore.enable()`. Les game plugins n'ont rien à + faire pour bénéficier du broadcast des events natifs CR-Core ; pour + leurs propres events, ils appellent `core.broadcasts().broadcast(...)`. +- **Pas de cancellation** : le broadcast est post-event ; si une route + est mal configurée, on ne casse pas la logique métier — au pire un + message non envoyé ou envoyé trop large. +- **Nouvelle commande `/core reload`** : permission `crcore.reload`, + recharge `messages` + `broadcasts` depuis les fichiers user. Les + defaults en jar restent fixes. Hot reload utile en dev / pour ajuster + les routes sans restart. +- **Override de l'impl** : `CRCore.buildBroadcastService(messages)` est + `protected` — comme pour les autres services. + ## 2026-06-09 — Réorganisation packages : `impl/` et `exception/` séparés - **Choix** : pour chaque domaine (`team`, `player`, `message`), les diff --git a/docs/diagrams/broadcasts-class-diagram.puml b/docs/diagrams/broadcasts-class-diagram.puml new file mode 100644 index 0000000..2528a33 --- /dev/null +++ b/docs/diagrams/broadcasts-class-diagram.puml @@ -0,0 +1,102 @@ +@startuml broadcasts-class-diagram +title CR-Core — Broadcast service (class diagram) + +skinparam classAttributeIconSize 0 +hide empty members + +package "fr.luc.crcore.broadcast" { + + enum BroadcastAudience { + NONE + LEADER + TEAM + ADMIN + ALL + } + + class BroadcastContext { + - team: Team + - involvedPlayerId: UUID + - placeholders: Map + -- + + {static} of(team): BroadcastContext + + {static} empty(): BroadcastContext + + involving(playerId): BroadcastContext + + with(key, value): BroadcastContext + + getTeam(): Optional + + getInvolvedPlayerId(): Optional + + getPlaceholders(): Map + + toPlaceholderPairs(): Object[] + } + + interface BroadcastService { + + broadcast(eventKey, context): void + + getAudiences(eventKey): List + + reload(): void + } + + class CRCoreBroadcastListener { + - broadcasts: BroadcastService + + registerOn(plugin: JavaPlugin): void + -- + @ onTeamCreate(TeamCreateEvent) + @ onTeamDissolve(TeamDissolveEvent) + @ onTeamMemberAdd(TeamMemberAddEvent) + @ onTeamMemberRemove(TeamMemberRemoveEvent) + @ onPlayerJoinTeam(PlayerJoinTeamEvent) + @ onLeadershipTransfer(TeamLeadershipTransferEvent) + @ onVisibilityChange(TeamVisibilityChangeEvent) + @ onTeamScoreChange(TeamScoreChangeEvent) + @ onTeamSpawnChange(TeamSpawnPointChangeEvent) + @ onProfileCreate(PlayerProfileCreateEvent) + @ onProfileDelete(PlayerProfileDeleteEvent) + @ onPlayerScoreChange(PlayerScoreChangeEvent) + } + CRCoreBroadcastListener ..|> "org.bukkit.event.Listener" + + package "fr.luc.crcore.broadcast.impl" { + class YamlBroadcastService { + - plugin: JavaPlugin + - messages: MessagesService + - defaults: Map> + - audiences: Map> + - userFile: File + -- + + YamlBroadcastService(plugin, messages) + - loadDefaultsFromResource(): void + - ensureUserFile(): void + - rebuildEffectiveAudiences(): void + - resolveRecipients(list, ctx): Set + } + YamlBroadcastService ..|> BroadcastService + } + + BroadcastService ..> BroadcastContext : consumes + BroadcastContext --> BroadcastAudience + YamlBroadcastService --> "fr.luc.crcore.message.MessagesService" : reads templates + CRCoreBroadcastListener --> BroadcastService : delegates + CRCoreBroadcastListener ..> BroadcastContext : builds +} + +package "fr.luc.crcore" { + class CRCore { + + broadcasts(): BroadcastService + # buildBroadcastService(messages): BroadcastService + } + CRCore "1" *-- "1" BroadcastService : owns + CRCore ..> CRCoreBroadcastListener : registers +} + +note bottom of YamlBroadcastService + Modèle "un seul fichier par plugin" : + + Sources en mémoire : + 1. crcore-broadcasts.yml ← jar (fallback) + 2. -broadcasts.yml ← dataFolder (édité par l'admin) + + Séparation routes / templates : + - Routes = ce fichier (qui reçoit quoi) + - Templates = MessagesService (clés .broadcast) +end note + +@enduml diff --git a/docs/features.md b/docs/features.md index 5f73ce9..8c7cdfd 100644 --- a/docs/features.md +++ b/docs/features.md @@ -684,7 +684,130 @@ placeholders documentés en commentaire. --- -## 9. Bootstrap `CRCore` +## 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/broadcasts-class-diagram.puml) + +--- + +## 10. Bootstrap `CRCore` **Statut** : implémenté. Point d'entrée unique pour les plugins de jeu. diff --git a/docs/setup.md b/docs/setup.md index 8b62055..aca12fc 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -265,6 +265,24 @@ CitesPlugin/ # dossier IntelliJ (renommer plus t └── SqlitePlayerProfileRepository.java ``` +## Fichiers de config générés au premier `enable()` + +Au premier démarrage, CR-Core crée DEUX fichiers dans le dataFolder : + +| Fichier | Rôle | +|---|---| +| `-messages.yml` | Templates de tous les messages (commandes + broadcasts) | +| `-broadcasts.yml` | Routes : qui reçoit quel event | + +Les deux suivent le même pattern : si ton plugin de jeu bundle un fichier +au même nom dans ses ressources, c'est lui qui sert de template initial à +la place des defaults CR-Core. Les defaults restent en mémoire en +fallback — donc les clés non présentes dans le fichier user marchent quand +même. + +Hot reload : `/core reload` (permission `crcore.reload`) relit les deux +fichiers sans restart. + ## Fichier messages Au premier `enable()`, CR-Core crée : diff --git a/src/main/java/fr/luc/crcore/CRCore.java b/src/main/java/fr/luc/crcore/CRCore.java index dd444f4..6afa0a3 100644 --- a/src/main/java/fr/luc/crcore/CRCore.java +++ b/src/main/java/fr/luc/crcore/CRCore.java @@ -1,5 +1,8 @@ package fr.luc.crcore; +import fr.luc.crcore.broadcast.BroadcastService; +import fr.luc.crcore.broadcast.CRCoreBroadcastListener; +import fr.luc.crcore.broadcast.impl.YamlBroadcastService; import fr.luc.crcore.command.builtin.CoreCommand; import fr.luc.crcore.database.Database; import fr.luc.crcore.message.MessagesService; @@ -80,6 +83,7 @@ public class CRCore { private PlayerProfileRepository playerProfileRepository; private PlayerProfileService playerProfileService; private MessagesService messages; + private BroadcastService broadcasts; private CoreCommand coreCommand; private boolean enabled = false; @@ -118,8 +122,12 @@ public class CRCore { this.playerProfileService = buildPlayerProfileService(playerProfileRepository); this.messages = buildMessagesService(); + this.broadcasts = buildBroadcastService(messages); - this.coreCommand = buildCoreCommand(teamService, playerProfileService, messages); + // Listener Bukkit qui route les events CR-Core vers le BroadcastService. + new CRCoreBroadcastListener(broadcasts).registerOn(plugin); + + this.coreCommand = buildCoreCommand(teamService, playerProfileService, messages, broadcasts); registerCommand(); registerPlaceholderHook(); @@ -188,8 +196,9 @@ public class CRCore { /** Construit le {@link CoreCommand}. Override pour ajouter des groupes top-level. */ protected CoreCommand buildCoreCommand(TeamService teamService, PlayerProfileService playerProfileService, - MessagesService messages) { - return new CoreCommand(teamService, playerProfileService, messages); + MessagesService messages, + BroadcastService broadcasts) { + return new CoreCommand(teamService, playerProfileService, messages, broadcasts); } /** Construit le {@link MessagesService}. Override pour utiliser une impl custom. */ @@ -197,6 +206,11 @@ public class CRCore { return new YamlMessagesService(plugin); } + /** Construit le {@link BroadcastService}. Override pour utiliser une impl custom. */ + protected BroadcastService buildBroadcastService(MessagesService messages) { + return new YamlBroadcastService(plugin, messages); + } + /** * Enregistre {@link #coreCommand} sous le nom configuré, avec fallback * dynamique. Stratégie : @@ -273,6 +287,8 @@ public class CRCore { public PlayerProfileService getPlayerProfileService() { return playerProfileService; } public MessagesService getMessages() { return messages; } public MessagesService messages() { return messages; } + public BroadcastService getBroadcasts() { return broadcasts; } + public BroadcastService broadcasts() { return broadcasts; } public CoreCommand getCoreCommand() { return coreCommand; } public boolean isEnabled() { return enabled; } } diff --git a/src/main/java/fr/luc/crcore/broadcast/BroadcastAudience.java b/src/main/java/fr/luc/crcore/broadcast/BroadcastAudience.java new file mode 100644 index 0000000..48e9485 --- /dev/null +++ b/src/main/java/fr/luc/crcore/broadcast/BroadcastAudience.java @@ -0,0 +1,26 @@ +package fr.luc.crcore.broadcast; + +/** + * Public destinataire d'un broadcast CR-Core. + * + *

Une clé d'event dans {@code -broadcasts.yml} mappe sur une + * liste d'audiences ; un joueur appartenant à plusieurs audiences ne + * reçoit qu'un seul exemplaire (union de Sets). + */ +public enum BroadcastAudience { + + /** Aucun broadcast. Équivalent à une liste vide. */ + NONE, + + /** Le chef de l'équipe concernée par l'event, s'il est en ligne. */ + LEADER, + + /** Tous les membres en ligne de l'équipe concernée par l'event. */ + TEAM, + + /** Joueurs en ligne ayant la permission {@code crcore.broadcast.admin}. */ + ADMIN, + + /** Tous les joueurs en ligne sur le serveur. */ + ALL +} diff --git a/src/main/java/fr/luc/crcore/broadcast/BroadcastContext.java b/src/main/java/fr/luc/crcore/broadcast/BroadcastContext.java new file mode 100644 index 0000000..2d14636 --- /dev/null +++ b/src/main/java/fr/luc/crcore/broadcast/BroadcastContext.java @@ -0,0 +1,84 @@ +package fr.luc.crcore.broadcast; + +import fr.luc.crcore.team.Team; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** + * Contexte d'un broadcast : team concernée (optionnelle), joueur impliqué + * (optionnel) et placeholders pour la substitution dans le template message. + * + *

Builder fluide : + *

{@code
+ * BroadcastContext.of(team)
+ *     .involving(player.getUniqueId())
+ *     .with("name", team.getName())
+ *     .with("tag", team.getTag());
+ * }
+ * + *

Utilisé par {@link BroadcastService#broadcast(String, BroadcastContext)} + * pour résoudre les audiences ({@link BroadcastAudience#LEADER}, + * {@link BroadcastAudience#TEAM}) et alimenter les placeholders du template. + */ +public final class BroadcastContext { + + private final Team team; + private UUID involvedPlayerId; + private final Map placeholders = new LinkedHashMap<>(); + + private BroadcastContext(Team team) { + this.team = team; + } + + /** Contexte centré sur une équipe (le cas le plus courant). */ + public static BroadcastContext of(Team team) { + return new BroadcastContext(team); + } + + /** Contexte sans équipe (events purement player, ex. PlayerProfileCreateEvent). */ + public static BroadcastContext empty() { + return new BroadcastContext(null); + } + + /** Précise le joueur impliqué dans l'event (ajouté, retiré, qui a rejoint, etc.). */ + public BroadcastContext involving(UUID playerId) { + this.involvedPlayerId = playerId; + return this; + } + + /** Ajoute un placeholder {@code {key}} → {@code value} pour la substitution dans le template. */ + public BroadcastContext with(String key, String value) { + if (key != null) { + placeholders.put(key, value != null ? value : ""); + } + return this; + } + + public Optional getTeam() { + return Optional.ofNullable(team); + } + + public Optional getInvolvedPlayerId() { + return Optional.ofNullable(involvedPlayerId); + } + + /** Map immuable des placeholders accumulés. */ + public Map getPlaceholders() { + return Collections.unmodifiableMap(placeholders); + } + + /** Convertit les placeholders en varargs pour {@code messages.get(key, ...)}. */ + public Object[] toPlaceholderPairs() { + Object[] pairs = new Object[placeholders.size() * 2]; + int i = 0; + for (Map.Entry entry : placeholders.entrySet()) { + pairs[i++] = entry.getKey(); + pairs[i++] = entry.getValue(); + } + return pairs; + } +} diff --git a/src/main/java/fr/luc/crcore/broadcast/BroadcastService.java b/src/main/java/fr/luc/crcore/broadcast/BroadcastService.java new file mode 100644 index 0000000..1b4e767 --- /dev/null +++ b/src/main/java/fr/luc/crcore/broadcast/BroadcastService.java @@ -0,0 +1,59 @@ +package fr.luc.crcore.broadcast; + +import java.util.List; + +/** + * Service de broadcasts CR-Core : décide à qui envoyer un message pour un + * event donné, selon une config YAML routière. + * + *

Routes vs templates

+ * + *

Le service ne stocke pas les textes — il ne lit que la config + * des audiences ({@link BroadcastAudience}) par event. Le texte vient du + * {@code MessagesService} (clé {@code .broadcast}, par exemple + * {@code team.create.broadcast}). + * + *

Modèle de fichier

+ * + *

Un seul fichier par plugin : + * {@code /-broadcasts.yml}. + * Defaults bundlés dans le jar à {@code crcore-broadcasts.yml}, copiés au + * premier démarrage. Couche défaut + couche fichier user en mémoire ; le + * fichier user écrase clé par clé. + * + *

Audiences

+ * + *

Chaque event mappe sur une liste de {@link BroadcastAudience}. L'union + * des Players résolus de chaque audience est destinataire. {@code [NONE]} ou + * liste vide → pas de broadcast. + * + *

Usage

+ * + *
{@code
+ * core.broadcasts().broadcast("team.create",
+ *     BroadcastContext.of(team)
+ *         .with("name", team.getName())
+ *         .with("tag", team.getTag())
+ *         .with("color", team.getColor().getChatColor().toString()));
+ * }
+ * + *

Le listener interne {@code CRCoreBroadcastListener} fait ça + * automatiquement pour les 12 events Bukkit livrés avec CR-Core. Les game + * plugins peuvent appeler {@link #broadcast} avec leurs propres clés. + */ +public interface BroadcastService { + + /** + * Tire un broadcast pour l'event {@code eventKey}, en résolvant les + * audiences configurées et en envoyant le template + * {@code .broadcast} (via {@code MessagesService}) à chaque + * destinataire. + */ + void broadcast(String eventKey, BroadcastContext context); + + /** Liste des audiences configurées pour cet event (vide si rien défini). */ + List getAudiences(String eventKey); + + /** Recharge la config depuis le disque (fichier user uniquement). */ + void reload(); +} diff --git a/src/main/java/fr/luc/crcore/broadcast/CRCoreBroadcastListener.java b/src/main/java/fr/luc/crcore/broadcast/CRCoreBroadcastListener.java new file mode 100644 index 0000000..9bff5f8 --- /dev/null +++ b/src/main/java/fr/luc/crcore/broadcast/CRCoreBroadcastListener.java @@ -0,0 +1,190 @@ +package fr.luc.crcore.broadcast; + +import fr.luc.crcore.player.event.PlayerProfileCreateEvent; +import fr.luc.crcore.player.event.PlayerProfileDeleteEvent; +import fr.luc.crcore.player.event.PlayerScoreChangeEvent; +import fr.luc.crcore.team.Team; +import fr.luc.crcore.team.event.PlayerJoinTeamEvent; +import fr.luc.crcore.team.event.TeamCreateEvent; +import fr.luc.crcore.team.event.TeamDissolveEvent; +import fr.luc.crcore.team.event.TeamLeadershipTransferEvent; +import fr.luc.crcore.team.event.TeamMemberAddEvent; +import fr.luc.crcore.team.event.TeamMemberRemoveEvent; +import fr.luc.crcore.team.event.TeamScoreChangeEvent; +import fr.luc.crcore.team.event.TeamSpawnPointChangeEvent; +import fr.luc.crcore.team.event.TeamVisibilityChangeEvent; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.Objects; +import java.util.UUID; + +/** + * Listener Bukkit interne qui traduit chaque event CR-Core en appel à + * {@link BroadcastService#broadcast}. Wire-up automatique au {@code + * CRCore.enable()}. + * + *

Mapping {@code Event → eventKey} : + * + * + * + * + * + * + * + * + * + * + * + * + * + *
{@code TeamCreateEvent} {@code team.create}
{@code TeamDissolveEvent} {@code team.dissolve}
{@code TeamMemberAddEvent} {@code team.member.add}
{@code TeamMemberRemoveEvent} {@code team.member.remove}
{@code PlayerJoinTeamEvent} {@code team.player.join}
{@code TeamLeadershipTransferEvent}{@code team.leadership.transfer}
{@code TeamVisibilityChangeEvent} {@code team.visibility.change}
{@code TeamScoreChangeEvent} {@code team.score.change}
{@code TeamSpawnPointChangeEvent} {@code team.spawn.change}
{@code PlayerProfileCreateEvent} {@code player.profile.create}
{@code PlayerProfileDeleteEvent} {@code player.profile.delete}
{@code PlayerScoreChangeEvent} {@code player.score.change}
+ * + *

Les placeholders standards injectés dans le contexte (à utiliser dans + * les templates {@code .broadcast} du fichier messages) : + *

    + *
  • {@code {name}}, {@code {tag}}, {@code {color}}, {@code {team_name}} — + * attributs de l'équipe (le {@code team_name} et {@code name} sont + * identiques, on garde les deux pour lisibilité des templates).
  • + *
  • {@code {player}} — nom du joueur impliqué quand pertinent.
  • + *
  • Spécifiques selon l'event : {@code {new_leader}}, {@code {old_value}}, + * {@code {new_value}}, {@code {score_name}}, {@code {new_visibility}}, + * etc.
  • + *
+ */ +public class CRCoreBroadcastListener implements Listener { + + private final BroadcastService broadcasts; + + public CRCoreBroadcastListener(BroadcastService broadcasts) { + this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts"); + } + + /** Enregistre le listener sur le {@link org.bukkit.plugin.PluginManager} du serveur. */ + public void registerOn(JavaPlugin plugin) { + Objects.requireNonNull(plugin, "plugin").getServer() + .getPluginManager().registerEvents(this, plugin); + } + + // ---- Team events ---- + + @EventHandler + public void onTeamCreate(TeamCreateEvent e) { + broadcasts.broadcast("team.create", teamContext(e.getTeam())); + } + + @EventHandler + public void onTeamDissolve(TeamDissolveEvent e) { + broadcasts.broadcast("team.dissolve", teamContext(e.getTeam())); + } + + @EventHandler + public void onTeamMemberAdd(TeamMemberAddEvent e) { + BroadcastContext ctx = teamContext(e.getTeam()) + .involving(e.getMember().getPlayerId()) + .with("player", resolveName(e.getMember().getPlayerId())); + broadcasts.broadcast("team.member.add", ctx); + } + + @EventHandler + public void onTeamMemberRemove(TeamMemberRemoveEvent e) { + BroadcastContext ctx = teamContext(e.getTeam()) + .involving(e.getPlayerId()) + .with("player", resolveName(e.getPlayerId())); + broadcasts.broadcast("team.member.remove", ctx); + } + + @EventHandler + public void onPlayerJoinTeam(PlayerJoinTeamEvent e) { + BroadcastContext ctx = teamContext(e.getTeam()) + .involving(e.getMember().getPlayerId()) + .with("player", resolveName(e.getMember().getPlayerId())); + broadcasts.broadcast("team.player.join", ctx); + } + + @EventHandler + public void onLeadershipTransfer(TeamLeadershipTransferEvent e) { + BroadcastContext ctx = teamContext(e.getTeam()) + .involving(e.getNewLeaderId()) + .with("new_leader", resolveName(e.getNewLeaderId())) + .with("old_leader", e.getOldLeaderId().map(this::resolveName).orElse("-")); + broadcasts.broadcast("team.leadership.transfer", ctx); + } + + @EventHandler + public void onVisibilityChange(TeamVisibilityChangeEvent e) { + BroadcastContext ctx = teamContext(e.getTeam()) + .with("old_visibility", e.getOldVisibility().name()) + .with("new_visibility", e.getNewVisibility().name()); + broadcasts.broadcast("team.visibility.change", ctx); + } + + @EventHandler + public void onTeamScoreChange(TeamScoreChangeEvent e) { + BroadcastContext ctx = teamContext(e.getTeam()) + .with("score_name", e.getScoreName()) + .with("old_value", String.valueOf(e.getOldValue())) + .with("new_value", String.valueOf(e.getNewValue())) + .with("delta", String.valueOf(e.getDelta())); + broadcasts.broadcast("team.score.change", ctx); + } + + @EventHandler + public void onTeamSpawnChange(TeamSpawnPointChangeEvent e) { + broadcasts.broadcast("team.spawn.change", teamContext(e.getTeam())); + } + + // ---- Player events ---- + + @EventHandler + public void onProfileCreate(PlayerProfileCreateEvent e) { + BroadcastContext ctx = BroadcastContext.empty() + .involving(e.getProfile().getPlayerId()) + .with("player", resolveName(e.getProfile().getPlayerId())); + broadcasts.broadcast("player.profile.create", ctx); + } + + @EventHandler + public void onProfileDelete(PlayerProfileDeleteEvent e) { + BroadcastContext ctx = BroadcastContext.empty() + .involving(e.getProfile().getPlayerId()) + .with("player", resolveName(e.getProfile().getPlayerId())); + broadcasts.broadcast("player.profile.delete", ctx); + } + + @EventHandler + public void onPlayerScoreChange(PlayerScoreChangeEvent e) { + UUID playerId = e.getProfile().getPlayerId(); + BroadcastContext ctx = BroadcastContext.empty() + .involving(playerId) + .with("player", resolveName(playerId)) + .with("score_name", e.getScoreName()) + .with("old_value", String.valueOf(e.getOldValue())) + .with("new_value", String.valueOf(e.getNewValue())) + .with("delta", String.valueOf(e.getDelta())); + broadcasts.broadcast("player.score.change", ctx); + } + + // ---- Helpers ---- + + /** Construit un contexte avec les placeholders standards d'une équipe. */ + private BroadcastContext teamContext(Team team) { + return BroadcastContext.of(team) + .with("name", team.getName()) + .with("team_name", team.getName()) + .with("tag", team.getTag()) + .with("color", team.getColor().getChatColor().toString()) + .with("visibility", team.getVisibility().name()); + } + + @SuppressWarnings("deprecation") + private String resolveName(UUID playerId) { + if (playerId == null) return "-"; + OfflinePlayer p = Bukkit.getOfflinePlayer(playerId); + String name = p.getName(); + return name != null ? name : playerId.toString(); + } +} diff --git a/src/main/java/fr/luc/crcore/broadcast/impl/YamlBroadcastService.java b/src/main/java/fr/luc/crcore/broadcast/impl/YamlBroadcastService.java new file mode 100644 index 0000000..ef774f8 --- /dev/null +++ b/src/main/java/fr/luc/crcore/broadcast/impl/YamlBroadcastService.java @@ -0,0 +1,266 @@ +package fr.luc.crcore.broadcast.impl; + +import fr.luc.crcore.broadcast.BroadcastAudience; +import fr.luc.crcore.broadcast.BroadcastContext; +import fr.luc.crcore.broadcast.BroadcastService; +import fr.luc.crcore.message.MessagesService; +import fr.luc.crcore.team.Team; +import fr.luc.crcore.team.TeamMember; +import org.bukkit.Bukkit; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implémentation par défaut de {@link BroadcastService}, basée sur les YAML + * Bukkit. Modèle "un seul fichier par plugin" (voir {@link BroadcastService}). + * + *

Architecture identique à {@code YamlMessagesService} : + *

    + *
  1. Defaults bundlés chargés depuis {@code crcore-broadcasts.yml} dans + * le jar — toujours en mémoire en fallback.
  2. + *
  3. Fichier user {@code /-broadcasts.yml} créé au + * premier boot (depuis le template du plugin de jeu s'il en bundle + * un, sinon les defaults CR-Core).
  4. + *
  5. Lecture : fichier user écrase les defaults clé par clé.
  6. + *
+ * + *

La permission admin pour {@link BroadcastAudience#ADMIN} est + * {@code crcore.broadcast.admin}. + */ +public class YamlBroadcastService implements BroadcastService { + + /** Nom de la ressource bundlée (embarquée dans le jar CR-Core / shadée). */ + private static final String CRCORE_DEFAULTS_RESOURCE = "crcore-broadcasts.yml"; + + /** Permission qui détermine l'audience {@link BroadcastAudience#ADMIN}. */ + private static final String ADMIN_PERMISSION = "crcore.broadcast.admin"; + + private final JavaPlugin plugin; + private final MessagesService messages; + private final Logger logger; + /** Couche défauts (immutable après load). */ + private final Map> defaults = new HashMap<>(); + /** Couche effective : defaults + fichier user. */ + private final Map> audiences = new HashMap<>(); + private final String userFileName; + private final File userFile; + + public YamlBroadcastService(JavaPlugin plugin, MessagesService messages) { + this.plugin = Objects.requireNonNull(plugin, "plugin"); + this.messages = Objects.requireNonNull(messages, "messages"); + this.logger = plugin.getLogger(); + this.userFileName = plugin.getName().toLowerCase(Locale.ROOT) + "-broadcasts.yml"; + this.userFile = new File(plugin.getDataFolder(), userFileName); + initialize(); + } + + private void initialize() { + loadDefaultsFromResource(); + ensureUserFile(); + rebuildEffectiveAudiences(); + } + + private void loadDefaultsFromResource() { + try (InputStream is = plugin.getResource(CRCORE_DEFAULTS_RESOURCE)) { + if (is == null) { + logger.warning("Ressource " + CRCORE_DEFAULTS_RESOURCE + + " introuvable dans le jar — defaults broadcasts indisponibles."); + return; + } + YamlConfiguration cfg = YamlConfiguration.loadConfiguration( + new InputStreamReader(is, StandardCharsets.UTF_8)); + flatten(cfg, "", defaults); + } catch (IOException ex) { + logger.log(Level.WARNING, "Échec lecture defaults broadcasts CR-Core", ex); + } + } + + private void ensureUserFile() { + if (userFile.exists()) return; + if (!plugin.getDataFolder().exists() && !plugin.getDataFolder().mkdirs()) { + logger.warning("Impossible de créer " + plugin.getDataFolder()); + return; + } + // Priorité 1 : ressource du plugin de jeu sous le même nom. + try (InputStream pluginResource = plugin.getResource(userFileName)) { + if (pluginResource != null) { + copyStreamToFile(pluginResource, userFile); + logger.info("Fichier broadcasts créé depuis le template du plugin : " + userFileName); + return; + } + } catch (IOException ex) { + logger.log(Level.WARNING, "Échec copie du template plugin", ex); + } + // Priorité 2 : copie des defaults CR-Core. + try (InputStream coreResource = plugin.getResource(CRCORE_DEFAULTS_RESOURCE)) { + if (coreResource != null) { + copyStreamToFile(coreResource, userFile); + logger.info("Fichier broadcasts créé depuis les defaults CR-Core : " + userFileName); + } + } catch (IOException ex) { + logger.log(Level.WARNING, "Échec copie des defaults broadcasts", ex); + } + } + + private static void copyStreamToFile(InputStream in, File target) throws IOException { + try (FileOutputStream out = new FileOutputStream(target)) { + in.transferTo(out); + } + } + + /** Recompose la map effective : defaults + override du fichier user. */ + private void rebuildEffectiveAudiences() { + audiences.clear(); + audiences.putAll(defaults); + if (userFile.exists()) { + try { + YamlConfiguration cfg = YamlConfiguration.loadConfiguration(userFile); + flatten(cfg, "", audiences); + } catch (Exception ex) { + logger.log(Level.WARNING, "Échec chargement " + userFile, ex); + } + } + } + + // ---- BroadcastService API ---- + + @Override + public void broadcast(String eventKey, BroadcastContext context) { + Objects.requireNonNull(eventKey, "eventKey"); + Objects.requireNonNull(context, "context"); + List audiencesList = getAudiences(eventKey); + if (audiencesList.isEmpty() || audiencesList.contains(BroadcastAudience.NONE) + && audiencesList.size() == 1) { + return; + } + + Set recipients = resolveRecipients(audiencesList, context); + if (recipients.isEmpty()) return; + + String messageKey = eventKey + ".broadcast"; + if (!messages.has(messageKey)) { + // Pas de template défini — silent (l'admin a configuré un broadcast + // sans message associé, c'est une no-op intentionnelle). + return; + } + String text = messages.get(messageKey, context.toPlaceholderPairs()); + for (Player p : recipients) { + p.sendMessage(text); + } + } + + @Override + public List getAudiences(String eventKey) { + return audiences.getOrDefault(eventKey, Collections.emptyList()); + } + + @Override + public void reload() { + rebuildEffectiveAudiences(); + } + + // ---- Résolution des audiences en Players ---- + + private Set resolveRecipients(List list, BroadcastContext context) { + Set result = new LinkedHashSet<>(); + for (BroadcastAudience audience : list) { + switch (audience) { + case NONE: + break; + case LEADER: + context.getTeam().flatMap(Team::getLeaderId) + .map(Bukkit::getPlayer) + .filter(Objects::nonNull) + .ifPresent(result::add); + break; + case TEAM: + context.getTeam().ifPresent(team -> { + for (TeamMember m : team.getMembers()) { + Player p = Bukkit.getPlayer(m.getPlayerId()); + if (p != null) result.add(p); + } + }); + break; + case ADMIN: + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.hasPermission(ADMIN_PERMISSION)) result.add(p); + } + break; + case ALL: + result.addAll(Bukkit.getOnlinePlayers()); + break; + } + } + return result; + } + + // ---- Parsing du YAML : section imbriquée → map plate ---- + + /** + * Parcourt récursivement la {@link ConfigurationSection} et pousse chaque + * feuille (liste de strings ou string unique) en clé plate. Les valeurs + * inconnues sont loggées et ignorées. + */ + private void flatten(ConfigurationSection section, String prefix, + Map> out) { + for (String key : section.getKeys(false)) { + String fullKey = prefix.isEmpty() ? key : prefix + "." + key; + Object value = section.get(key); + if (value instanceof ConfigurationSection) { + flatten((ConfigurationSection) value, fullKey, out); + } else { + List parsed = parseAudienceValue(fullKey, value); + if (parsed != null) { + out.put(fullKey, parsed); + } + } + } + } + + private List parseAudienceValue(String key, Object value) { + if (value == null) return Collections.emptyList(); + List rawTokens = new ArrayList<>(); + if (value instanceof List) { + for (Object item : (List) value) { + if (item != null) rawTokens.add(item.toString()); + } + } else { + rawTokens.add(value.toString()); + } + List result = new ArrayList<>(rawTokens.size()); + Set seen = new HashSet<>(); + for (String token : rawTokens) { + String trimmed = token.trim(); + if (trimmed.isEmpty()) continue; + try { + BroadcastAudience aud = BroadcastAudience.valueOf(trimmed.toUpperCase(Locale.ROOT)); + if (seen.add(aud)) result.add(aud); + } catch (IllegalArgumentException ex) { + logger.warning("Audience inconnue '" + trimmed + "' pour la clé '" + key + + "' — ignorée. Valeurs valides : NONE, LEADER, TEAM, ADMIN, ALL."); + } + } + return result; + } +} diff --git a/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java b/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java index b25424a..f683da2 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java @@ -1,5 +1,6 @@ package fr.luc.crcore.command.builtin; +import fr.luc.crcore.broadcast.BroadcastService; import fr.luc.crcore.command.BaseCommand; import fr.luc.crcore.command.CommandResult; import fr.luc.crcore.command.builtin.team.TeamGroupSubCommand; @@ -11,43 +12,33 @@ import org.bukkit.command.CommandSender; import java.util.Objects; /** - * Commande racine {@code /core}. Container des groupes par défaut. - * - *

Branchée par {@code CRCore.enable()} sur la {@code PluginCommand "core"} - * du plugin de jeu (ou enregistrée dynamiquement via le {@code CommandMap} - * si elle n'est pas dans le {@code plugin.yml}). - * - *

Override

- * Pour remplacer un groupe entier : - *
{@code
- * core.getCoreCommand().replaceSubCommand("team", new MyTeamGroup(svc, msgs));
- * }
- * Pour remplacer une feuille : - *
{@code
- * core.getCoreCommand().findSubCommand("team")
- *     .ifPresent(t -> t.replaceSubCommand("create", new MyCreate(svc, msgs)));
- * }
+ * Commande racine {@code /core}. Container des groupes par défaut + la + * sous-commande {@link CoreReloadSubCommand} ({@code /core reload}). */ public class CoreCommand extends BaseCommand { protected final TeamService teamService; protected final PlayerProfileService playerProfileService; protected final MessagesService messages; + protected final BroadcastService broadcasts; public CoreCommand(TeamService teamService, PlayerProfileService playerProfileService, - MessagesService messages) { + MessagesService messages, + BroadcastService broadcasts) { super("core"); this.teamService = Objects.requireNonNull(teamService, "teamService"); this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService"); this.messages = Objects.requireNonNull(messages, "messages"); + this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts"); description("Commandes du noyau CR-Core"); registerDefaults(); } - /** Enregistre les groupes par défaut. Override pour ajouter / retirer des groupes. */ + /** Enregistre les groupes par défaut + la sous-commande reload. */ protected void registerDefaults() { addSubCommand(new TeamGroupSubCommand(teamService, messages)); + addSubCommand(new CoreReloadSubCommand(messages, broadcasts)); } /** diff --git a/src/main/java/fr/luc/crcore/command/builtin/CoreReloadSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/CoreReloadSubCommand.java new file mode 100644 index 0000000..cd63323 --- /dev/null +++ b/src/main/java/fr/luc/crcore/command/builtin/CoreReloadSubCommand.java @@ -0,0 +1,39 @@ +package fr.luc.crcore.command.builtin; + +import fr.luc.crcore.broadcast.BroadcastService; +import fr.luc.crcore.command.CommandContext; +import fr.luc.crcore.command.CommandResult; +import fr.luc.crcore.command.SubCommand; +import fr.luc.crcore.message.MessagesService; + +import java.util.Objects; + +/** + * {@code /core reload} — recharge les fichiers + * {@code -messages.yml} et {@code -broadcasts.yml} depuis + * le disque, sans restart du serveur. + * + *

Permission : {@code crcore.reload}. Les defaults bundlés dans le jar + * ne sont pas re-chargés (ils ne bougent pas pendant le runtime), seulement + * les fichiers user en dataFolder. + */ +public class CoreReloadSubCommand extends SubCommand { + + protected final MessagesService messages; + protected final BroadcastService broadcasts; + + public CoreReloadSubCommand(MessagesService messages, BroadcastService broadcasts) { + super("reload"); + this.messages = Objects.requireNonNull(messages, "messages"); + this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts"); + description("Recharger les fichiers messages et broadcasts"); + permission("crcore.reload"); + } + + @Override + public CommandResult execute(CommandContext ctx) { + messages.reload(); + broadcasts.reload(); + return CommandResult.success(messages.get("common.reload.success")); + } +} diff --git a/src/main/resources/crcore-broadcasts.yml b/src/main/resources/crcore-broadcasts.yml new file mode 100644 index 0000000..f52fbfa --- /dev/null +++ b/src/main/resources/crcore-broadcasts.yml @@ -0,0 +1,62 @@ +# ============================================================================= +# CR-Core — broadcasts par défaut +# ----------------------------------------------------------------------------- +# Ce fichier est embarqué dans le jar CR-Core et copié dans : +# /-broadcasts.yml +# au premier démarrage. C'est CE fichier (la copie en dataFolder) que tu édites. +# +# Pour chaque event CR-Core, on définit la liste d'AUDIENCES qui reçoivent +# le broadcast. Valeurs supportées : +# NONE → rien (équivalent à []) +# LEADER → le chef de l'équipe concernée (si en ligne) +# TEAM → tous les membres en ligne de l'équipe concernée +# ADMIN → joueurs en ligne avec la perm 'crcore.broadcast.admin' +# ALL → tous les joueurs en ligne sur le serveur +# +# Multi-cibles : liste, ex. [TEAM, ADMIN]. Union → pas de doublon. +# +# Le TEXTE des broadcasts est dans le fichier messages (clé .broadcast, +# ex. team.create.broadcast). Ce fichier-ci ne contient QUE des routes. +# ============================================================================= + +team: + # Création d'une équipe (admin) + create: [ADMIN] + + # Dissolution d'une équipe (admin) + dissolve: [TEAM, ADMIN] + + # Ajout d'un membre par le chef ou l'admin + member: + add: [TEAM] + remove: [TEAM] + + # Le joueur a auto-rejoint une équipe PUBLIC + player: + join: [TEAM] + + # Transfert / réassignation du leadership + leadership: + transfer: [TEAM, ADMIN] + + # Visibilité PUBLIC/PRIVATE + visibility: + change: [LEADER] + + # Changement d'un score d'équipe (peut être très noisy) + score: + change: [NONE] + + # Définition / clear du point de spawn + spawn: + change: [LEADER] + +player: + profile: + # Création du profil (en général à la 1re action de score — silent par défaut) + create: [NONE] + delete: [ADMIN] + + score: + # Changement d'un score joueur (très noisy si activé) + change: [NONE] diff --git a/src/main/resources/crcore-messages.yml b/src/main/resources/crcore-messages.yml index d611ab2..74439f5 100644 --- a/src/main/resources/crcore-messages.yml +++ b/src/main/resources/crcore-messages.yml @@ -19,6 +19,8 @@ common: player-only: "&cSeul un joueur peut utiliser cette commande." failure: "&cÉchec de la commande." invalid-usage: "&cUsage incorrect." + reload: + success: "&aMessages et broadcasts rechargés." team: # Placeholders : {name} @@ -109,3 +111,20 @@ team: header-score: "&eTop {count} ({score}) :" # Placeholders : {rank}, {color}, {name}, {value} entry: "&7 {rank}. {color}{name} &7— &f{value}" + +# ----------------------------------------------------------------------------- +# Messages de BROADCAST — envoyés aux audiences configurées dans +# -broadcasts.yml. Clé = .broadcast. +# ----------------------------------------------------------------------------- +team.create.broadcast: "&7[CR] &fNouvelle équipe : {color}[{tag}] {name}" +team.dissolve.broadcast: "&7[CR] &fL'équipe {color}{name}&f a été dissoute." +team.member.add.broadcast: "&7[CR] &f{player} rejoint {color}{team_name}&f." +team.member.remove.broadcast: "&7[CR] &f{player} quitte {color}{team_name}&f." +team.player.join.broadcast: "&7[CR] &f{player} rejoint {color}{team_name}&f." +team.leadership.transfer.broadcast: "&7[CR] &fNouveau chef de {color}{team_name}&f : {new_leader}." +team.visibility.change.broadcast: "&7[CR] &fVisibilité de {color}{team_name}&f : {new_visibility}." +team.score.change.broadcast: "&7[CR] &f{color}{team_name}&f : {score_name} {old_value} → &f{new_value}" +team.spawn.change.broadcast: "&7[CR] &fSpawn de {color}{team_name}&f mis à jour." +player.profile.create.broadcast: "&7[CR] &f{player} a maintenant un profil CR-Core." +player.profile.delete.broadcast: "&7[CR] &fProfil de {player} supprimé." +player.score.change.broadcast: "&7[CR] &f{player} : {score_name} {old_value} → &f{new_value}"