From 75d2fa866dc61ee80c48045f8f7074f4d167bb98 Mon Sep 17 00:00:00 2001 From: Antone Barbaud Date: Wed, 10 Jun 2026 11:46:51 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20typed=20team=20settings=20(cascade=20pe?= =?UTF-8?q?r-team=20=E2=86=92=20global=20=E2=86=92=20default)=20+=20in-gam?= =?UTF-8?q?e=20GUI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New fr.luc.crcore.team.config module: - TeamSetting (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 for per-team overrides. - New SQLite table crcore_team_settings (team_id, key, value, type) with load + write-through persist in SqliteTeamRepository. - Global YAML -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 --- docs/README.md | 10 + docs/decisions.md | 63 +++++ docs/diagrams/gui-class-diagram.puml | 82 +++++++ docs/diagrams/team-config-class-diagram.puml | 119 ++++++++++ docs/features.md | 115 ++++++++- docs/setup.md | 4 +- src/main/java/fr/luc/crcore/CRCore.java | 22 +- .../crcore/command/builtin/CoreCommand.java | 10 +- .../command/builtin/CoreReloadSubCommand.java | 20 +- .../builtin/team/TeamGroupSubCommand.java | 21 +- .../builtin/team/TeamSettingsSubCommand.java | 64 +++++ .../luc/crcore/gui/AbstractInventoryGui.java | 118 ++++++++++ .../fr/luc/crcore/gui/GuiClickHandler.java | 20 ++ src/main/java/fr/luc/crcore/gui/GuiItems.java | 107 +++++++++ .../java/fr/luc/crcore/gui/GuiListener.java | 55 +++++ src/main/java/fr/luc/crcore/team/Team.java | 15 ++ .../crcore/team/config/TeamConfigService.java | 69 ++++++ .../luc/crcore/team/config/TeamSetting.java | 122 ++++++++++ .../luc/crcore/team/config/TeamSettings.java | 97 ++++++++ .../team/config/gui/AbstractSettingsGui.java | 168 +++++++++++++ .../team/config/gui/GlobalSettingsGui.java | 35 +++ .../team/config/gui/TeamSettingsGui.java | 63 +++++ .../config/impl/YamlTeamConfigService.java | 222 ++++++++++++++++++ .../team/impl/SqliteTeamRepository.java | 53 +++++ src/main/resources/crcore-team-config.yml | 38 +++ 25 files changed, 1686 insertions(+), 26 deletions(-) create mode 100644 docs/diagrams/gui-class-diagram.puml create mode 100644 docs/diagrams/team-config-class-diagram.puml create mode 100644 src/main/java/fr/luc/crcore/command/builtin/team/TeamSettingsSubCommand.java create mode 100644 src/main/java/fr/luc/crcore/gui/AbstractInventoryGui.java create mode 100644 src/main/java/fr/luc/crcore/gui/GuiClickHandler.java create mode 100644 src/main/java/fr/luc/crcore/gui/GuiItems.java create mode 100644 src/main/java/fr/luc/crcore/gui/GuiListener.java create mode 100644 src/main/java/fr/luc/crcore/team/config/TeamConfigService.java create mode 100644 src/main/java/fr/luc/crcore/team/config/TeamSetting.java create mode 100644 src/main/java/fr/luc/crcore/team/config/TeamSettings.java create mode 100644 src/main/java/fr/luc/crcore/team/config/gui/AbstractSettingsGui.java create mode 100644 src/main/java/fr/luc/crcore/team/config/gui/GlobalSettingsGui.java create mode 100644 src/main/java/fr/luc/crcore/team/config/gui/TeamSettingsGui.java create mode 100644 src/main/java/fr/luc/crcore/team/config/impl/YamlTeamConfigService.java create mode 100644 src/main/resources/crcore-team-config.yml diff --git a/docs/README.md b/docs/README.md index cf3811d..b06c620 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 + `-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 diff --git a/docs/decisions.md b/docs/decisions.md index 825032f..e2b5e46 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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` 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) persistée en SQLite ; + la couche globale dans `-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 diff --git a/docs/diagrams/gui-class-diagram.puml b/docs/diagrams/gui-class-diagram.puml new file mode 100644 index 0000000..becc0dd --- /dev/null +++ b/docs/diagrams/gui-class-diagram.puml @@ -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 + -- + # 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 <> { + + onClick(InventoryClickEvent): void + } + + class GuiListener { + + registerOn(JavaPlugin): void + -- + @ onClick(InventoryClickEvent) + @ onClose(InventoryCloseEvent) + } + GuiListener ..|> "org.bukkit.event.Listener" + + class GuiItems <> { + + {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): 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 diff --git a/docs/diagrams/team-config-class-diagram.puml b/docs/diagrams/team-config-class-diagram.puml new file mode 100644 index 0000000..b5dbc84 --- /dev/null +++ b/docs/diagrams/team-config-class-diagram.puml @@ -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" as TeamSetting <> { + - key: String + - type: Class + - defaultValue: T + - kind: Kind + - parser: Function + - serializer: Function + -- + + {static} ofBoolean(key, default): TeamSetting + + {static} ofInt(key, default): TeamSetting + + {static} ofString(key, default): TeamSetting + + {static} ofEnum(key, default): TeamSetting + -- + + parse(raw): T + + serialize(value): Object + + getKey() / getType() / getDefaultValue() / getKind() + } + + enum "TeamSetting.Kind" as Kind { + BOOLEAN + INTEGER + STRING + ENUM + } + TeamSetting +-- Kind + + class TeamSettings <> { + + {static} FRIENDLY_FIRE: TeamSetting + + {static} PVP_PROTECTION_SECONDS: TeamSetting + + {static} MAX_SIZE: TeamSetting + + {static} MIN_SIZE: TeamSetting + + {static} RESPAWN_AT_TEAM_SPAWN: TeamSetting + + {static} TEAM_CHAT_ENABLED: TeamSetting + + {static} SHOW_TAG_ABOVE_HEAD: TeamSetting + + {static} TEAM_COLOR_IN_NAME: TeamSetting + -- + + {static} register(setting): void + + {static} get(key): Optional> + + {static} all(): Collection> + } + 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, Object> + + getGlobalFileName(): Optional + } + + package "fr.luc.crcore.team.config.impl" { + class YamlTeamConfigService { + - plugin: JavaPlugin + - teamRepository: TeamRepository + - userFile: File + - globalValues: Map + -- + - ensureUserFile(): void + - rebuildGlobalValues(): void + - persistGlobals(): void + } + YamlTeamConfigService ..|> TeamConfigService + } + + TeamConfigService ..> TeamSetting : reads/writes +} + +package "fr.luc.crcore.team" { + class Team { + - settings: Map + + getSettings(): Map + } +} + +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) ← -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 diff --git a/docs/features.md b/docs/features.md index 8c7cdfd..6c13928 100644 --- a/docs/features.md +++ b/docs/features.md @@ -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 -team-config.yml ← admin via GUI ou YAML + 3. per-team override table SQLite crcore_team_settings ← admin via GUI +``` + +`config.get(team, setting)` cascade per-team → global → default. +`config.getGlobal(setting)` cascade global → default (skip per-team). +Toutes les valeurs retournées sont **non-null** grâce au default en bout +de chaîne. + +### Settings standards (`TeamSettings`) + +| Constante | Clé YAML/SQL | Type | Défaut | +|---|---|---|---| +| `FRIENDLY_FIRE` | `friendly_fire` | bool | `false` | +| `PVP_PROTECTION_SECONDS` | `pvp_protection_seconds` | int | `0` | +| `MAX_SIZE` | `max_size` | int | `0` (illimité) | +| `MIN_SIZE` | `min_size` | int | `0` | +| `RESPAWN_AT_TEAM_SPAWN` | `respawn_at_team_spawn` | bool | `true` | +| `TEAM_CHAT_ENABLED` | `team_chat_enabled` | bool | `true` | +| `SHOW_TAG_ABOVE_HEAD` | `show_tag_above_head` | bool | `true` | +| `TEAM_COLOR_IN_NAME` | `team_color_in_name` | bool | `true` | + +CR-Core fournit les défauts ; **c'est au plugin de jeu d'appliquer ces +settings** dans sa logique (ex. écouter `EntityDamageByEntityEvent` et +checker `config.get(team, FRIENDLY_FIRE)` pour décider si le coup passe). + +### API typée + +```java +boolean ff = core.teamConfig().get(team, TeamSettings.FRIENDLY_FIRE); +int max = core.teamConfig().getGlobal(TeamSettings.MAX_SIZE); +core.teamConfig().setPerTeam(team, TeamSettings.FRIENDLY_FIRE, true); +core.teamConfig().resetPerTeam(team, TeamSettings.FRIENDLY_FIRE); +core.teamConfig().setGlobal(TeamSettings.MAX_SIZE, 8); // persiste le YAML +core.teamConfig().reload(); +``` + +### Settings custom (game plugin) + +Un game plugin peut enregistrer ses propres settings : + +```java +public static final TeamSetting CITES_PVP_ROUND_END = + TeamSetting.ofBoolean("cites_pvp_round_end", false); + +@Override public void onEnable() { + core = new CRCore(this).enable(); + TeamSettings.register(CITES_PVP_ROUND_END); +} +``` + +→ La clé apparaîtra automatiquement dans les GUI globaux et per-team, et +sera persistée en SQLite + YAML comme les standards. + +### Commande GUI + +`/core team settings [team]` — player-only, ouvre l'interface graphique. + +- Sans argument → **GUI globaux** (perm `crcore.team.settings.global`). + Modif → écrit dans `-team-config.yml`. +- Avec argument → **GUI per-team** (perm `crcore.team.settings`). Modif → + écrit en SQLite (overrides). Bouton "Reset tous les overrides" pour + remettre toutes les valeurs au global. + +### Mécaniques GUI + +- **Inventaire 27 slots**, settings sur la ligne du milieu (slots 10..16). +- **Booléens** : lampe verte (ON) / grise (OFF), clic = toggle. +- **Entiers** : item livre. + - Clic gauche = +1, shift = +10 + - Clic droit = -1, shift = -10 + - Clamp à 0 minimum +- **Strings/Enums** : affichage seul (édition via YAML — pas dans la V1 + du GUI). +- **Per-team** : indication visuelle "Override per-team actif" dans la + lore quand une valeur est différente du global. +- **Slot 22** : bouton Fermer. +- **Slot 18** (per-team uniquement) : bouton "Reset tous les overrides". + +### Framework GUI réutilisable + +Le module `fr.luc.crcore.gui` est **générique** — réutilisable pour tout +futur GUI CR-Core ou game plugin : + +- `AbstractInventoryGui implements InventoryHolder` — base abstraite, + `rebuild()`, `setButton(slot, item, handler)`, `setDecoration(...)`, + `clearSlot(...)`, hook `onClose(...)`. +- `GuiClickHandler` (FunctionalInterface) — handler de clic par slot. +- `GuiListener` — un seul Listener Bukkit qui route les clics et les + fermetures vers le bon GUI via `inventory.getHolder()`. +- `GuiItems` — builder fluide `named(material, "&aTitre").lore(...).build()`, + filler décoratif gris. + +Pour faire un GUI custom : `extends AbstractInventoryGui`, créer +l'inventaire dans le constructeur, override `rebuild()`. Le `GuiListener` +est déjà enregistré par `CRCore.enable()`. + +### Diagrammes + +- [team-config-class-diagram.puml](diagrams/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. diff --git a/docs/setup.md b/docs/setup.md index aca12fc..602bdd4 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -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 | |---|---| | `-messages.yml` | Templates de tous les messages (commandes + broadcasts) | | `-broadcasts.yml` | Routes : qui reçoit quel event | +| `-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) diff --git a/src/main/java/fr/luc/crcore/CRCore.java b/src/main/java/fr/luc/crcore/CRCore.java index 6afa0a3..d55af67 100644 --- a/src/main/java/fr/luc/crcore/CRCore.java +++ b/src/main/java/fr/luc/crcore/CRCore.java @@ -5,8 +5,11 @@ import fr.luc.crcore.broadcast.CRCoreBroadcastListener; import fr.luc.crcore.broadcast.impl.YamlBroadcastService; import fr.luc.crcore.command.builtin.CoreCommand; import fr.luc.crcore.database.Database; +import fr.luc.crcore.gui.GuiListener; import fr.luc.crcore.message.MessagesService; 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.InMemoryPlayerProfileRepository; import fr.luc.crcore.player.PlayerProfileRepository; @@ -84,6 +87,7 @@ public class CRCore { private PlayerProfileService playerProfileService; private MessagesService messages; private BroadcastService broadcasts; + private TeamConfigService teamConfig; private CoreCommand coreCommand; private boolean enabled = false; @@ -123,11 +127,13 @@ public class CRCore { this.messages = buildMessagesService(); 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 GuiListener().registerOn(plugin); - this.coreCommand = buildCoreCommand(teamService, playerProfileService, messages, broadcasts); + this.coreCommand = buildCoreCommand(teamService, playerProfileService, messages, broadcasts, teamConfig); registerCommand(); registerPlaceholderHook(); @@ -197,8 +203,14 @@ public class CRCore { protected CoreCommand buildCoreCommand(TeamService teamService, PlayerProfileService playerProfileService, MessagesService messages, - BroadcastService broadcasts) { - return new CoreCommand(teamService, playerProfileService, messages, broadcasts); + BroadcastService 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. */ @@ -289,6 +301,8 @@ public class CRCore { public MessagesService messages() { return messages; } public BroadcastService getBroadcasts() { return broadcasts; } public BroadcastService broadcasts() { return broadcasts; } + public TeamConfigService getTeamConfig() { return teamConfig; } + public TeamConfigService teamConfig() { return teamConfig; } public CoreCommand getCoreCommand() { return coreCommand; } public boolean isEnabled() { return enabled; } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java b/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java index f683da2..1b12ccc 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java @@ -7,6 +7,7 @@ import fr.luc.crcore.command.builtin.team.TeamGroupSubCommand; import fr.luc.crcore.message.MessagesService; import fr.luc.crcore.player.PlayerProfileService; import fr.luc.crcore.team.TeamService; +import fr.luc.crcore.team.config.TeamConfigService; import org.bukkit.command.CommandSender; import java.util.Objects; @@ -21,24 +22,27 @@ public class CoreCommand extends BaseCommand { protected final PlayerProfileService playerProfileService; protected final MessagesService messages; protected final BroadcastService broadcasts; + protected final TeamConfigService teamConfig; public CoreCommand(TeamService teamService, PlayerProfileService playerProfileService, MessagesService messages, - BroadcastService broadcasts) { + BroadcastService broadcasts, + TeamConfigService teamConfig) { super("core"); this.teamService = Objects.requireNonNull(teamService, "teamService"); this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService"); this.messages = Objects.requireNonNull(messages, "messages"); this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts"); + this.teamConfig = Objects.requireNonNull(teamConfig, "teamConfig"); description("Commandes du noyau CR-Core"); registerDefaults(); } /** Enregistre les groupes par défaut + la sous-commande reload. */ protected void registerDefaults() { - addSubCommand(new TeamGroupSubCommand(teamService, messages)); - addSubCommand(new CoreReloadSubCommand(messages, broadcasts)); + addSubCommand(new TeamGroupSubCommand(teamService, messages, teamConfig)); + addSubCommand(new CoreReloadSubCommand(messages, broadcasts, teamConfig)); } /** diff --git a/src/main/java/fr/luc/crcore/command/builtin/CoreReloadSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/CoreReloadSubCommand.java index cd63323..870e8a1 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/CoreReloadSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/CoreReloadSubCommand.java @@ -5,28 +5,31 @@ 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.config.TeamConfigService; import java.util.Objects; /** - * {@code /core reload} — recharge les fichiers - * {@code -messages.yml} et {@code -broadcasts.yml} depuis - * le disque, sans restart du serveur. + * {@code /core reload} — recharge tous les fichiers user du dataFolder : + * {@code -messages.yml}, {@code -broadcasts.yml} et + * {@code -team-config.yml}, sans restart. * - *

