feat: typed team settings (cascade per-team → global → default) + in-game GUI

New fr.luc.crcore.team.config module:
- TeamSetting<T> (typed, with key/type/default/parser/serializer; factories
  ofBoolean/ofInt/ofString/ofEnum).
- TeamSettings registry: 8 standard settings (FRIENDLY_FIRE,
  PVP_PROTECTION_SECONDS, MAX_SIZE, MIN_SIZE, RESPAWN_AT_TEAM_SPAWN,
  TEAM_CHAT_ENABLED, SHOW_TAG_ABOVE_HEAD, TEAM_COLOR_IN_NAME), extensible
  via register() for game plugins.
- TeamConfigService interface with cascade get(team, setting) →
  per-team override (SQLite) → global YAML → hard default. Persists per-
  team via TeamRepository.save(), global via YamlConfiguration.save().
- YamlTeamConfigService default impl with bundled crcore-team-config.yml.

Storage:
- Team.getSettings() Map<String, Object> for per-team overrides.
- New SQLite table crcore_team_settings (team_id, key, value, type) with
  load + write-through persist in SqliteTeamRepository.
- Global YAML <plugin>-team-config.yml in dataFolder, auto-created at
  first boot (template from game plugin's resource of the same name
  takes priority).

New reusable GUI framework fr.luc.crcore.gui:
- AbstractInventoryGui (implements InventoryHolder, rebuild() abstract,
  setButton/setDecoration/clearSlot helpers, onClose hook, openTo()).
- GuiClickHandler FunctionalInterface.
- GuiListener (single Bukkit listener, detects via getHolder(), ALWAYS
  cancels clicks even on slots without handlers).
- GuiItems builder (named/of/filler + lore/amount/build, '&' color codes
  translated).

Concrete settings GUIs (fr.luc.crcore.team.config.gui):
- AbstractSettingsGui base renderer: 27 slots, settings in row 2,
  booleans = LIME_DYE / GRAY_DYE toggle, integers = BOOK with left +1 /
  right -1 (shift × 10), strings/enums display-only.
- GlobalSettingsGui: writes to YAML on each change.
- TeamSettingsGui: writes to per-team overrides, "override active" flag
  in lore when value differs from global, "Reset all overrides" footer
  button.

New /core team settings [team] subcommand:
- No arg → GlobalSettingsGui (perm crcore.team.settings.global).
- With arg → TeamSettingsGui (perm crcore.team.settings).
- Player-only (Bukkit needs HumanEntity to open inventory).
- Lives under /core team to stay modular (objective: split into modules
  later; everything team-related under /core team).

CRCore: buildTeamConfigService() override point, teamConfig()/getTeamConfig()
getters, GuiListener.registerOn(plugin) at enable(). CoreCommand,
TeamGroupSubCommand and CoreReloadSubCommand extended to receive
TeamConfigService. /core reload now reloads messages + broadcasts +
team-config.

