docs: document MessagesService — single per-plugin YAML, defaults in jar

features.md: new section 8 "Service de messages" with the single-file model,
two-layer in-memory (jar defaults + user file), API examples,
template-override mechanism, and override-the-impl extension point.
Bootstrap section renumbered to 9.

decisions.md: new decision logging the choice of MessagesService design
(YAML, single per-plugin file, in-jar fallback for new keys, template
priority from the game plugin's own resource, programmatic set() for
dynamic cases).

README.md: feature list mentions externalized messages; diagrams index
adds messages-class-diagram.puml.

setup.md: new "Fichier messages" section explaining the file location,
how to seed it from a game plugin's bundled template, and the API for
hot reload / extras / programmatic set().

New diagram: docs/diagrams/messages-class-diagram.puml — MessagesService
interface, YamlMessagesService impl, link to CRCore, with explanatory
note on the two-source merge and template fallback at first boot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-09 16:06:19 +02:00
parent f92f22f6c8
commit 4651ccbe69
5 changed files with 242 additions and 1 deletions
+5
View File
@@ -26,6 +26,10 @@ d'initialisation côté plugin de jeu :
- **Persistance SQLite** — wrapper `Database` + `TableBuilder` fluide,
repositories SQLite write-through, table custom en 2 lignes pour les
plugins downstream.
- **Messages externalisés** — `MessagesService` charge un seul fichier
YAML `<plugin>-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.
- **Bootstrap unique** — `new CRCore(this).enable()` dans le `onEnable()`
du plugin de jeu, et tout est branché.
@@ -50,6 +54,7 @@ d'initialisation côté plugin de jeu :
| [builtin-commands-diagram.puml](diagrams/builtin-commands-diagram.puml) | Classe | Arbre des commandes `/core team ...` |
| [events-diagram.puml](diagrams/events-diagram.puml) | Classe | Évènements Bukkit team + player |
| [database-diagram.puml](diagrams/database-diagram.puml) | Classe | Wrapper SQLite + table builder |
| [messages-class-diagram.puml](diagrams/messages-class-diagram.puml) | Classe | Service de messages YAML |
| [bootstrap-sequence.puml](diagrams/bootstrap-sequence.puml) | Séquence | `CRCore.enable()` côté plugin de jeu |
## Conventions
+30
View File
@@ -367,6 +367,36 @@ 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-09 — `MessagesService` : YAML externalisable, un seul fichier par plugin
- **Choix** : nouveau module `fr.luc.crcore.message` avec une interface
`MessagesService` et une impl `YamlMessagesService`. Toutes les chaînes
utilisateur des commandes built-in passent par ce service.
- **Modèle « un seul fichier par plugin »** :
- Defaults CR-Core embarqués dans le jar à
`resources/crcore-messages.yml`**jamais écrits sur disque**, juste
chargés en mémoire comme couche de fallback.
- Fichier user unique :
`<plugin-dataFolder>/<plugin-name-lowercase>-messages.yml`. Auto-créé
au premier `enable()` à partir du template du plugin de jeu s'il en
bundle un sous le même nom, sinon à partir des defaults CR-Core.
- Lecture : le fichier user écrase les defaults sur les mêmes clés ;
une clé manquante retombe automatiquement sur le default CR-Core (donc
une future release CR-Core qui ajoute une clé marche sans intervention
admin).
- **Pourquoi un seul fichier** : (1) UX admin — il édite un fichier, pas
deux ; (2) le plugin de jeu peut pré-remplir le template avec ses
overrides + ses propres messages en bundlant simplement son fichier
homonyme dans les ressources ; (3) zéro maintenance pour les clés
inchangées — elles restent en jar.
- **Substitution** : placeholders `{name}` style, varargs key/value,
codes couleur `&` traduits automatiquement.
- **Override de l'impl** : `CRCore.buildMessagesService()` protected,
surchargeable pour passer à une autre source (DB, microservice, etc.).
- **Pas de programmatique-only** : le service supporte `set(key, template)`
en mémoire pour des cas dynamiques, mais le mode principal reste le
YAML pour l'éditabilité par l'admin sans recompile.
## 2026-06-09 — Toutes les commandes "chef" deviennent admin (révision)
- **Révision** de la décision "Refonte permissions + modèle admin/chef/joueur"
+63
View File
@@ -0,0 +1,63 @@
@startuml messages-class-diagram
title CR-Core — Messages service (class diagram)
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.message" {
interface MessagesService {
+ get(key, placeholderPairs...): String
+ raw(key): String
+ has(key): boolean
+ set(key, template): void
+ reload(): void
+ loadAdditional(resourceName): void
+ setApplyColorCodes(enabled): void
+ isApplyColorCodes(): boolean
+ getUserFile(): File
}
class YamlMessagesService {
- plugin: JavaPlugin
- defaults: Map<String, String> ' in-memory (jar resource)
- messages: Map<String, String> ' effective (defaults + user file)
- userFileName: String ' <plugin>-messages.yml
- userFile: File
- applyColorCodes: boolean
--
+ YamlMessagesService(plugin: JavaPlugin)
--
- loadDefaultsFromResource(): void ' charge crcore-messages.yml (jar)
- ensureUserFile(): void ' copie template si absent
- rebuildEffectiveMessages(): void ' defaults + user file
- {static} flatten(section, prefix, out): void
}
YamlMessagesService ..|> MessagesService
}
package "fr.luc.crcore" {
class CRCore {
+ messages(): MessagesService
# buildMessagesService(): MessagesService ' override point
}
CRCore "1" *-- "1" MessagesService : owns
}
note bottom of YamlMessagesService
Modèle "un seul fichier par plugin" :
Sources en mémoire (la 2e écrase la 1ère sur clés communes) :
1. crcore-messages.yml ← jar (toujours présent, fallback)
2. <plugin>-messages.yml ← dataFolder (édité par l'admin)
Création du fichier user au 1er boot :
Priorité 1 : ressource du plugin de jeu sous le même nom
Priorité 2 : copie des defaults CR-Core
Placeholders : {name} via varargs key/value
Codes couleur : &a → §a (toggle)
end note
@enduml
+115 -1
View File
@@ -570,7 +570,121 @@ la hook.
---
## 8. Bootstrap `CRCore`
## 8. Service de messages (`fr.luc.crcore.message`)
**Statut** : implémenté. Toutes les commandes built-in `/core team ...` passent
par ce service ; plus rien n'est hardcodé dans le code.
### Modèle « un seul fichier par plugin »
Au premier démarrage, CR-Core crée **un seul fichier** dans le dataFolder du
plugin de jeu :
```
<plugin-dataFolder>/<plugin-name-lowercase>-messages.yml
```
(par exemple `cites-messages.yml` si le plugin s'appelle `Cites`.) C'est *le*
fichier que l'admin du serveur édite. Pas de `crcore-messages.yml` séparé sur
disque — les defaults CR-Core vivent dans le jar et sont chargés en mémoire
comme couche de fallback.
### Deux couches en mémoire
| Ordre | Source | Mutabilité |
|---|---|---|
| 1 | `crcore-messages.yml` embarqué dans le jar CR-Core | Read-only, in-memory |
| 2 | `<plugin>-messages.yml` dans le dataFolder | Lecture du disque |
La couche 2 écrase la couche 1 sur les clés communes. Une clé manquante dans
le fichier user **retombe automatiquement** sur le default CR-Core. Si une
future release CR-Core ajoute une nouvelle clé, l'admin n'a rien à faire — ça
marche immédiatement.
### Création du fichier user au premier démarrage
L'ordre de priorité pour générer le starter file :
1. Si le plugin de jeu **bundle son propre** `<plugin-name-lowercase>-messages.yml`
dans ses ressources → c'est ce fichier qui devient le template (donc tu peux
pré-remplir avec tes overrides + tes messages perso au build).
2. Sinon → copie des defaults CR-Core comme starter (l'admin voit toutes les
clés CR-Core et peut les éditer).
### API `MessagesService`
```java
// Lecture avec placeholders nommés
core.messages().get("team.create.success",
"name", team.getName(),
"tag", team.getTag(),
"visibility", team.getVisibility().name());
// → "&aÉquipe Wolves [#WOLF] créée (PRIVATE, sans chef)." (codes & déjà traduits)
// Lecture brute (sans substitution ni couleur)
core.messages().raw("team.create.success");
// Ajout/override programmatique en mémoire (non persisté)
core.messages().set("mygame.welcome", "&aBienvenue {player} !");
// Charge un fichier YAML additionnel (en plus du fichier user principal)
core.messages().loadAdditional("my-extras.yml");
// Hot reload (relit le fichier user, garde les defaults en mémoire)
core.messages().reload();
// Toggle codes couleur
core.messages().setApplyColorCodes(true); // défaut
// Chemin du fichier user (informationnel)
File path = core.messages().getUserFile();
```
### Placeholders
Format `{name}` avec substitution via varargs paire-par-paire. Codes couleur
Bukkit `&a`, `&c`, `&7`, `&f`, etc. traduits automatiquement en `§…`.
### Clés manquantes
Si une clé n'existe ni dans le fichier user ni dans les defaults,
`messages.get(...)` renvoie `[missing: key]` pour rendre visible la clé qu'il
manque (debug facile).
### Override côté plugin de jeu
**Option 1 — Bundler son propre template** dans les ressources du plugin de
jeu, fichier nommé `<plugin-name-lowercase>-messages.yml`. À la première
exécution, ce fichier sert de starter dans le dataFolder.
**Option 2 — Éditer le fichier généré** après premier démarrage. Le YAML
contient déjà les clés CR-Core ; ajoute / modifie / supprime ce que tu veux.
**Option 3 — Override programmatique** :
```java
core.messages().set("team.create.success", "Custom format for {name}");
```
### Override de l'impl
`CRCore.buildMessagesService()` est `protected`. Sous-classe CRCore et
redéfinis-la pour une impl custom (ex. messages venant d'une base de données,
d'un microservice de traduction, etc.).
### Fichier `crcore-messages.yml` — référence
~25 clés organisées en sections : `common.*` (no-permission, player-only, …),
`team.create.*`, `team.delete.success`, `team.add.*`, etc. Voir le fichier
dans `src/main/resources/crcore-messages.yml` pour la liste complète avec les
placeholders documentés en commentaire.
### Diagramme
- Classes : [messages-class-diagram.puml](diagrams/messages-class-diagram.puml)
---
## 9. Bootstrap `CRCore`
**Statut** : implémenté. Point d'entrée unique pour les plugins de jeu.
+29
View File
@@ -257,6 +257,35 @@ CitesPlugin/ # dossier IntelliJ (renommer plus t
└── PlayerScoreChangeEvent.java
```
## Fichier messages
Au premier `enable()`, CR-Core crée :
```
<plugin-dataFolder>/<plugin-name-lowercase>-messages.yml
```
(ex. `cites-messages.yml` si ton plugin s'appelle `Cites`)
Édite ce fichier pour customiser tous les messages des commandes `/core team
...`. Les clés non présentes retombent sur les defaults CR-Core (dans le jar)
— donc tu peux n'inclure que ce que tu veux changer.
Pour pré-remplir ce fichier avec tes overrides + tes propres messages dès la
première exécution, bundle un fichier au **même nom** dans tes ressources
plugin (`src/main/resources/cites-messages.yml`). Si présent, c'est lui qui
sert de template au lieu des defaults CR-Core.
API en code :
```java
core.messages().get("team.create.success",
"name", team.getName(), "tag", team.getTag());
core.messages().set("mygame.custom", "&aHello {player} !");
core.messages().loadAdditional("my-extras.yml");
core.messages().reload(); // hot reload depuis le disque
```
## Tables SQLite créées par CR-Core
Au premier `enable()`, les tables suivantes sont créées (en `IF NOT EXISTS`) :