Permission : {@code crcore.reload}. Les defaults bundlés dans le jar - * ne sont pas re-chargés (ils ne bougent pas pendant le runtime), seulement - * les fichiers user en dataFolder. + *

Permission : {@code crcore.reload}. */ public class CoreReloadSubCommand extends SubCommand { protected final MessagesService messages; protected final BroadcastService broadcasts; + protected final TeamConfigService teamConfig; - public CoreReloadSubCommand(MessagesService messages, BroadcastService broadcasts) { + public CoreReloadSubCommand(MessagesService messages, + BroadcastService broadcasts, + TeamConfigService teamConfig) { super("reload"); this.messages = Objects.requireNonNull(messages, "messages"); 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"); } @@ -34,6 +37,7 @@ public class CoreReloadSubCommand extends SubCommand { public CommandResult execute(CommandContext ctx) { messages.reload(); broadcasts.reload(); + teamConfig.reload(); return CommandResult.success(messages.get("common.reload.success")); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java index 568d05b..2c3dc1c 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java @@ -3,36 +3,38 @@ package fr.luc.crcore.command.builtin.team; import fr.luc.crcore.command.SubCommand; import fr.luc.crcore.message.MessagesService; import fr.luc.crcore.team.TeamService; +import fr.luc.crcore.team.config.TeamConfigService; import java.util.Objects; /** - * Groupe {@code /core team ...} : container de toutes les sous-commandes - * d'équipe par défaut. + * Groupe {@code /core team ...} — container des sous-commandes par défaut. * - *

Pour overrider une sous-commande, un plugin de jeu fait : + *

Override d'une feuille : *

{@code
  * core.getCoreCommand().findSubCommand("team")
- *     .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc, msgs)));
+ *     .ifPresent(team -> team.replaceSubCommand("create",
+ *                       new MyCustomCreate(svc, msgs)));
  * }
