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 templates. Un listener interne wire les 12 events natifs ; les game
plugins peuvent broadcast leurs propres events. `/core reload` recharge plugins peuvent broadcast leurs propres events. `/core reload` recharge
les deux fichiers à chaud. 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()` - **Bootstrap unique** — `new CRCore(this).enable()` dans le `onEnable()`
du plugin de jeu, et tout est branché. 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 | | [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 | | [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 | | [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 | | [bootstrap-sequence.puml](diagrams/bootstrap-sequence.puml) | Séquence | `CRCore.enable()` côté plugin de jeu |
## Conventions ## 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 bases existantes, ALTER TABLE manuel ou suppression du fichier
(les bases d'event sont jetables). (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` ## 2026-06-10 — Système de broadcasts configurables + `/core reload`
- **Choix** : nouveau module `fr.luc.crcore.broadcast` avec - **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. **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()` ## 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 | | Fichier | Rôle |
|---|---| |---|---|
| `<plugin-name-lowercase>-messages.yml` | Templates de tous les messages (commandes + broadcasts) | | `<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>-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 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 à 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) visibility, spawn_world/x/y/z/yaw/pitch)
- `crcore_team_members` — un membre = (team_id, player_id, role, joined_at) - `crcore_team_members` — un membre = (team_id, player_id, role, joined_at)
- `crcore_team_scores` — (team_id, score_name, value) - `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_profiles` — un profil = (id)
- `crcore_player_scores` — (profile_id, score_name, value) - `crcore_player_scores` — (profile_id, score_name, value)
+18 -4
View File
@@ -5,8 +5,11 @@ import fr.luc.crcore.broadcast.CRCoreBroadcastListener;
import fr.luc.crcore.broadcast.impl.YamlBroadcastService; import fr.luc.crcore.broadcast.impl.YamlBroadcastService;
import fr.luc.crcore.command.builtin.CoreCommand; import fr.luc.crcore.command.builtin.CoreCommand;
import fr.luc.crcore.database.Database; import fr.luc.crcore.database.Database;
import fr.luc.crcore.gui.GuiListener;
import fr.luc.crcore.message.MessagesService; import fr.luc.crcore.message.MessagesService;
import fr.luc.crcore.message.impl.YamlMessagesService; import fr.luc.crcore.message.impl.YamlMessagesService;
import fr.luc.crcore.team.config.TeamConfigService;
import fr.luc.crcore.team.config.impl.YamlTeamConfigService;
import fr.luc.crcore.player.impl.BukkitEventFiringPlayerProfileServiceImpl; import fr.luc.crcore.player.impl.BukkitEventFiringPlayerProfileServiceImpl;
import fr.luc.crcore.player.impl.InMemoryPlayerProfileRepository; import fr.luc.crcore.player.impl.InMemoryPlayerProfileRepository;
import fr.luc.crcore.player.PlayerProfileRepository; import fr.luc.crcore.player.PlayerProfileRepository;
@@ -84,6 +87,7 @@ public class CRCore {
private PlayerProfileService playerProfileService; private PlayerProfileService playerProfileService;
private MessagesService messages; private MessagesService messages;
private BroadcastService broadcasts; private BroadcastService broadcasts;
private TeamConfigService teamConfig;
private CoreCommand coreCommand; private CoreCommand coreCommand;
private boolean enabled = false; private boolean enabled = false;
@@ -123,11 +127,13 @@ public class CRCore {
this.messages = buildMessagesService(); this.messages = buildMessagesService();
this.broadcasts = buildBroadcastService(messages); this.broadcasts = buildBroadcastService(messages);
this.teamConfig = buildTeamConfigService(teamRepository);
// Listener Bukkit qui route les events CR-Core vers le BroadcastService. // Listeners Bukkit : broadcasts (events CR-Core) + GUI (inventory clicks/close).
new CRCoreBroadcastListener(broadcasts).registerOn(plugin); new CRCoreBroadcastListener(broadcasts).registerOn(plugin);
new GuiListener().registerOn(plugin);
this.coreCommand = buildCoreCommand(teamService, playerProfileService, messages, broadcasts); this.coreCommand = buildCoreCommand(teamService, playerProfileService, messages, broadcasts, teamConfig);
registerCommand(); registerCommand();
registerPlaceholderHook(); registerPlaceholderHook();
@@ -197,8 +203,14 @@ public class CRCore {
protected CoreCommand buildCoreCommand(TeamService teamService, protected CoreCommand buildCoreCommand(TeamService teamService,
PlayerProfileService playerProfileService, PlayerProfileService playerProfileService,
MessagesService messages, MessagesService messages,
BroadcastService broadcasts) { BroadcastService broadcasts,
return new CoreCommand(teamService, playerProfileService, messages, broadcasts); TeamConfigService teamConfig) {
return new CoreCommand(teamService, playerProfileService, messages, broadcasts, teamConfig);
}
/** Construit le {@link TeamConfigService}. Override pour utiliser une impl custom. */
protected TeamConfigService buildTeamConfigService(TeamRepository repository) {
return new YamlTeamConfigService(plugin, repository);
} }
/** Construit le {@link MessagesService}. Override pour utiliser une impl custom. */ /** Construit le {@link MessagesService}. Override pour utiliser une impl custom. */
@@ -289,6 +301,8 @@ public class CRCore {
public MessagesService messages() { return messages; } public MessagesService messages() { return messages; }
public BroadcastService getBroadcasts() { return broadcasts; } public BroadcastService getBroadcasts() { return broadcasts; }
public BroadcastService broadcasts() { return broadcasts; } public BroadcastService broadcasts() { return broadcasts; }
public TeamConfigService getTeamConfig() { return teamConfig; }
public TeamConfigService teamConfig() { return teamConfig; }
public CoreCommand getCoreCommand() { return coreCommand; } public CoreCommand getCoreCommand() { return coreCommand; }
public boolean isEnabled() { return enabled; } public boolean isEnabled() { return enabled; }
} }
@@ -7,6 +7,7 @@ import fr.luc.crcore.command.builtin.team.TeamGroupSubCommand;
import fr.luc.crcore.message.MessagesService; import fr.luc.crcore.message.MessagesService;
import fr.luc.crcore.player.PlayerProfileService; import fr.luc.crcore.player.PlayerProfileService;
import fr.luc.crcore.team.TeamService; import fr.luc.crcore.team.TeamService;
import fr.luc.crcore.team.config.TeamConfigService;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import java.util.Objects; import java.util.Objects;
@@ -21,24 +22,27 @@ public class CoreCommand extends BaseCommand {
protected final PlayerProfileService playerProfileService; protected final PlayerProfileService playerProfileService;
protected final MessagesService messages; protected final MessagesService messages;
protected final BroadcastService broadcasts; protected final BroadcastService broadcasts;
protected final TeamConfigService teamConfig;
public CoreCommand(TeamService teamService, public CoreCommand(TeamService teamService,
PlayerProfileService playerProfileService, PlayerProfileService playerProfileService,
MessagesService messages, MessagesService messages,
BroadcastService broadcasts) { BroadcastService broadcasts,
TeamConfigService teamConfig) {
super("core"); super("core");
this.teamService = Objects.requireNonNull(teamService, "teamService"); this.teamService = Objects.requireNonNull(teamService, "teamService");
this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService"); this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService");
this.messages = Objects.requireNonNull(messages, "messages"); this.messages = Objects.requireNonNull(messages, "messages");
this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts"); this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts");
this.teamConfig = Objects.requireNonNull(teamConfig, "teamConfig");
description("Commandes du noyau CR-Core"); description("Commandes du noyau CR-Core");
registerDefaults(); registerDefaults();
} }
/** Enregistre les groupes par défaut + la sous-commande reload. */ /** Enregistre les groupes par défaut + la sous-commande reload. */
protected void registerDefaults() { protected void registerDefaults() {
addSubCommand(new TeamGroupSubCommand(teamService, messages)); addSubCommand(new TeamGroupSubCommand(teamService, messages, teamConfig));
addSubCommand(new CoreReloadSubCommand(messages, broadcasts)); addSubCommand(new CoreReloadSubCommand(messages, broadcasts, teamConfig));
} }
/** /**
@@ -5,28 +5,31 @@ import fr.luc.crcore.command.CommandContext;
import fr.luc.crcore.command.CommandResult; import fr.luc.crcore.command.CommandResult;
import fr.luc.crcore.command.SubCommand; import fr.luc.crcore.command.SubCommand;
import fr.luc.crcore.message.MessagesService; import fr.luc.crcore.message.MessagesService;
import fr.luc.crcore.team.config.TeamConfigService;
import java.util.Objects; import java.util.Objects;
/** /**
* {@code /core reload} — recharge les fichiers * {@code /core reload} — recharge tous les fichiers user du dataFolder :
* {@code <plugin>-messages.yml} et {@code <plugin>-broadcasts.yml} depuis * {@code <plugin>-messages.yml}, {@code <plugin>-broadcasts.yml} et
* le disque, sans restart du serveur. * {@code <plugin>-team-config.yml}, sans restart.
* *
* <p>Permission : {@code crcore.reload}. Les defaults bundlés dans le jar * <p>Permission : {@code crcore.reload}.
* ne sont pas re-chargés (ils ne bougent pas pendant le runtime), seulement
* les fichiers user en dataFolder.
*/ */
public class CoreReloadSubCommand extends SubCommand { public class CoreReloadSubCommand extends SubCommand {
protected final MessagesService messages; protected final MessagesService messages;
protected final BroadcastService broadcasts; protected final BroadcastService broadcasts;
protected final TeamConfigService teamConfig;
public CoreReloadSubCommand(MessagesService messages, BroadcastService broadcasts) { public CoreReloadSubCommand(MessagesService messages,
BroadcastService broadcasts,
TeamConfigService teamConfig) {
super("reload"); super("reload");
this.messages = Objects.requireNonNull(messages, "messages"); this.messages = Objects.requireNonNull(messages, "messages");
this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts"); this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts");
description("Recharger les fichiers messages et broadcasts"); this.teamConfig = Objects.requireNonNull(teamConfig, "teamConfig");
description("Recharger les fichiers messages, broadcasts et team-config");
permission("crcore.reload"); permission("crcore.reload");
} }
@@ -34,6 +37,7 @@ public class CoreReloadSubCommand extends SubCommand {
public CommandResult execute(CommandContext ctx) { public CommandResult execute(CommandContext ctx) {
messages.reload(); messages.reload();
broadcasts.reload(); broadcasts.reload();
teamConfig.reload();
return CommandResult.success(messages.get("common.reload.success")); return CommandResult.success(messages.get("common.reload.success"));
} }
} }
@@ -3,36 +3,38 @@ package fr.luc.crcore.command.builtin.team;
import fr.luc.crcore.command.SubCommand; import fr.luc.crcore.command.SubCommand;
import fr.luc.crcore.message.MessagesService; import fr.luc.crcore.message.MessagesService;
import fr.luc.crcore.team.TeamService; import fr.luc.crcore.team.TeamService;
import fr.luc.crcore.team.config.TeamConfigService;
import java.util.Objects; import java.util.Objects;
/** /**
* Groupe {@code /core team ...} : container de toutes les sous-commandes * Groupe {@code /core team ...} container des sous-commandes par défaut.
* d'équipe par défaut.
* *
* <p>Pour overrider une sous-commande, un plugin de jeu fait : * <p>Override d'une feuille :
* <pre>{@code * <pre>{@code
* core.getCoreCommand().findSubCommand("team") * core.getCoreCommand().findSubCommand("team")
* .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc, msgs))); * .ifPresent(team -> team.replaceSubCommand("create",
* new MyCustomCreate(svc, msgs)));
* }</pre> * }</pre>
*/ */
public class TeamGroupSubCommand extends SubCommand { public class TeamGroupSubCommand extends SubCommand {
protected final TeamService service; protected final TeamService service;
protected final MessagesService messages; protected final MessagesService messages;
protected final TeamConfigService config;
public TeamGroupSubCommand(TeamService service, MessagesService messages) { public TeamGroupSubCommand(TeamService service,
MessagesService messages,
TeamConfigService config) {
super("team"); super("team");
this.service = Objects.requireNonNull(service, "service"); this.service = Objects.requireNonNull(service, "service");
this.messages = Objects.requireNonNull(messages, "messages"); this.messages = Objects.requireNonNull(messages, "messages");
this.config = Objects.requireNonNull(config, "config");
description("Gestion des équipes"); description("Gestion des équipes");
registerDefaults(); registerDefaults();
} }
/** /** Override pour exclure ou ajouter des sous-commandes au jeu standard. */
* Enregistre toutes les sous-commandes par défaut. Override pour exclure
* ou ajouter des sous-commandes au lieu du jeu standard.
*/
protected void registerDefaults() { protected void registerDefaults() {
addSubCommand(new TeamCreateSubCommand(service, messages)); addSubCommand(new TeamCreateSubCommand(service, messages));
addSubCommand(new TeamDeleteSubCommand(service, messages)); addSubCommand(new TeamDeleteSubCommand(service, messages));
@@ -48,5 +50,6 @@ public class TeamGroupSubCommand extends SubCommand {
addSubCommand(new TeamScoreSubCommand(service, messages)); addSubCommand(new TeamScoreSubCommand(service, messages));
addSubCommand(new TeamTopSubCommand(service, messages)); addSubCommand(new TeamTopSubCommand(service, messages));
addSubCommand(new TeamSetSpawnSubCommand(service, messages)); addSubCommand(new TeamSetSpawnSubCommand(service, messages));
addSubCommand(new TeamSettingsSubCommand(config, messages, service));
} }
} }
@@ -0,0 +1,64 @@
package fr.luc.crcore.command.builtin.team;
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 fr.luc.crcore.team.Team;
import fr.luc.crcore.team.config.TeamConfigService;
import fr.luc.crcore.team.config.gui.GlobalSettingsGui;
import fr.luc.crcore.team.config.gui.TeamSettingsGui;
import org.bukkit.entity.Player;
import java.util.Objects;
import java.util.Optional;
/**
* {@code /core team settings [team]}
*
* <p>Sans argument {@code team} → ouvre le GUI <b>globaux</b> (permission
* {@code crcore.team.settings.global}).
*
* <p>Avec argument {@code team} → ouvre le GUI <b>per-team</b> pour cette
* équipe (permission {@code crcore.team.settings}).
*
* <p>Player-only (besoin d'un inventory holder côté Bukkit).
*/
public class TeamSettingsSubCommand extends SubCommand {
protected final TeamConfigService config;
protected final MessagesService messages;
public TeamSettingsSubCommand(TeamConfigService config,
MessagesService messages,
fr.luc.crcore.team.TeamService teamService) {
super("settings");
this.config = Objects.requireNonNull(config, "config");
this.messages = Objects.requireNonNull(messages, "messages");
description("Modifier les paramètres globaux ou per-team (GUI)");
// Pas de permission fixée ici — on la vérifie manuellement dans execute
// selon que l'arg [team] est passé ou non (deux permissions distinctes).
playerOnly();
optionalArgument("team", TeamArgumentTypes.teamByName(teamService));
}
@Override
public CommandResult execute(CommandContext ctx) {
Player player = ctx.requirePlayer();
Optional<Team> teamOpt = ctx.getOptional("team");
if (teamOpt.isEmpty()) {
if (!player.hasPermission("crcore.team.settings.global")) {
return CommandResult.noPermission();
}
new GlobalSettingsGui(config).openTo(player);
return CommandResult.success();
}
if (!player.hasPermission("crcore.team.settings")) {
return CommandResult.noPermission();
}
new TeamSettingsGui(config, teamOpt.get()).openTo(player);
return CommandResult.success();
}
}
@@ -0,0 +1,118 @@
package fr.luc.crcore.gui;
import org.bukkit.Bukkit;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Base abstraite pour tous les GUI CR-Core. Implémente {@link InventoryHolder}
* pour que {@link GuiListener} puisse identifier les clics destinés à un GUI
* CR-Core (via {@code inventory.getHolder() instanceof AbstractInventoryGui}).
*
* <h2>Pattern d'utilisation</h2>
*
* <ol>
* <li>Sous-classer (ex. {@code GlobalSettingsGui}).</li>
* <li>Dans le constructeur, créer l'inventaire avec
* {@link Bukkit#createInventory(InventoryHolder, int, String)} en
* passant {@code this} comme holder, puis stocker la référence via
* {@link #setInventory(Inventory)}.</li>
* <li>Appeler {@link #rebuild()} pour construire le contenu initial.</li>
* <li>Pour chaque slot interactif, appeler
* {@link #setButton(int, ItemStack, GuiClickHandler)} qui dépose
* l'item ET enregistre le handler de clic.</li>
* <li>Override {@link #rebuild()} pour reconstruire l'inventaire (utile
* après une modification de l'état).</li>
* <li>Override {@link #onClose(HumanEntity)} pour persister un état au
* moment de la fermeture si besoin.</li>
* </ol>
*
* <p>Ouvrir le GUI à un joueur : {@code gui.openTo(player)}.
*/
public abstract class AbstractInventoryGui implements InventoryHolder {
private Inventory inventory;
/** Handlers par slot. Maintenu en parallèle des items déposés. */
private final Map<Integer, GuiClickHandler> handlers = new HashMap<>();
/** À appeler une fois dans le constructeur de la sous-classe. */
protected final void setInventory(Inventory inventory) {
this.inventory = Objects.requireNonNull(inventory, "inventory");
}
@Override
public Inventory getInventory() {
return inventory;
}
/** Construit / reconstruit le contenu de l'inventaire. Override obligatoire. */
public abstract void rebuild();
/**
* Hook appelé à la fermeture du GUI. Override pour persister un état
* (ex. écrire le YAML une seule fois après plusieurs modifs). Défaut :
* no-op.
*/
public void onClose(HumanEntity who) {
}
/** Ouvre ce GUI au joueur (raccourci). */
public void openTo(HumanEntity player) {
Objects.requireNonNull(player, "player");
rebuild();
player.openInventory(inventory);
}
// ---- Slot management ----
/** Pose un item + handler de clic sur un slot. */
protected final void setButton(int slot, ItemStack item, GuiClickHandler handler) {
inventory.setItem(slot, item);
if (handler != null) {
handlers.put(slot, handler);
} else {
handlers.remove(slot);
}
}
/** Pose un item décoratif (pas de handler de clic — un clic ne fait rien). */
protected final void setDecoration(int slot, ItemStack item) {
inventory.setItem(slot, item);
handlers.remove(slot);
}
/** Vide le slot et son handler. */
protected final void clearSlot(int slot) {
inventory.setItem(slot, null);
handlers.remove(slot);
}
// ---- Click dispatch (appelé par GuiListener) ----
/**
* Appelé par {@link GuiListener} sur un click dans l'inventaire de ce
* GUI. Route vers le handler du slot s'il y en a un. <b>Toujours</b>
* cancellable côté listener (l'utilisateur ne peut pas déplacer les
* items du GUI).
*/
public final void handleClick(InventoryClickEvent event) {
int slot = event.getRawSlot();
GuiClickHandler handler = handlers.get(slot);
if (handler != null) {
handler.onClick(event);
}
}
/** Hook interne pour {@link GuiListener} sur fermeture. */
public final void handleClose(InventoryCloseEvent event) {
onClose(event.getPlayer());
}
}
@@ -0,0 +1,20 @@
package fr.luc.crcore.gui;
import org.bukkit.event.inventory.InventoryClickEvent;
/**
* Handler de click pour un slot d'un {@link AbstractInventoryGui}. Le clic
* dans l'inventaire d'un GUI CR-Core est <b>toujours annulé</b> par le
* {@link GuiListener} avant invocation du handler — le slot reste inchangé,
* c'est au handler de modifier l'état interne du GUI puis d'appeler
* {@code rebuild()} si besoin.
*
* <p>Le {@link InventoryClickEvent#getClick()} distingue clic gauche / droit /
* shift / etc. — utile pour les boutons d'incrément (gauche = +1, shift+gauche
* = +10, droit = -1).
*/
@FunctionalInterface
public interface GuiClickHandler {
void onClick(InventoryClickEvent event);
}
@@ -0,0 +1,107 @@
package fr.luc.crcore.gui;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Helpers de construction d'{@link ItemStack} pour les GUI. Traduit les
* codes couleur {@code &} → {@code §} comme le {@link ChatColor}.
*
* <pre>{@code
* ItemStack toggle = GuiItems.named(Material.LIME_DYE, "&aFriendly fire : ON")
* .lore("&7Clic gauche : toggle", "&7Valeur : &fON");
* }</pre>
*/
public final class GuiItems {
private GuiItems() {
}
/** Crée un ItemStack avec un nom (codes couleur '&' traduits). */
public static Builder named(Material material, String displayName) {
return new Builder(material).name(displayName);
}
/** Crée un ItemStack sans nom custom. */
public static Builder of(Material material) {
return new Builder(material);
}
/** Item décoratif gris (verre) — utilisé pour le bordering. */
public static ItemStack filler() {
return new Builder(Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
}
/** Builder fluide. */
public static final class Builder {
private final ItemStack stack;
private final ItemMeta meta;
private Builder(Material material) {
this.stack = new ItemStack(material);
this.meta = stack.getItemMeta();
}
public Builder name(String displayName) {
if (meta != null && displayName != null) {
meta.setDisplayName(ChatColor.translateAlternateColorCodes('&', displayName));
}
return this;
}
public Builder lore(String... lines) {
if (meta != null && lines != null && lines.length > 0) {
List<String> processed = new ArrayList<>(lines.length);
for (String line : lines) {
if (line == null) {
processed.add("");
} else {
processed.add(ChatColor.translateAlternateColorCodes('&', line));
}
}
meta.setLore(processed);
}
return this;
}
public Builder lore(List<String> lines) {
if (lines == null) return this;
return lore(lines.toArray(new String[0]));
}
public Builder amount(int amount) {
stack.setAmount(Math.max(1, Math.min(64, amount)));
return this;
}
public ItemStack build() {
if (meta != null) stack.setItemMeta(meta);
return stack;
}
/** Implicit build au cast — pratique avec {@code setButton(slot, builder, handler)}. */
public ItemStack asItem() {
return build();
}
}
/**
* Implicit conversion pour passer un Builder directement à
* {@link AbstractInventoryGui#setButton(int, ItemStack, GuiClickHandler)}.
*/
public static ItemStack item(Builder builder) {
return builder.build();
}
/** Liste de strings vers tableau (utilitaire pour lore dynamique). */
public static String[] lore(String... lines) {
return Arrays.copyOf(lines, lines.length);
}
}
@@ -0,0 +1,55 @@
package fr.luc.crcore.gui;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Objects;
/**
* Listener Bukkit unique qui route les évènements d'inventaire vers le bon
* {@link AbstractInventoryGui} — détecté via {@code inventory.getHolder()
* instanceof AbstractInventoryGui}.
*
* <p>Comportement standard :
* <ul>
* <li>{@link InventoryClickEvent} sur un GUI CR-Core →
* {@code event.setCancelled(true)} systématique (l'utilisateur ne
* peut <b>jamais</b> déplacer les items), puis
* {@link AbstractInventoryGui#handleClick}.</li>
* <li>{@link InventoryCloseEvent} sur un GUI CR-Core →
* {@link AbstractInventoryGui#handleClose} (le GUI peut persister
* son état).</li>
* </ul>
*
* <p>Instancié et enregistré une fois dans {@code CRCore.enable()}.
*/
public class GuiListener implements Listener {
/** Enregistre ce listener sur le PluginManager du serveur. */
public void registerOn(JavaPlugin plugin) {
Objects.requireNonNull(plugin, "plugin").getServer()
.getPluginManager().registerEvents(this, plugin);
}
@EventHandler
public void onClick(InventoryClickEvent event) {
InventoryHolder holder = event.getInventory().getHolder();
if (!(holder instanceof AbstractInventoryGui)) return;
// Annule TOUT clic dans un GUI CR-Core (pas de déplacement d'items
// possible, même sur les slots décoratifs sans handler).
event.setCancelled(true);
((AbstractInventoryGui) holder).handleClick(event);
}
@EventHandler
public void onClose(InventoryCloseEvent event) {
InventoryHolder holder = event.getInventory().getHolder();
if (holder instanceof AbstractInventoryGui) {
((AbstractInventoryGui) holder).handleClose(event);
}
}
}
@@ -36,6 +36,7 @@ public class Team extends AbstractEntity implements Named, ScoreHolder {
private final TeamColor color; private final TeamColor color;
private final Set<TeamMember> members; private final Set<TeamMember> members;
private final Map<String, Integer> scores; private final Map<String, Integer> scores;
private final Map<String, Object> settings;
private UUID leaderId; private UUID leaderId;
private TeamVisibility visibility; private TeamVisibility visibility;
private Location spawnPoint; private Location spawnPoint;
@@ -70,11 +71,25 @@ public class Team extends AbstractEntity implements Named, ScoreHolder {
this.leaderId = leaderId; // nullable this.leaderId = leaderId; // nullable
this.members = new HashSet<>(); this.members = new HashSet<>();
this.scores = new HashMap<>(); this.scores = new HashMap<>();
this.settings = new HashMap<>();
if (leaderId != null) { if (leaderId != null) {
this.members.add(newMember(leaderId, TeamRole.LEADER)); this.members.add(newMember(leaderId, TeamRole.LEADER));
} }
} }
/**
* Map mutable des overrides per-team de settings. Clé = nom du
* {@link fr.luc.crcore.team.config.TeamSetting}, valeur = forme
* sérialisable (Boolean, Integer, String, …).
*
* <p>Lecture / modification typée recommandée via
* {@link fr.luc.crcore.team.config.TeamConfigService}, qui gère la
* cascade per-team → global → default et persiste les écritures.
*/
public Map<String, Object> getSettings() {
return settings;
}
/** Override to instantiate a custom TeamMember subclass. */ /** Override to instantiate a custom TeamMember subclass. */
protected TeamMember newMember(UUID playerId, TeamRole role) { protected TeamMember newMember(UUID playerId, TeamRole role) {
return new TeamMember(playerId, role); return new TeamMember(playerId, role);
@@ -0,0 +1,69 @@
package fr.luc.crcore.team.config;
import fr.luc.crcore.team.Team;
import java.util.Map;
import java.util.Optional;
/**
* Service de configuration des équipes — résolution en cascade :
*
* <ol>
* <li><b>Per-team</b> — valeur stockée dans {@link Team#getSettings()},
* persistée en SQLite.</li>
* <li><b>Global</b> — valeur dans {@code <plugin>-team-config.yml},
* modifiable via le GUI globaux ou édition directe du YAML.</li>
* <li><b>Hard default</b> — défini en code dans {@link TeamSettings}.</li>
* </ol>
*
* <p>Le service garantit qu'un {@link #get(Team, TeamSetting)} renvoie
* toujours une valeur non-null grâce à la cascade.
*/
public interface TeamConfigService {
/**
* Récupère la valeur effective d'un setting pour une équipe (cascade
* per-team → global → default).
*/
<T> T get(Team team, TeamSetting<T> setting);
/**
* Récupère la valeur globale d'un setting (cascade global → default,
* sans per-team).
*/
<T> T getGlobal(TeamSetting<T> setting);
/**
* Définit un override per-team. Persiste en SQLite via le
* {@code TeamRepository} (l'appelant doit save() la team ensuite — ou
* passer par le service haut niveau qui le fait).
*/
<T> void setPerTeam(Team team, TeamSetting<T> setting, T value);
/**
* Supprime l'override per-team pour une clé donnée — la team retombe
* sur la valeur globale.
*/
void resetPerTeam(Team team, TeamSetting<?> setting);
/**
* Définit une valeur globale et persiste le fichier YAML
* {@code <plugin>-team-config.yml}.
*/
<T> void setGlobal(TeamSetting<T> setting, T value);
/** Recharge le fichier global depuis le disque. */
void reload();
/**
* Indique si une équipe a un override pour cette clé (utile pour
* l'affichage GUI : « hérité du global » vs « override »).
*/
boolean hasPerTeamOverride(Team team, TeamSetting<?> setting);
/** Snapshot des valeurs globales actuelles (déjà parsées). */
Map<TeamSetting<?>, Object> getGlobalSnapshot();
/** Chemin du fichier YAML global (informationnel). */
Optional<String> getGlobalFileName();
}
@@ -0,0 +1,122 @@
package fr.luc.crcore.team.config;
import java.util.Objects;
import java.util.function.Function;
/**
* Définition typée d'un paramètre de team : clé, type, valeur par défaut,
* sérialisation YAML/SQL.
*
* <p>Un {@code TeamSetting<T>} est immutable et créé une fois pour toutes
* dans {@link TeamSettings}. Il sert à la fois côté lecture
* ({@code config.get(team, FRIENDLY_FIRE) → Boolean}) et côté écriture
* ({@code config.set(team, FRIENDLY_FIRE, true)}), avec une garantie de
* type au compile-time.
*
* <p>Quatre types supportés via les factories statiques :
* <ul>
* <li>{@link #ofBoolean(String, boolean)}</li>
* <li>{@link #ofInt(String, int)}</li>
* <li>{@link #ofString(String, String)}</li>
* <li>{@link #ofEnum(String, Enum)} — l'enum est sérialisé par {@code .name()}</li>
* </ul>
*
* <p>Les valeurs lues depuis YAML / SQLite sont des {@code Object} ;
* {@link #parse(Object)} les convertit en {@code T}. Si la conversion
* échoue, le default est utilisé.
*/
public final class TeamSetting<T> {
/** Type primitif simple pour aiguillage côté GUI / sérialisation. */
public enum Kind { BOOLEAN, INTEGER, STRING, ENUM }
private final String key;
private final Class<T> type;
private final T defaultValue;
private final Kind kind;
/** Convertit un Object brut (lu depuis YAML/SQL) vers {@code T}. */
private final Function<Object, T> parser;
/** Convertit {@code T} vers une représentation sérialisable (String/Number/Boolean). */
private final Function<T, Object> serializer;
private TeamSetting(String key, Class<T> type, T defaultValue, Kind kind,
Function<Object, T> parser, Function<T, Object> serializer) {
this.key = Objects.requireNonNull(key, "key");
this.type = Objects.requireNonNull(type, "type");
this.defaultValue = Objects.requireNonNull(defaultValue, "defaultValue");
this.kind = Objects.requireNonNull(kind, "kind");
this.parser = Objects.requireNonNull(parser, "parser");
this.serializer = Objects.requireNonNull(serializer, "serializer");
}
public String getKey() { return key; }
public Class<T> getType() { return type; }
public T getDefaultValue() { return defaultValue; }
public Kind getKind() { return kind; }
/** Convertit une valeur brute (YAML, JDBC) en {@code T}, renvoie le default si échec. */
public T parse(Object raw) {
if (raw == null) return defaultValue;
try {
T parsed = parser.apply(raw);
return parsed != null ? parsed : defaultValue;
} catch (Exception ex) {
return defaultValue;
}
}
/** Convertit une valeur {@code T} vers la forme sérialisable (à stocker en YAML/SQL). */
public Object serialize(T value) {
return value != null ? serializer.apply(value) : null;
}
// ---- Factories ----
public static TeamSetting<Boolean> ofBoolean(String key, boolean defaultValue) {
return new TeamSetting<>(key, Boolean.class, defaultValue, Kind.BOOLEAN,
raw -> {
if (raw instanceof Boolean) return (Boolean) raw;
if (raw instanceof Number) return ((Number) raw).intValue() != 0;
return Boolean.parseBoolean(raw.toString());
},
b -> b);
}
public static TeamSetting<Integer> ofInt(String key, int defaultValue) {
return new TeamSetting<>(key, Integer.class, defaultValue, Kind.INTEGER,
raw -> {
if (raw instanceof Number) return ((Number) raw).intValue();
return Integer.parseInt(raw.toString().trim());
},
i -> i);
}
public static TeamSetting<String> ofString(String key, String defaultValue) {
return new TeamSetting<>(key, String.class, defaultValue, Kind.STRING,
Object::toString,
s -> s);
}
public static <E extends Enum<E>> TeamSetting<E> ofEnum(String key, E defaultValue) {
@SuppressWarnings("unchecked")
Class<E> enumClass = (Class<E>) defaultValue.getDeclaringClass();
return new TeamSetting<>(key, enumClass, defaultValue, Kind.ENUM,
raw -> Enum.valueOf(enumClass, raw.toString().trim().toUpperCase()),
Enum::name);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TeamSetting)) return false;
return key.equals(((TeamSetting<?>) o).key);
}
@Override
public int hashCode() { return key.hashCode(); }
@Override
public String toString() {
return "TeamSetting[" + key + " : " + kind + ", default=" + defaultValue + "]";
}
}
@@ -0,0 +1,97 @@
package fr.luc.crcore.team.config;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
* Registry des {@link TeamSetting} connus par CR-Core.
*
* <p>Définit l'ensemble fermé des settings standards et expose une API
* de lookup par clé. Un game plugin peut ajouter ses propres settings via
* {@link #register(TeamSetting)}.
*
* <h2>Settings standards</h2>
*
* <table>
* <tr><th>Constante</th><th>Clé YAML/SQL</th><th>Type</th><th>Défaut</th></tr>
* <tr><td>{@link #FRIENDLY_FIRE}</td> <td>friendly_fire</td> <td>bool</td> <td>false</td></tr>
* <tr><td>{@link #PVP_PROTECTION_SECONDS}</td> <td>pvp_protection_seconds</td> <td>int</td> <td>0</td></tr>
* <tr><td>{@link #MAX_SIZE}</td> <td>max_size</td> <td>int</td> <td>0 (illimité)</td></tr>
* <tr><td>{@link #MIN_SIZE}</td> <td>min_size</td> <td>int</td> <td>0</td></tr>
* <tr><td>{@link #RESPAWN_AT_TEAM_SPAWN}</td> <td>respawn_at_team_spawn</td> <td>bool</td> <td>true</td></tr>
* <tr><td>{@link #TEAM_CHAT_ENABLED}</td> <td>team_chat_enabled</td> <td>bool</td> <td>true</td></tr>
* <tr><td>{@link #SHOW_TAG_ABOVE_HEAD}</td> <td>show_tag_above_head</td> <td>bool</td> <td>true</td></tr>
* <tr><td>{@link #TEAM_COLOR_IN_NAME}</td> <td>team_color_in_name</td> <td>bool</td> <td>true</td></tr>
* </table>
*/
public final class TeamSettings {
// ---- Settings standards ----
public static final TeamSetting<Boolean> FRIENDLY_FIRE =
TeamSetting.ofBoolean("friendly_fire", false);
public static final TeamSetting<Integer> PVP_PROTECTION_SECONDS =
TeamSetting.ofInt("pvp_protection_seconds", 0);
public static final TeamSetting<Integer> MAX_SIZE =
TeamSetting.ofInt("max_size", 0);
public static final TeamSetting<Integer> MIN_SIZE =
TeamSetting.ofInt("min_size", 0);
public static final TeamSetting<Boolean> RESPAWN_AT_TEAM_SPAWN =
TeamSetting.ofBoolean("respawn_at_team_spawn", true);
public static final TeamSetting<Boolean> TEAM_CHAT_ENABLED =
TeamSetting.ofBoolean("team_chat_enabled", true);
public static final TeamSetting<Boolean> SHOW_TAG_ABOVE_HEAD =
TeamSetting.ofBoolean("show_tag_above_head", true);
public static final TeamSetting<Boolean> TEAM_COLOR_IN_NAME =
TeamSetting.ofBoolean("team_color_in_name", true);
// ---- Registry interne ----
/** LinkedHashMap pour préserver l'ordre d'enregistrement (utile pour l'affichage GUI). */
private static final Map<String, TeamSetting<?>> REGISTRY = new LinkedHashMap<>();
static {
register(FRIENDLY_FIRE);
register(PVP_PROTECTION_SECONDS);
register(MAX_SIZE);
register(MIN_SIZE);
register(RESPAWN_AT_TEAM_SPAWN);
register(TEAM_CHAT_ENABLED);
register(SHOW_TAG_ABOVE_HEAD);
register(TEAM_COLOR_IN_NAME);
}
private TeamSettings() {
}
/**
* Enregistre un setting custom. Un game plugin peut appeler
* {@code TeamSettings.register(MY_SETTING)} dans son onEnable() pour
* que sa clé apparaisse dans les GUI globaux et per-team.
*/
public static synchronized void register(TeamSetting<?> setting) {
Objects.requireNonNull(setting, "setting");
REGISTRY.put(setting.getKey(), setting);
}
/** Récupère un setting par sa clé (non typé — utile pour itération générique). */
public static Optional<TeamSetting<?>> get(String key) {
return Optional.ofNullable(REGISTRY.get(key));
}
/** Tous les settings enregistrés dans l'ordre d'enregistrement. */
public static Collection<TeamSetting<?>> all() {
return Collections.unmodifiableCollection(REGISTRY.values());
}
}
@@ -0,0 +1,168 @@
package fr.luc.crcore.team.config.gui;
import fr.luc.crcore.gui.AbstractInventoryGui;
import fr.luc.crcore.gui.GuiItems;
import fr.luc.crcore.team.config.TeamSetting;
import fr.luc.crcore.team.config.TeamSettings;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
/**
* Base commune des deux GUI de settings (globaux et per-team). Implémente
* le rendu en grille des settings standards :
*
* <ul>
* <li>Booléens : lampe verte / rouge selon état, clic = toggle.</li>
* <li>Entiers : item livre avec lore, clic gauche +1 / shift +10, clic
* droit -1 / shift -10, clamp à 0 mini.</li>
* <li>Strings et Enums : affichage de la valeur (édition pas exposée
* dans cette V1 — à passer par YAML pour ces types).</li>
* </ul>
*
* <p>La sous-classe fournit la valeur courante et le callback de changement.
*/
public abstract class AbstractSettingsGui extends AbstractInventoryGui {
/** Taille fixe de l'inventaire (3 lignes × 9 = 27 slots). Suffisant pour 8 settings. */
protected static final int SIZE = 27;
/** Slots où les settings sont positionnés (10..16 = la ligne du milieu sans les bords). */
protected static final int[] SETTING_SLOTS = {10, 11, 12, 13, 14, 15, 16};
protected AbstractSettingsGui(String title) {
Inventory inv = Bukkit.createInventory(this, SIZE, title);
setInventory(inv);
}
@Override
public void rebuild() {
// Bordures décoratives.
ItemStack filler = GuiItems.filler();
for (int i = 0; i < SIZE; i++) {
getInventory().setItem(i, filler);
}
// Settings dans les slots du milieu.
List<TeamSetting<?>> all = new ArrayList<>(TeamSettings.all());
for (int i = 0; i < all.size() && i < SETTING_SLOTS.length; i++) {
TeamSetting<?> setting = all.get(i);
renderSetting(SETTING_SLOTS[i], setting);
}
// Slot 22 = footer (reset/info/close — défini par les sous-classes via renderFooter()).
renderFooter();
}
/** Hook pour les sous-classes : poser un bouton de footer (close, reset, etc.). */
protected void renderFooter() {
// Par défaut, juste une fermeture.
setButton(22,
GuiItems.named(Material.BARRIER, "&cFermer").build(),
e -> e.getWhoClicked().closeInventory());
}
@SuppressWarnings("unchecked")
private <T> void renderSetting(int slot, TeamSetting<T> setting) {
T currentValue = getCurrentValue(setting);
boolean isOverride = isOverride(setting);
ItemStack icon;
switch (setting.getKind()) {
case BOOLEAN: {
Boolean b = (Boolean) currentValue;
Material mat = Boolean.TRUE.equals(b)
? Material.LIME_DYE
: Material.GRAY_DYE;
String state = Boolean.TRUE.equals(b) ? "&aON" : "&7OFF";
icon = GuiItems.named(mat, "&f" + setting.getKey())
.lore(buildLore("Booléen",
"État : " + state,
"&7Clic gauche : toggle",
isOverride ? "&eOverride per-team actif" : null))
.build();
setButton(slot, icon, e -> {
boolean current = Boolean.TRUE.equals(getCurrentValue(setting));
onChange((TeamSetting<T>) setting, (T) Boolean.valueOf(!current));
rebuild();
});
break;
}
case INTEGER: {
Integer val = (Integer) currentValue;
icon = GuiItems.named(Material.BOOK, "&f" + setting.getKey())
.lore(buildLore("Entier",
"Valeur : &f" + val,
"&7Clic gauche : +1 (shift : +10)",
"&7Clic droit : -1 (shift : -10)",
isOverride ? "&eOverride per-team actif" : null))
.build();
setButton(slot, icon, createIntHandler(setting, val));
break;
}
case STRING:
case ENUM:
default: {
icon = GuiItems.named(Material.PAPER, "&f" + setting.getKey())
.lore(buildLore("Texte / Enum",
"Valeur : &f" + currentValue,
"&7Édition non exposée — modifier via YAML",
isOverride ? "&eOverride per-team actif" : null))
.build();
setDecoration(slot, icon);
break;
}
}
}
private <T> fr.luc.crcore.gui.GuiClickHandler createIntHandler(TeamSetting<T> setting, Integer baseValue) {
return event -> {
Integer current = (Integer) getCurrentValue((TeamSetting<?>) setting);
int amount = computeIntDelta(event);
int next = Math.max(0, current + amount);
@SuppressWarnings("unchecked")
T typed = (T) Integer.valueOf(next);
onChange(setting, typed);
rebuild();
};
}
/** Calcule le delta pour un clic sur un entier (gauche/droit + shift). */
protected int computeIntDelta(InventoryClickEvent event) {
boolean shift = event.isShiftClick();
switch (event.getClick()) {
case LEFT: return shift ? +10 : +1;
case RIGHT: return shift ? -10 : -1;
case SHIFT_LEFT: return +10;
case SHIFT_RIGHT: return -10;
default: return 0;
}
}
/** Construit une lore en filtrant les lignes null. */
protected String[] buildLore(String... lines) {
List<String> out = new ArrayList<>(lines.length);
for (String line : lines) {
if (line != null) out.add(line);
}
return out.toArray(new String[0]);
}
// ---- Hooks pour les sous-classes ----
/** Valeur courante affichée pour ce setting (globale ou per-team selon le GUI). */
protected abstract <T> T getCurrentValue(TeamSetting<T> setting);
/** Appelé quand l'utilisateur modifie un setting via clic. */
protected abstract <T> void onChange(TeamSetting<T> setting, T newValue);
/** Indique si la valeur affichée est un override per-team (juste pour l'UI). */
protected boolean isOverride(TeamSetting<?> setting) {
return false;
}
}
@@ -0,0 +1,35 @@
package fr.luc.crcore.team.config.gui;
import fr.luc.crcore.team.config.TeamConfigService;
import fr.luc.crcore.team.config.TeamSetting;
import java.util.Objects;
/**
* GUI d'édition des settings <b>globaux</b>. Lit / écrit dans le fichier
* {@code <plugin>-team-config.yml} via {@link TeamConfigService}.
*
* <p>Chaque modification est immédiatement persistée sur disque (pas de
* "save" différé) — comme ça si le serveur crash, rien n'est perdu.
*/
public class GlobalSettingsGui extends AbstractSettingsGui {
private final TeamConfigService config;
public GlobalSettingsGui(TeamConfigService config) {
super("§eParamètres globaux d'équipe");
this.config = Objects.requireNonNull(config, "config");
}
@Override
protected <T> T getCurrentValue(TeamSetting<T> setting) {
return config.getGlobal(setting);
}
@Override
protected <T> void onChange(TeamSetting<T> setting, T newValue) {
config.setGlobal(setting, newValue);
}
// isOverride() reste à false : on est globaux, rien n'est "override per-team".
}
@@ -0,0 +1,63 @@
package fr.luc.crcore.team.config.gui;
import fr.luc.crcore.gui.GuiItems;
import fr.luc.crcore.team.Team;
import fr.luc.crcore.team.config.TeamConfigService;
import fr.luc.crcore.team.config.TeamSetting;
import fr.luc.crcore.team.config.TeamSettings;
import org.bukkit.Material;
import java.util.Objects;
/**
* GUI d'édition des settings <b>per-team</b>. Lit la valeur effective
* (cascade per-team → global → default) et écrit dans la map
* {@link Team#getSettings()} via {@link TeamConfigService}.
*
* <p>Footer additionnel : bouton "Reset tous les overrides" qui efface
* toutes les valeurs per-team de l'équipe pour les faire retomber sur le
* global.
*/
public class TeamSettingsGui extends AbstractSettingsGui {
private final TeamConfigService config;
private final Team team;
public TeamSettingsGui(TeamConfigService config, Team team) {
super("§e" + team.getName() + " — paramètres");
this.config = Objects.requireNonNull(config, "config");
this.team = Objects.requireNonNull(team, "team");
}
@Override
protected <T> T getCurrentValue(TeamSetting<T> setting) {
return config.get(team, setting);
}
@Override
protected <T> void onChange(TeamSetting<T> setting, T newValue) {
config.setPerTeam(team, setting, newValue);
}
@Override
protected boolean isOverride(TeamSetting<?> setting) {
return config.hasPerTeamOverride(team, setting);
}
@Override
protected void renderFooter() {
// Bouton "Reset all" en plus du Close.
setButton(18,
GuiItems.named(Material.LAVA_BUCKET, "&cReset tous les overrides")
.lore("&7Efface toutes les valeurs per-team de",
"&7" + team.getName() + " — retour au global.")
.build(),
e -> {
for (TeamSetting<?> s : TeamSettings.all()) {
config.resetPerTeam(team, s);
}
rebuild();
});
super.renderFooter(); // Close
}
}
@@ -0,0 +1,222 @@
package fr.luc.crcore.team.config.impl;
import fr.luc.crcore.team.Team;
import fr.luc.crcore.team.TeamRepository;
import fr.luc.crcore.team.config.TeamConfigService;
import fr.luc.crcore.team.config.TeamSetting;
import fr.luc.crcore.team.config.TeamSettings;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
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.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Impl YAML par défaut : charge le fichier {@code <plugin>-team-config.yml}
* en mémoire, persiste les modifs globales avec
* {@link YamlConfiguration#save(File)}, et délègue le per-team au stockage
* dans {@link Team#getSettings()} (persisté par {@link TeamRepository}).
*
* <p>Pattern identique à {@code YamlMessagesService} et
* {@code YamlBroadcastService} : defaults bundlés dans le jar
* ({@code crcore-team-config.yml}), fichier user créé au premier boot
* (template du plugin de jeu en priorité s'il en bundle un, sinon defaults).
*
* <p>Les valeurs globales sont stockées en {@code Object} dans la map
* {@link #globalValues}, parsées à la lecture via
* {@link TeamSetting#parse(Object)}.
*/
public class YamlTeamConfigService implements TeamConfigService {
private static final String CRCORE_DEFAULTS_RESOURCE = "crcore-team-config.yml";
private final JavaPlugin plugin;
private final TeamRepository teamRepository;
private final Logger logger;
private final String userFileName;
private final File userFile;
/** Snapshot des valeurs globales lues depuis le fichier user (clé → Object brut). */
private final Map<String, Object> globalValues = new HashMap<>();
public YamlTeamConfigService(JavaPlugin plugin, TeamRepository teamRepository) {
this.plugin = Objects.requireNonNull(plugin, "plugin");
this.teamRepository = Objects.requireNonNull(teamRepository, "teamRepository");
this.logger = plugin.getLogger();
this.userFileName = plugin.getName().toLowerCase(Locale.ROOT) + "-team-config.yml";
this.userFile = new File(plugin.getDataFolder(), userFileName);
initialize();
}
private void initialize() {
ensureUserFile();
rebuildGlobalValues();
}
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 team-config créé depuis le template du plugin : " + userFileName);
return;
}
} catch (IOException ex) {
logger.log(Level.WARNING, "Échec copie du template plugin", ex);
}
// Priorité 2 : defaults CR-Core.
try (InputStream coreResource = plugin.getResource(CRCORE_DEFAULTS_RESOURCE)) {
if (coreResource != null) {
copyStreamToFile(coreResource, userFile);
logger.info("Fichier team-config créé depuis les defaults CR-Core : " + userFileName);
}
} catch (IOException ex) {
logger.log(Level.WARNING, "Échec copie des defaults team-config", ex);
}
}
private static void copyStreamToFile(InputStream in, File target) throws IOException {
try (FileOutputStream out = new FileOutputStream(target)) {
in.transferTo(out);
}
}
/** Recharge les valeurs globales depuis le fichier user (ou rien si absent). */
private void rebuildGlobalValues() {
globalValues.clear();
if (!userFile.exists()) return;
try {
YamlConfiguration cfg = YamlConfiguration.loadConfiguration(userFile);
flatten(cfg, "", globalValues);
} catch (Exception ex) {
logger.log(Level.WARNING, "Échec chargement " + userFile, ex);
}
}
// ---- TeamConfigService API ----
@Override
public <T> T get(Team team, TeamSetting<T> setting) {
Objects.requireNonNull(team, "team");
Objects.requireNonNull(setting, "setting");
Object override = team.getSettings().get(setting.getKey());
if (override != null) {
return setting.parse(override);
}
return getGlobal(setting);
}
@Override
public <T> T getGlobal(TeamSetting<T> setting) {
Objects.requireNonNull(setting, "setting");
Object raw = globalValues.get(setting.getKey());
if (raw == null) return setting.getDefaultValue();
return setting.parse(raw);
}
@Override
public <T> void setPerTeam(Team team, TeamSetting<T> setting, T value) {
Objects.requireNonNull(team, "team");
Objects.requireNonNull(setting, "setting");
Object serialized = setting.serialize(value);
if (serialized == null) {
team.getSettings().remove(setting.getKey());
} else {
team.getSettings().put(setting.getKey(), serialized);
}
teamRepository.save(team);
}
@Override
public void resetPerTeam(Team team, TeamSetting<?> setting) {
Objects.requireNonNull(team, "team");
Objects.requireNonNull(setting, "setting");
if (team.getSettings().remove(setting.getKey()) != null) {
teamRepository.save(team);
}
}
@Override
public <T> void setGlobal(TeamSetting<T> setting, T value) {
Objects.requireNonNull(setting, "setting");
Object serialized = setting.serialize(value);
globalValues.put(setting.getKey(), serialized);
persistGlobals();
}
@Override
public void reload() {
rebuildGlobalValues();
}
@Override
public boolean hasPerTeamOverride(Team team, TeamSetting<?> setting) {
return team.getSettings().containsKey(setting.getKey());
}
@Override
public Map<TeamSetting<?>, Object> getGlobalSnapshot() {
Map<TeamSetting<?>, Object> snapshot = new LinkedHashMap<>();
for (TeamSetting<?> s : TeamSettings.all()) {
snapshot.put(s, getGlobalAsObject(s));
}
return snapshot;
}
private Object getGlobalAsObject(TeamSetting<?> setting) {
Object raw = globalValues.get(setting.getKey());
if (raw == null) return setting.getDefaultValue();
return setting.parse(raw);
}
@Override
public Optional<String> getGlobalFileName() {
return Optional.of(userFileName);
}
// ---- Persistance YAML ----
private void persistGlobals() {
try {
YamlConfiguration cfg = new YamlConfiguration();
// Sérialisation flat : clés à plat (les settings n'ont pas de structure imbriquée).
for (Map.Entry<String, Object> e : globalValues.entrySet()) {
cfg.set(e.getKey(), e.getValue());
}
cfg.save(userFile);
} catch (IOException ex) {
logger.log(Level.WARNING, "Échec écriture " + userFile, ex);
}
}
// ---- YAML helpers ----
private static void flatten(ConfigurationSection section, String prefix, Map<String, Object> 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 if (value != null) {
out.put(fullKey, value);
}
}
}
}
@@ -43,6 +43,7 @@ public class SqliteTeamRepository extends InMemoryTeamRepository {
private static final String TABLE_TEAMS = "crcore_teams"; private static final String TABLE_TEAMS = "crcore_teams";
private static final String TABLE_MEMBERS = "crcore_team_members"; private static final String TABLE_MEMBERS = "crcore_team_members";
private static final String TABLE_SCORES = "crcore_team_scores"; private static final String TABLE_SCORES = "crcore_team_scores";
private static final String TABLE_SETTINGS = "crcore_team_settings";
private final Database db; private final Database db;
@@ -82,6 +83,15 @@ public class SqliteTeamRepository extends InMemoryTeamRepository {
.column("value", ColumnType.INTEGER).notNull() .column("value", ColumnType.INTEGER).notNull()
.create(); .create();
// Settings per-team (overrides de la config globale).
// type ∈ {bool, int, str} pour reconstruire le bon type Java au load.
db.table(TABLE_SETTINGS).ifNotExists()
.column("team_id", ColumnType.UUID).notNull()
.column("key", ColumnType.TEXT).notNull()
.column("value", ColumnType.TEXT).notNull()
.column("type", ColumnType.TEXT).notNull()
.create();
// Migration : ancien schéma avait leader_id NOT NULL ; on le rend nullable // Migration : ancien schéma avait leader_id NOT NULL ; on le rend nullable
// pour permettre des équipes leaderless. SQLite ne supporte pas ALTER COLUMN, // pour permettre des équipes leaderless. SQLite ne supporte pas ALTER COLUMN,
// donc on recrée la table en copiant les données. // donc on recrée la table en copiant les données.
@@ -180,6 +190,18 @@ public class SqliteTeamRepository extends InMemoryTeamRepository {
}, },
row.id row.id
); );
// Settings (overrides per-team). Reconstruit le type Java selon la colonne "type".
db.query(
"SELECT key, value, type FROM " + TABLE_SETTINGS + " WHERE team_id = ?",
rs -> {
String key = rs.getString("key");
String raw = rs.getString("value");
String type = rs.getString("type");
team.getSettings().put(key, deserializeSetting(raw, type));
return null;
},
row.id
);
// Spawn point — différé : nécessite un World qui n'est pas forcément chargé // Spawn point — différé : nécessite un World qui n'est pas forcément chargé
// au moment du load. Le serveur charge les worlds avant les plugins normalement, // au moment du load. Le serveur charge les worlds avant les plugins normalement,
// mais on est défensif. // mais on est défensif.
@@ -209,6 +231,7 @@ public class SqliteTeamRepository extends InMemoryTeamRepository {
public boolean delete(UUID id) { public boolean delete(UUID id) {
boolean removed = super.delete(id); boolean removed = super.delete(id);
if (removed) { if (removed) {
db.update("DELETE FROM " + TABLE_SETTINGS + " WHERE team_id = ?", id);
db.update("DELETE FROM " + TABLE_SCORES + " WHERE team_id = ?", id); db.update("DELETE FROM " + TABLE_SCORES + " WHERE team_id = ?", id);
db.update("DELETE FROM " + TABLE_MEMBERS + " WHERE team_id = ?", id); db.update("DELETE FROM " + TABLE_MEMBERS + " WHERE team_id = ?", id);
db.update("DELETE FROM " + TABLE_TEAMS + " WHERE id = ?", id); db.update("DELETE FROM " + TABLE_TEAMS + " WHERE id = ?", id);
@@ -253,6 +276,36 @@ public class SqliteTeamRepository extends InMemoryTeamRepository {
team.getId(), name, value team.getId(), name, value
) )
); );
// Settings : replace en bloc, comme les scores.
db.update("DELETE FROM " + TABLE_SETTINGS + " WHERE team_id = ?", team.getId());
team.getSettings().forEach((key, value) -> {
String type = inferTypeTag(value);
db.update(
"INSERT INTO " + TABLE_SETTINGS + " (team_id, key, value, type) VALUES (?, ?, ?, ?)",
team.getId(), key, String.valueOf(value), type
);
});
}
/** Détecte un tag de type ("bool" / "int" / "str") pour persister la valeur. */
private static String inferTypeTag(Object value) {
if (value instanceof Boolean) return "bool";
if (value instanceof Integer || value instanceof Long || value instanceof Short) return "int";
return "str";
}
/** Reconstruit la valeur Java depuis la forme String + type tag. */
private static Object deserializeSetting(String raw, String type) {
if (raw == null) return null;
switch (type) {
case "bool": return Boolean.parseBoolean(raw);
case "int":
try { return Integer.parseInt(raw); }
catch (NumberFormatException ex) { return 0; }
case "str":
default: return raw;
}
} }
// Tuples internes pour le load. Classes immutables manuelles (Java 11 compat). // Tuples internes pour le load. Classes immutables manuelles (Java 11 compat).
+38
View File
@@ -0,0 +1,38 @@
# =============================================================================
# CR-Core — paramètres globaux d'équipe (defaults)
# -----------------------------------------------------------------------------
# Ce fichier est embarqué dans le jar CR-Core et copié dans :
# <plugin>/<nom-plugin>-team-config.yml
# au premier démarrage. C'est CE fichier (la copie en dataFolder) que l'admin
# édite — soit à la main, soit via le GUI in-game (/core team settings).
#
# Modèle de résolution pour une équipe :
# 1. valeur per-team (stockée en SQLite, override admin via GUI)
# 2. valeur de ce fichier (globale, override admin via YAML ou GUI globaux)
# 3. valeur en dur dans TeamSettings (jamais touchée à runtime)
# =============================================================================
# PvP entre membres d'une même équipe.
friendly_fire: false
# Protection PvP en secondes après création / respawn (0 = pas de protection).
pvp_protection_seconds: 0
# Taille maximale d'une équipe. 0 = illimitée.
max_size: 0
# Taille minimale pour qu'une équipe soit considérée "active". 0 = pas de minimum.
min_size: 0
# Le respawn s'effectue au point de spawn de l'équipe (si défini).
# false → respawn au spawn vanilla du monde.
respawn_at_team_spawn: true
# Active le chat dédié à la team (canal séparé).
team_chat_enabled: true
# Affiche le tag [#TAG] au-dessus de la tête des membres.
show_tag_above_head: true
# Colorie le nom du joueur avec la couleur de la team (chat, tablist).
team_color_in_name: true