Docs: new section 10 "Paramètres d'équipe", new decisions logged,
setup.md tree updated, two new diagrams (team-config + gui).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-10 11:46:51 +02:00
parent a94bc56a5b
commit 75d2fa866d
25 changed files with 1686 additions and 26 deletions
+10
View File
@@ -36,6 +36,14 @@ d'initialisation côté plugin de jeu :
templates. Un listener interne wire les 12 events natifs ; les game
plugins peuvent broadcast leurs propres events. `/core reload` recharge
les deux fichiers à chaud.
- **Paramètres d'équipe** — `TeamConfigService` typé avec cascade
per-team → global → default. 8 settings standards
(`FRIENDLY_FIRE`, `MAX_SIZE`, etc.), étendable. Globaux dans
`<plugin>-team-config.yml`, per-team en SQLite. GUI in-game via
`/core team settings [team]`.
- **Framework GUI** — `AbstractInventoryGui` + `GuiListener` réutilisable
pour tout GUI custom. Détection par holder, clic toujours annulé,
`GuiItems` builder fluide avec codes couleur `&`.
- **Bootstrap unique** — `new CRCore(this).enable()` dans le `onEnable()`
du plugin de jeu, et tout est branché.
@@ -62,6 +70,8 @@ d'initialisation côté plugin de jeu :
| [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 |
| [team-config-class-diagram.puml](diagrams/team-config-class-diagram.puml) | Classe | Paramètres d'équipe (cascade + GUI) |
| [gui-class-diagram.puml](diagrams/gui-class-diagram.puml) | Classe | Framework GUI réutilisable |
| [bootstrap-sequence.puml](diagrams/bootstrap-sequence.puml) | Séquence | `CRCore.enable()` côté plugin de jeu |
## Conventions
+63
View File
@@ -367,6 +367,69 @@ 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 — Settings d'équipe : cascade per-team → global → default + GUI
- **Choix** : nouveau module `fr.luc.crcore.team.config` avec :
- `TeamSetting<T>` typé (factories `ofBoolean`, `ofInt`, `ofString`,
`ofEnum`) — chaque setting porte sa clé, son type, son default et sa
sérialisation YAML/SQL.
- `TeamSettings` registry des 8 settings standards
(`FRIENDLY_FIRE`, `PVP_PROTECTION_SECONDS`, `MAX_SIZE`, `MIN_SIZE`,
`RESPAWN_AT_TEAM_SPAWN`, `TEAM_CHAT_ENABLED`, `SHOW_TAG_ABOVE_HEAD`,
`TEAM_COLOR_IN_NAME`), extensible via `TeamSettings.register(...)`
pour les game plugins.
- `TeamConfigService` (interface) + `YamlTeamConfigService` (impl).
- **Cascade de résolution** : per-team → global → hard default. Garantie
non-null grâce au default. La couche per-team est stockée dans
{@code Team.getSettings()} (Map<String, Object>) persistée en SQLite ;
la couche globale dans `<plugin>-team-config.yml` ; les defaults sont
des constantes Java.
- **Stockage per-team SQLite** : nouvelle table `crcore_team_settings`
(team_id, key, value, type). Le type tag (bool/int/str) permet de
reconstruire le type Java au load sans réflexion.
- **Settings custom (game plugin)** : le game plugin peut faire
`TeamSettings.register(MON_SETTING)` dans son onEnable() pour
l'enregistrer ; il apparaîtra automatiquement dans les GUI globaux et
per-team, et sera persisté comme les standards.
- **Pas d'application automatique** : CR-Core ne fait que stocker /
exposer les settings. C'est au game plugin d'écouter les events Bukkit
pertinents (ex. `EntityDamageByEntityEvent`) et de consulter
`config.get(team, FRIENDLY_FIRE)` pour appliquer la règle. CR-Core ne
veut pas hardcoder des semantics gameplay.
## 2026-06-10 — Framework GUI réutilisable (`fr.luc.crcore.gui`)
- **Choix** : module GUI générique avec
`AbstractInventoryGui implements InventoryHolder` (base abstraite),
`GuiClickHandler` (FunctionalInterface), `GuiListener` (un seul
Listener Bukkit pour TOUS les GUI CR-Core), `GuiItems` (builder fluide
d'`ItemStack` avec codes couleur).
- **Détection par holder** : `event.getInventory().getHolder() instanceof
AbstractInventoryGui` — propre, sans titre/UUID custom, marche même
après un translate.
- **Click toujours annulé** : le `GuiListener` cancel TOUT clic dans un
GUI CR-Core (avant invocation du handler) — l'utilisateur ne peut
jamais déplacer un item du GUI, même sur un slot sans handler.
- **Réutilisable** : c'est un framework, pas un GUI métier. Tout futur
GUI (settings, kits, classements interactifs, etc.) hérite
d'`AbstractInventoryGui`.
## 2026-06-10 — `/core team settings` (global = sans arg, per-team = avec arg)
- **Choix** : commande unique `/core team settings [team]` qui multiplexe :
- Sans arg → ouvre `GlobalSettingsGui` (perm
`crcore.team.settings.global`).
- Avec arg `team` → ouvre `TeamSettingsGui` (perm `crcore.team.settings`).
- **Pas `/core settings`** au top-level — l'objectif est de séparer
plus tard en modules (team, score, kits, …). Tout ce qui touche les
teams reste sous `/core team`.
- **Player-only** : Bukkit a besoin d'un `HumanEntity` pour ouvrir un
inventaire. Pas de fallback console.
- **Mécaniques** : booléens → toggle, entiers → clic gauche +1/right -1
(shift = ×10), strings/enums → édition différée au YAML (V1).
- **GUI per-team** : un bouton "Reset tous les overrides" qui efface
tous les per-team de l'équipe pour les faire retomber sur le global.
## 2026-06-10 — Système de broadcasts configurables + `/core reload`
- **Choix** : nouveau module `fr.luc.crcore.broadcast` avec
+82
View File
@@ -0,0 +1,82 @@
@startuml gui-class-diagram
title CR-Core — GUI framework (class diagram, réutilisable)
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.gui" {
abstract class AbstractInventoryGui {
- inventory: Inventory
- handlers: Map<Integer, GuiClickHandler>
--
# setInventory(Inventory): void
+ getInventory(): Inventory
+ {abstract} rebuild(): void
+ onClose(HumanEntity): void
+ openTo(HumanEntity): void
# setButton(slot, item, handler): void
# setDecoration(slot, item): void
# clearSlot(slot): void
+ handleClick(event): void ' appelé par GuiListener
+ handleClose(event): void
}
AbstractInventoryGui ..|> "org.bukkit.inventory.InventoryHolder"
interface GuiClickHandler <<FunctionalInterface>> {
+ onClick(InventoryClickEvent): void
}
class GuiListener {
+ registerOn(JavaPlugin): void
--
@ onClick(InventoryClickEvent)
@ onClose(InventoryCloseEvent)
}
GuiListener ..|> "org.bukkit.event.Listener"
class GuiItems <<utility>> {
+ {static} named(material, name): Builder
+ {static} of(material): Builder
+ {static} filler(): ItemStack
+ {static} item(builder): ItemStack
}
class "GuiItems.Builder" as Builder {
- stack: ItemStack
- meta: ItemMeta
+ name(text): Builder
+ lore(lines...): Builder
+ lore(List<String>): Builder
+ amount(int): Builder
+ build(): ItemStack
+ asItem(): ItemStack
}
GuiItems +-- Builder
GuiListener ..> AbstractInventoryGui : dispatches via getHolder()
AbstractInventoryGui --> GuiClickHandler : per-slot
}
note right of GuiListener
Détection par holder :
if (e.getInventory().getHolder()
instanceof AbstractInventoryGui gui) {
e.setCancelled(true); // ← TOUJOURS, même slot vide
gui.handleClick(e);
}
Enregistré une fois dans CRCore.enable().
end note
note right of AbstractInventoryGui
Pattern :
1. extends AbstractInventoryGui
2. constructeur :
Inventory inv = Bukkit.createInventory(this, 27, "&eTitre");
setInventory(inv);
3. override rebuild() pour peindre
4. setButton(slot, GuiItems.named(...).build(), handler)
end note
@enduml
@@ -0,0 +1,119 @@
@startuml team-config-class-diagram
title CR-Core — Team config (class diagram, cascade per-team → global → default)
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.team.config" {
class "TeamSetting<T>" as TeamSetting <<final>> {
- key: String
- type: Class<T>
- defaultValue: T
- kind: Kind
- parser: Function<Object,T>
- serializer: Function<T,Object>
--
+ {static} ofBoolean(key, default): TeamSetting<Boolean>
+ {static} ofInt(key, default): TeamSetting<Integer>
+ {static} ofString(key, default): TeamSetting<String>
+ {static} ofEnum(key, default): TeamSetting<E>
--
+ parse(raw): T
+ serialize(value): Object
+ getKey() / getType() / getDefaultValue() / getKind()
}
enum "TeamSetting.Kind" as Kind {
BOOLEAN
INTEGER
STRING
ENUM
}
TeamSetting +-- Kind
class TeamSettings <<utility>> {
+ {static} FRIENDLY_FIRE: TeamSetting<Boolean>
+ {static} PVP_PROTECTION_SECONDS: TeamSetting<Integer>
+ {static} MAX_SIZE: TeamSetting<Integer>
+ {static} MIN_SIZE: TeamSetting<Integer>
+ {static} RESPAWN_AT_TEAM_SPAWN: TeamSetting<Boolean>
+ {static} TEAM_CHAT_ENABLED: TeamSetting<Boolean>
+ {static} SHOW_TAG_ABOVE_HEAD: TeamSetting<Boolean>
+ {static} TEAM_COLOR_IN_NAME: TeamSetting<Boolean>
--
+ {static} register(setting): void
+ {static} get(key): Optional<TeamSetting<?>>
+ {static} all(): Collection<TeamSetting<?>>
}
TeamSettings ..> TeamSetting
interface TeamConfigService {
+ get(team, setting): T
+ getGlobal(setting): T
+ setPerTeam(team, setting, value): void
+ resetPerTeam(team, setting): void
+ setGlobal(setting, value): void
+ reload(): void
+ hasPerTeamOverride(team, setting): boolean
+ getGlobalSnapshot(): Map<TeamSetting<?>, Object>
+ getGlobalFileName(): Optional<String>
}
package "fr.luc.crcore.team.config.impl" {
class YamlTeamConfigService {
- plugin: JavaPlugin
- teamRepository: TeamRepository
- userFile: File
- globalValues: Map<String, Object>
--
- ensureUserFile(): void
- rebuildGlobalValues(): void
- persistGlobals(): void
}
YamlTeamConfigService ..|> TeamConfigService
}
TeamConfigService ..> TeamSetting : reads/writes
}
package "fr.luc.crcore.team" {
class Team {
- settings: Map<String, Object>
+ getSettings(): Map<String, Object>
}
}
package "fr.luc.crcore.team.config.gui" {
abstract class AbstractSettingsGui {
- rebuild() : peint la grille
# {abstract} getCurrentValue(setting): T
# {abstract} onChange(setting, newValue): void
# isOverride(setting): boolean
# renderFooter(): void
}
class GlobalSettingsGui
class TeamSettingsGui
GlobalSettingsGui --|> AbstractSettingsGui
TeamSettingsGui --|> AbstractSettingsGui
AbstractSettingsGui --|> "fr.luc.crcore.gui.AbstractInventoryGui"
GlobalSettingsGui --> TeamConfigService
TeamSettingsGui --> TeamConfigService
TeamSettingsGui --> Team
}
YamlTeamConfigService --> "fr.luc.crcore.team.TeamRepository" : persists per-team via save()
TeamConfigService ..> Team : reads/writes settings map
note bottom of YamlTeamConfigService
Cascade de résolution :
1. team.getSettings().get(key) ← override per-team (SQLite)
2. globalValues.get(key) ← <plugin>-team-config.yml
3. setting.getDefaultValue() ← constante Java
Le YAML global est ré-écrit à chaque setGlobal() (persistant à crash).
Les per-team sont écrits via teamRepository.save(team).
end note
@enduml
+114 -1
View File
@@ -807,7 +807,120 @@ depuis les fichiers user du dataFolder. Les defaults en jar ne bougent pas
---
## 10. Bootstrap `CRCore`
## 10. Paramètres d'équipe (`fr.luc.crcore.team.config`)
**Statut** : implémenté. 8 settings standards + GUI in-game + cascade
per-team → global → default.
### Modèle de résolution
```
1. hard default défini en code dans TeamSettings (constantes)
2. global config <plugin>-team-config.yml ← admin via GUI ou YAML
3. per-team override table SQLite crcore_team_settings ← admin via GUI
```
`config.get(team, setting)` cascade per-team → global → default.
`config.getGlobal(setting)` cascade global → default (skip per-team).
Toutes les valeurs retournées sont **non-null** grâce au default en bout
de chaîne.
### Settings standards (`TeamSettings`)
| Constante | Clé YAML/SQL | Type | Défaut |
|---|---|---|---|
| `FRIENDLY_FIRE` | `friendly_fire` | bool | `false` |
| `PVP_PROTECTION_SECONDS` | `pvp_protection_seconds` | int | `0` |
| `MAX_SIZE` | `max_size` | int | `0` (illimité) |
| `MIN_SIZE` | `min_size` | int | `0` |
| `RESPAWN_AT_TEAM_SPAWN` | `respawn_at_team_spawn` | bool | `true` |
| `TEAM_CHAT_ENABLED` | `team_chat_enabled` | bool | `true` |
| `SHOW_TAG_ABOVE_HEAD` | `show_tag_above_head` | bool | `true` |
| `TEAM_COLOR_IN_NAME` | `team_color_in_name` | bool | `true` |
CR-Core fournit les défauts ; **c'est au plugin de jeu d'appliquer ces
settings** dans sa logique (ex. écouter `EntityDamageByEntityEvent` et
checker `config.get(team, FRIENDLY_FIRE)` pour décider si le coup passe).
### API typée
```java
boolean ff = core.teamConfig().get(team, TeamSettings.FRIENDLY_FIRE);
int max = core.teamConfig().getGlobal(TeamSettings.MAX_SIZE);
core.teamConfig().setPerTeam(team, TeamSettings.FRIENDLY_FIRE, true);
core.teamConfig().resetPerTeam(team, TeamSettings.FRIENDLY_FIRE);
core.teamConfig().setGlobal(TeamSettings.MAX_SIZE, 8); // persiste le YAML
core.teamConfig().reload();
```
### Settings custom (game plugin)
Un game plugin peut enregistrer ses propres settings :
```java
public static final TeamSetting<Boolean> CITES_PVP_ROUND_END =
TeamSetting.ofBoolean("cites_pvp_round_end", false);
@Override public void onEnable() {
core = new CRCore(this).enable();
TeamSettings.register(CITES_PVP_ROUND_END);
}
```
→ La clé apparaîtra automatiquement dans les GUI globaux et per-team, et
sera persistée en SQLite + YAML comme les standards.
### Commande GUI
`/core team settings [team]` — player-only, ouvre l'interface graphique.
- Sans argument → **GUI globaux** (perm `crcore.team.settings.global`).
Modif → écrit dans `<plugin>-team-config.yml`.
- Avec argument → **GUI per-team** (perm `crcore.team.settings`). Modif →
écrit en SQLite (overrides). Bouton "Reset tous les overrides" pour
remettre toutes les valeurs au global.
### Mécaniques GUI
- **Inventaire 27 slots**, settings sur la ligne du milieu (slots 10..16).
- **Booléens** : lampe verte (ON) / grise (OFF), clic = toggle.
- **Entiers** : item livre.
- Clic gauche = +1, shift = +10
- Clic droit = -1, shift = -10
- Clamp à 0 minimum
- **Strings/Enums** : affichage seul (édition via YAML — pas dans la V1
du GUI).
- **Per-team** : indication visuelle "Override per-team actif" dans la
lore quand une valeur est différente du global.
- **Slot 22** : bouton Fermer.
- **Slot 18** (per-team uniquement) : bouton "Reset tous les overrides".
### Framework GUI réutilisable
Le module `fr.luc.crcore.gui` est **générique** — réutilisable pour tout
futur GUI CR-Core ou game plugin :
- `AbstractInventoryGui implements InventoryHolder` — base abstraite,
`rebuild()`, `setButton(slot, item, handler)`, `setDecoration(...)`,
`clearSlot(...)`, hook `onClose(...)`.
- `GuiClickHandler` (FunctionalInterface) — handler de clic par slot.
- `GuiListener` — un seul Listener Bukkit qui route les clics et les
fermetures vers le bon GUI via `inventory.getHolder()`.
- `GuiItems` — builder fluide `named(material, "&aTitre").lore(...).build()`,
filler décoratif gris.
Pour faire un GUI custom : `extends AbstractInventoryGui`, créer
l'inventaire dans le constructeur, override `rebuild()`. Le `GuiListener`
est déjà enregistré par `CRCore.enable()`.
### Diagrammes
- [team-config-class-diagram.puml](diagrams/team-config-class-diagram.puml)
- [gui-class-diagram.puml](diagrams/gui-class-diagram.puml)
---
## 11. Bootstrap `CRCore`
**Statut** : implémenté. Point d'entrée unique pour les plugins de jeu.
+3 -1
View File
@@ -267,12 +267,13 @@ CitesPlugin/ # dossier IntelliJ (renommer plus t
## Fichiers de config générés au premier `enable()`
Au premier démarrage, CR-Core crée DEUX fichiers dans le dataFolder :
Au premier démarrage, CR-Core crée TROIS fichiers dans le dataFolder :
| Fichier | Rôle |
|---|---|
| `<plugin-name-lowercase>-messages.yml` | Templates de tous les messages (commandes + broadcasts) |
| `<plugin-name-lowercase>-broadcasts.yml` | Routes : qui reçoit quel event |
| `<plugin-name-lowercase>-team-config.yml` | Paramètres globaux d'équipe (defaults appliqués à toutes les teams) |
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 à
@@ -320,6 +321,7 @@ Au premier `enable()`, les tables suivantes sont créées (en `IF NOT EXISTS`) :
visibility, spawn_world/x/y/z/yaw/pitch)
- `crcore_team_members` — un membre = (team_id, player_id, role, joined_at)
- `crcore_team_scores` — (team_id, score_name, value)
- `crcore_team_settings` — (team_id, key, value, type) — overrides per-team
- `crcore_player_profiles` — un profil = (id)
- `crcore_player_scores` — (profile_id, score_name, value)