*/ public class TeamGroupSubCommand extends SubCommand { protected final TeamService service; protected final MessagesService messages; + protected final TeamConfigService config; - public TeamGroupSubCommand(TeamService service, MessagesService messages) { + public TeamGroupSubCommand(TeamService service, + MessagesService messages, + TeamConfigService config) { super("team"); this.service = Objects.requireNonNull(service, "service"); this.messages = Objects.requireNonNull(messages, "messages"); + this.config = Objects.requireNonNull(config, "config"); description("Gestion des équipes"); registerDefaults(); } - /** - * Enregistre toutes les sous-commandes par défaut. Override pour exclure - * ou ajouter des sous-commandes au lieu du jeu standard. - */ + /** Override pour exclure ou ajouter des sous-commandes au jeu standard. */ protected void registerDefaults() { addSubCommand(new TeamCreateSubCommand(service, messages)); addSubCommand(new TeamDeleteSubCommand(service, messages)); @@ -48,5 +50,6 @@ public class TeamGroupSubCommand extends SubCommand { addSubCommand(new TeamScoreSubCommand(service, messages)); addSubCommand(new TeamTopSubCommand(service, messages)); addSubCommand(new TeamSetSpawnSubCommand(service, messages)); + addSubCommand(new TeamSettingsSubCommand(config, messages, service)); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamSettingsSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSettingsSubCommand.java new file mode 100644 index 0000000..9f3f81c --- /dev/null +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSettingsSubCommand.java @@ -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]} + * + *

Sans argument {@code team} → ouvre le GUI globaux (permission + * {@code crcore.team.settings.global}). + * + *

Avec argument {@code team} → ouvre le GUI per-team pour cette + * équipe (permission {@code crcore.team.settings}). + * + *

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 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(); + } +} diff --git a/src/main/java/fr/luc/crcore/gui/AbstractInventoryGui.java b/src/main/java/fr/luc/crcore/gui/AbstractInventoryGui.java new file mode 100644 index 0000000..cc471f9 --- /dev/null +++ b/src/main/java/fr/luc/crcore/gui/AbstractInventoryGui.java @@ -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}). + * + *

Pattern d'utilisation

+ * + *
    + *
  1. Sous-classer (ex. {@code GlobalSettingsGui}).
  2. + *
  3. 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)}.
  4. + *
  5. Appeler {@link #rebuild()} pour construire le contenu initial.
  6. + *
  7. Pour chaque slot interactif, appeler + * {@link #setButton(int, ItemStack, GuiClickHandler)} qui dépose + * l'item ET enregistre le handler de clic.
  8. + *
  9. Override {@link #rebuild()} pour reconstruire l'inventaire (utile + * après une modification de l'état).
  10. + *
  11. Override {@link #onClose(HumanEntity)} pour persister un état au + * moment de la fermeture si besoin.
  12. + *
+ * + *

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 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. Toujours + * 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()); + } +} diff --git a/src/main/java/fr/luc/crcore/gui/GuiClickHandler.java b/src/main/java/fr/luc/crcore/gui/GuiClickHandler.java new file mode 100644 index 0000000..67937ff --- /dev/null +++ b/src/main/java/fr/luc/crcore/gui/GuiClickHandler.java @@ -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 toujours annulé 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. + * + *

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); +} diff --git a/src/main/java/fr/luc/crcore/gui/GuiItems.java b/src/main/java/fr/luc/crcore/gui/GuiItems.java new file mode 100644 index 0000000..2d6b3a2 --- /dev/null +++ b/src/main/java/fr/luc/crcore/gui/GuiItems.java @@ -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}. + * + *

{@code
+ * ItemStack toggle = GuiItems.named(Material.LIME_DYE, "&aFriendly fire : ON")
+ *     .lore("&7Clic gauche : toggle", "&7Valeur : &fON");
+ * }
+ */ +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 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 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); + } +} diff --git a/src/main/java/fr/luc/crcore/gui/GuiListener.java b/src/main/java/fr/luc/crcore/gui/GuiListener.java new file mode 100644 index 0000000..daa03ca --- /dev/null +++ b/src/main/java/fr/luc/crcore/gui/GuiListener.java @@ -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}. + * + *

Comportement standard : + *

    + *
  • {@link InventoryClickEvent} sur un GUI CR-Core → + * {@code event.setCancelled(true)} systématique (l'utilisateur ne + * peut jamais déplacer les items), puis + * {@link AbstractInventoryGui#handleClick}.
  • + *
  • {@link InventoryCloseEvent} sur un GUI CR-Core → + * {@link AbstractInventoryGui#handleClose} (le GUI peut persister + * son état).
  • + *
+ * + *

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); + } + } +} diff --git a/src/main/java/fr/luc/crcore/team/Team.java b/src/main/java/fr/luc/crcore/team/Team.java index a962fff..963e757 100644 --- a/src/main/java/fr/luc/crcore/team/Team.java +++ b/src/main/java/fr/luc/crcore/team/Team.java @@ -36,6 +36,7 @@ public class Team extends AbstractEntity implements Named, ScoreHolder { private final TeamColor color; private final Set members; private final Map scores; + private final Map settings; private UUID leaderId; private TeamVisibility visibility; private Location spawnPoint; @@ -70,11 +71,25 @@ public class Team extends AbstractEntity implements Named, ScoreHolder { this.leaderId = leaderId; // nullable this.members = new HashSet<>(); this.scores = new HashMap<>(); + this.settings = new HashMap<>(); if (leaderId != null) { 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, …). + * + *

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 getSettings() { + return settings; + } + /** Override to instantiate a custom TeamMember subclass. */ protected TeamMember newMember(UUID playerId, TeamRole role) { return new TeamMember(playerId, role); diff --git a/src/main/java/fr/luc/crcore/team/config/TeamConfigService.java b/src/main/java/fr/luc/crcore/team/config/TeamConfigService.java new file mode 100644 index 0000000..1b583c8 --- /dev/null +++ b/src/main/java/fr/luc/crcore/team/config/TeamConfigService.java @@ -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 : + * + *

    + *
  1. Per-team — valeur stockée dans {@link Team#getSettings()}, + * persistée en SQLite.
  2. + *
  3. Global — valeur dans {@code -team-config.yml}, + * modifiable via le GUI globaux ou édition directe du YAML.
  4. + *
  5. Hard default — défini en code dans {@link TeamSettings}.
  6. + *
+ * + *

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 get(Team team, TeamSetting setting); + + /** + * Récupère la valeur globale d'un setting (cascade global → default, + * sans per-team). + */ + T getGlobal(TeamSetting 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). + */ + void setPerTeam(Team team, TeamSetting 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 -team-config.yml}. + */ + void setGlobal(TeamSetting 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, Object> getGlobalSnapshot(); + + /** Chemin du fichier YAML global (informationnel). */ + Optional getGlobalFileName(); +} diff --git a/src/main/java/fr/luc/crcore/team/config/TeamSetting.java b/src/main/java/fr/luc/crcore/team/config/TeamSetting.java new file mode 100644 index 0000000..7c03ee4 --- /dev/null +++ b/src/main/java/fr/luc/crcore/team/config/TeamSetting.java @@ -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. + * + *

Un {@code TeamSetting} 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. + * + *

Quatre types supportés via les factories statiques : + *

    + *
  • {@link #ofBoolean(String, boolean)}
  • + *
  • {@link #ofInt(String, int)}
  • + *
  • {@link #ofString(String, String)}
  • + *
  • {@link #ofEnum(String, Enum)} — l'enum est sérialisé par {@code .name()}
  • + *
+ * + *

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 { + + /** Type primitif simple pour aiguillage côté GUI / sérialisation. */ + public enum Kind { BOOLEAN, INTEGER, STRING, ENUM } + + private final String key; + private final Class type; + private final T defaultValue; + private final Kind kind; + /** Convertit un Object brut (lu depuis YAML/SQL) vers {@code T}. */ + private final Function parser; + /** Convertit {@code T} vers une représentation sérialisable (String/Number/Boolean). */ + private final Function serializer; + + private TeamSetting(String key, Class type, T defaultValue, Kind kind, + Function parser, Function 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 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 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 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 ofString(String key, String defaultValue) { + return new TeamSetting<>(key, String.class, defaultValue, Kind.STRING, + Object::toString, + s -> s); + } + + public static > TeamSetting ofEnum(String key, E defaultValue) { + @SuppressWarnings("unchecked") + Class enumClass = (Class) 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 + "]"; + } +} diff --git a/src/main/java/fr/luc/crcore/team/config/TeamSettings.java b/src/main/java/fr/luc/crcore/team/config/TeamSettings.java new file mode 100644 index 0000000..ee832e5 --- /dev/null +++ b/src/main/java/fr/luc/crcore/team/config/TeamSettings.java @@ -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. + * + *

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)}. + * + *

Settings standards

+ * + * + * + * + * + * + * + * + * + * + * + *
ConstanteClé YAML/SQLTypeDéfaut
{@link #FRIENDLY_FIRE} friendly_fire bool false
{@link #PVP_PROTECTION_SECONDS} pvp_protection_seconds int 0
{@link #MAX_SIZE} max_size int 0 (illimité)
{@link #MIN_SIZE} min_size int 0
{@link #RESPAWN_AT_TEAM_SPAWN} respawn_at_team_spawn bool true
{@link #TEAM_CHAT_ENABLED} team_chat_enabled bool true
{@link #SHOW_TAG_ABOVE_HEAD} show_tag_above_head bool true
{@link #TEAM_COLOR_IN_NAME} team_color_in_name bool true
+ */ +public final class TeamSettings { + + // ---- Settings standards ---- + + public static final TeamSetting FRIENDLY_FIRE = + TeamSetting.ofBoolean("friendly_fire", false); + + public static final TeamSetting PVP_PROTECTION_SECONDS = + TeamSetting.ofInt("pvp_protection_seconds", 0); + + public static final TeamSetting MAX_SIZE = + TeamSetting.ofInt("max_size", 0); + + public static final TeamSetting MIN_SIZE = + TeamSetting.ofInt("min_size", 0); + + public static final TeamSetting RESPAWN_AT_TEAM_SPAWN = + TeamSetting.ofBoolean("respawn_at_team_spawn", true); + + public static final TeamSetting TEAM_CHAT_ENABLED = + TeamSetting.ofBoolean("team_chat_enabled", true); + + public static final TeamSetting SHOW_TAG_ABOVE_HEAD = + TeamSetting.ofBoolean("show_tag_above_head", true); + + public static final TeamSetting 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> 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> get(String key) { + return Optional.ofNullable(REGISTRY.get(key)); + } + + /** Tous les settings enregistrés dans l'ordre d'enregistrement. */ + public static Collection> all() { + return Collections.unmodifiableCollection(REGISTRY.values()); + } +} diff --git a/src/main/java/fr/luc/crcore/team/config/gui/AbstractSettingsGui.java b/src/main/java/fr/luc/crcore/team/config/gui/AbstractSettingsGui.java new file mode 100644 index 0000000..d11b49e --- /dev/null +++ b/src/main/java/fr/luc/crcore/team/config/gui/AbstractSettingsGui.java @@ -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 : + * + *
    + *
  • Booléens : lampe verte / rouge selon état, clic = toggle.
  • + *
  • Entiers : item livre avec lore, clic gauche +1 / shift +10, clic + * droit -1 / shift -10, clamp à 0 mini.
  • + *
  • Strings et Enums : affichage de la valeur (édition pas exposée + * dans cette V1 — à passer par YAML pour ces types).
  • + *
+ * + *

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> 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 void renderSetting(int slot, TeamSetting 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) 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 fr.luc.crcore.gui.GuiClickHandler createIntHandler(TeamSetting 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 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 getCurrentValue(TeamSetting setting); + + /** Appelé quand l'utilisateur modifie un setting via clic. */ + protected abstract void onChange(TeamSetting 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; + } +} diff --git a/src/main/java/fr/luc/crcore/team/config/gui/GlobalSettingsGui.java b/src/main/java/fr/luc/crcore/team/config/gui/GlobalSettingsGui.java new file mode 100644 index 0000000..68f8caf --- /dev/null +++ b/src/main/java/fr/luc/crcore/team/config/gui/GlobalSettingsGui.java @@ -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 globaux. Lit / écrit dans le fichier + * {@code -team-config.yml} via {@link TeamConfigService}. + * + *

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 getCurrentValue(TeamSetting setting) { + return config.getGlobal(setting); + } + + @Override + protected void onChange(TeamSetting setting, T newValue) { + config.setGlobal(setting, newValue); + } + + // isOverride() reste à false : on est globaux, rien n'est "override per-team". +} diff --git a/src/main/java/fr/luc/crcore/team/config/gui/TeamSettingsGui.java b/src/main/java/fr/luc/crcore/team/config/gui/TeamSettingsGui.java new file mode 100644 index 0000000..9bfbe5b --- /dev/null +++ b/src/main/java/fr/luc/crcore/team/config/gui/TeamSettingsGui.java @@ -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 per-team. Lit la valeur effective + * (cascade per-team → global → default) et écrit dans la map + * {@link Team#getSettings()} via {@link TeamConfigService}. + * + *

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 getCurrentValue(TeamSetting setting) { + return config.get(team, setting); + } + + @Override + protected void onChange(TeamSetting 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 + } +} diff --git a/src/main/java/fr/luc/crcore/team/config/impl/YamlTeamConfigService.java b/src/main/java/fr/luc/crcore/team/config/impl/YamlTeamConfigService.java new file mode 100644 index 0000000..e96934a --- /dev/null +++ b/src/main/java/fr/luc/crcore/team/config/impl/YamlTeamConfigService.java @@ -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 -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}). + * + *

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). + * + *

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 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 get(Team team, TeamSetting 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 getGlobal(TeamSetting setting) { + Objects.requireNonNull(setting, "setting"); + Object raw = globalValues.get(setting.getKey()); + if (raw == null) return setting.getDefaultValue(); + return setting.parse(raw); + } + + @Override + public void setPerTeam(Team team, TeamSetting 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 void setGlobal(TeamSetting 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, Object> getGlobalSnapshot() { + Map, 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 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 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 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); + } + } + } +} diff --git a/src/main/java/fr/luc/crcore/team/impl/SqliteTeamRepository.java b/src/main/java/fr/luc/crcore/team/impl/SqliteTeamRepository.java index 29ecd28..ea22344 100644 --- a/src/main/java/fr/luc/crcore/team/impl/SqliteTeamRepository.java +++ b/src/main/java/fr/luc/crcore/team/impl/SqliteTeamRepository.java @@ -43,6 +43,7 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { private static final String TABLE_TEAMS = "crcore_teams"; private static final String TABLE_MEMBERS = "crcore_team_members"; private static final String TABLE_SCORES = "crcore_team_scores"; + private static final String TABLE_SETTINGS = "crcore_team_settings"; private final Database db; @@ -82,6 +83,15 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { .column("value", ColumnType.INTEGER).notNull() .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 // pour permettre des équipes leaderless. SQLite ne supporte pas ALTER COLUMN, // donc on recrée la table en copiant les données. @@ -180,6 +190,18 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { }, 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é // au moment du load. Le serveur charge les worlds avant les plugins normalement, // mais on est défensif. @@ -209,6 +231,7 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { public boolean delete(UUID id) { boolean removed = super.delete(id); 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_MEMBERS + " WHERE team_id = ?", id); db.update("DELETE FROM " + TABLE_TEAMS + " WHERE id = ?", id); @@ -253,6 +276,36 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { 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). diff --git a/src/main/resources/crcore-team-config.yml b/src/main/resources/crcore-team-config.yml new file mode 100644 index 0000000..a422613 --- /dev/null +++ b/src/main/resources/crcore-team-config.yml @@ -0,0 +1,38 @@ +# ============================================================================= +# CR-Core — paramètres globaux d'équipe (defaults) +# ----------------------------------------------------------------------------- +# Ce fichier est embarqué dans le jar CR-Core et copié dans : +# /-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