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:
@@ -36,6 +36,14 @@ d'initialisation côté plugin de jeu :
|
||||
templates. Un listener interne wire les 12 events natifs ; les game
|
||||
plugins peuvent broadcast leurs propres events. `/core reload` recharge
|
||||
les deux fichiers à chaud.
|
||||
- **Paramètres d'équipe** — `TeamConfigService` typé avec cascade
|
||||
per-team → global → default. 8 settings standards
|
||||
(`FRIENDLY_FIRE`, `MAX_SIZE`, etc.), étendable. Globaux dans
|
||||
`<plugin>-team-config.yml`, per-team en SQLite. GUI in-game via
|
||||
`/core team settings [team]`.
|
||||
- **Framework GUI** — `AbstractInventoryGui` + `GuiListener` réutilisable
|
||||
pour tout GUI custom. Détection par holder, clic toujours annulé,
|
||||
`GuiItems` builder fluide avec codes couleur `&`.
|
||||
- **Bootstrap unique** — `new CRCore(this).enable()` dans le `onEnable()`
|
||||
du plugin de jeu, et tout est branché.
|
||||
|
||||
@@ -62,6 +70,8 @@ d'initialisation côté plugin de jeu :
|
||||
| [database-diagram.puml](diagrams/database-diagram.puml) | Classe | Wrapper SQLite + table builder |
|
||||
| [messages-class-diagram.puml](diagrams/messages-class-diagram.puml) | Classe | Service de messages YAML |
|
||||
| [broadcasts-class-diagram.puml](diagrams/broadcasts-class-diagram.puml) | Classe | Service de broadcasts YAML + listener |
|
||||
| [team-config-class-diagram.puml](diagrams/team-config-class-diagram.puml) | Classe | Paramètres d'équipe (cascade + GUI) |
|
||||
| [gui-class-diagram.puml](diagrams/gui-class-diagram.puml) | Classe | Framework GUI réutilisable |
|
||||
| [bootstrap-sequence.puml](diagrams/bootstrap-sequence.puml) | Séquence | `CRCore.enable()` côté plugin de jeu |
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -367,6 +367,69 @@ Format léger : une décision = un titre + contexte + choix + raison.
|
||||
bases existantes, ALTER TABLE manuel ou suppression du fichier
|
||||
(les bases d'event sont jetables).
|
||||
|
||||
## 2026-06-10 — Settings d'équipe : cascade per-team → global → default + GUI
|
||||
|
||||
- **Choix** : nouveau module `fr.luc.crcore.team.config` avec :
|
||||
- `TeamSetting<T>` typé (factories `ofBoolean`, `ofInt`, `ofString`,
|
||||
`ofEnum`) — chaque setting porte sa clé, son type, son default et sa
|
||||
sérialisation YAML/SQL.
|
||||
- `TeamSettings` registry des 8 settings standards
|
||||
(`FRIENDLY_FIRE`, `PVP_PROTECTION_SECONDS`, `MAX_SIZE`, `MIN_SIZE`,
|
||||
`RESPAWN_AT_TEAM_SPAWN`, `TEAM_CHAT_ENABLED`, `SHOW_TAG_ABOVE_HEAD`,
|
||||
`TEAM_COLOR_IN_NAME`), extensible via `TeamSettings.register(...)`
|
||||
pour les game plugins.
|
||||
- `TeamConfigService` (interface) + `YamlTeamConfigService` (impl).
|
||||
- **Cascade de résolution** : per-team → global → hard default. Garantie
|
||||
non-null grâce au default. La couche per-team est stockée dans
|
||||
{@code Team.getSettings()} (Map<String, Object>) persistée en SQLite ;
|
||||
la couche globale dans `<plugin>-team-config.yml` ; les defaults sont
|
||||
des constantes Java.
|
||||
- **Stockage per-team SQLite** : nouvelle table `crcore_team_settings`
|
||||
(team_id, key, value, type). Le type tag (bool/int/str) permet de
|
||||
reconstruire le type Java au load sans réflexion.
|
||||
- **Settings custom (game plugin)** : le game plugin peut faire
|
||||
`TeamSettings.register(MON_SETTING)` dans son onEnable() pour
|
||||
l'enregistrer ; il apparaîtra automatiquement dans les GUI globaux et
|
||||
per-team, et sera persisté comme les standards.
|
||||
- **Pas d'application automatique** : CR-Core ne fait que stocker /
|
||||
exposer les settings. C'est au game plugin d'écouter les events Bukkit
|
||||
pertinents (ex. `EntityDamageByEntityEvent`) et de consulter
|
||||
`config.get(team, FRIENDLY_FIRE)` pour appliquer la règle. CR-Core ne
|
||||
veut pas hardcoder des semantics gameplay.
|
||||
|
||||
## 2026-06-10 — Framework GUI réutilisable (`fr.luc.crcore.gui`)
|
||||
|
||||
- **Choix** : module GUI générique avec
|
||||
`AbstractInventoryGui implements InventoryHolder` (base abstraite),
|
||||
`GuiClickHandler` (FunctionalInterface), `GuiListener` (un seul
|
||||
Listener Bukkit pour TOUS les GUI CR-Core), `GuiItems` (builder fluide
|
||||
d'`ItemStack` avec codes couleur).
|
||||
- **Détection par holder** : `event.getInventory().getHolder() instanceof
|
||||
AbstractInventoryGui` — propre, sans titre/UUID custom, marche même
|
||||
après un translate.
|
||||
- **Click toujours annulé** : le `GuiListener` cancel TOUT clic dans un
|
||||
GUI CR-Core (avant invocation du handler) — l'utilisateur ne peut
|
||||
jamais déplacer un item du GUI, même sur un slot sans handler.
|
||||
- **Réutilisable** : c'est un framework, pas un GUI métier. Tout futur
|
||||
GUI (settings, kits, classements interactifs, etc.) hérite
|
||||
d'`AbstractInventoryGui`.
|
||||
|
||||
## 2026-06-10 — `/core team settings` (global = sans arg, per-team = avec arg)
|
||||
|
||||
- **Choix** : commande unique `/core team settings [team]` qui multiplexe :
|
||||
- Sans arg → ouvre `GlobalSettingsGui` (perm
|
||||
`crcore.team.settings.global`).
|
||||
- Avec arg `team` → ouvre `TeamSettingsGui` (perm `crcore.team.settings`).
|
||||
- **Pas `/core settings`** au top-level — l'objectif est de séparer
|
||||
plus tard en modules (team, score, kits, …). Tout ce qui touche les
|
||||
teams reste sous `/core team`.
|
||||
- **Player-only** : Bukkit a besoin d'un `HumanEntity` pour ouvrir un
|
||||
inventaire. Pas de fallback console.
|
||||
- **Mécaniques** : booléens → toggle, entiers → clic gauche +1/right -1
|
||||
(shift = ×10), strings/enums → édition différée au YAML (V1).
|
||||
- **GUI per-team** : un bouton "Reset tous les overrides" qui efface
|
||||
tous les per-team de l'équipe pour les faire retomber sur le global.
|
||||
|
||||
## 2026-06-10 — Système de broadcasts configurables + `/core reload`
|
||||
|
||||
- **Choix** : nouveau module `fr.luc.crcore.broadcast` avec
|
||||
|
||||
@@ -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
@@ -807,7 +807,120 @@ depuis les fichiers user du dataFolder. Les defaults en jar ne bougent pas
|
||||
|
||||
---
|
||||
|
||||
## 10. Bootstrap `CRCore`
|
||||
## 10. Paramètres d'équipe (`fr.luc.crcore.team.config`)
|
||||
|
||||
**Statut** : implémenté. 8 settings standards + GUI in-game + cascade
|
||||
per-team → global → default.
|
||||
|
||||
### Modèle de résolution
|
||||
|
||||
```
|
||||
1. hard default défini en code dans TeamSettings (constantes)
|
||||
2. global config <plugin>-team-config.yml ← admin via GUI ou YAML
|
||||
3. per-team override table SQLite crcore_team_settings ← admin via GUI
|
||||
```
|
||||
|
||||
`config.get(team, setting)` cascade per-team → global → default.
|
||||
`config.getGlobal(setting)` cascade global → default (skip per-team).
|
||||
Toutes les valeurs retournées sont **non-null** grâce au default en bout
|
||||
de chaîne.
|
||||
|
||||
### Settings standards (`TeamSettings`)
|
||||
|
||||
| Constante | Clé YAML/SQL | Type | Défaut |
|
||||
|---|---|---|---|
|
||||
| `FRIENDLY_FIRE` | `friendly_fire` | bool | `false` |
|
||||
| `PVP_PROTECTION_SECONDS` | `pvp_protection_seconds` | int | `0` |
|
||||
| `MAX_SIZE` | `max_size` | int | `0` (illimité) |
|
||||
| `MIN_SIZE` | `min_size` | int | `0` |
|
||||
| `RESPAWN_AT_TEAM_SPAWN` | `respawn_at_team_spawn` | bool | `true` |
|
||||
| `TEAM_CHAT_ENABLED` | `team_chat_enabled` | bool | `true` |
|
||||
| `SHOW_TAG_ABOVE_HEAD` | `show_tag_above_head` | bool | `true` |
|
||||
| `TEAM_COLOR_IN_NAME` | `team_color_in_name` | bool | `true` |
|
||||
|
||||
CR-Core fournit les défauts ; **c'est au plugin de jeu d'appliquer ces
|
||||
settings** dans sa logique (ex. écouter `EntityDamageByEntityEvent` et
|
||||
checker `config.get(team, FRIENDLY_FIRE)` pour décider si le coup passe).
|
||||
|
||||
### API typée
|
||||
|
||||
```java
|
||||
boolean ff = core.teamConfig().get(team, TeamSettings.FRIENDLY_FIRE);
|
||||
int max = core.teamConfig().getGlobal(TeamSettings.MAX_SIZE);
|
||||
core.teamConfig().setPerTeam(team, TeamSettings.FRIENDLY_FIRE, true);
|
||||
core.teamConfig().resetPerTeam(team, TeamSettings.FRIENDLY_FIRE);
|
||||
core.teamConfig().setGlobal(TeamSettings.MAX_SIZE, 8); // persiste le YAML
|
||||
core.teamConfig().reload();
|
||||
```
|
||||
|
||||
### Settings custom (game plugin)
|
||||
|
||||
Un game plugin peut enregistrer ses propres settings :
|
||||
|
||||
```java
|
||||
public static final TeamSetting<Boolean> CITES_PVP_ROUND_END =
|
||||
TeamSetting.ofBoolean("cites_pvp_round_end", false);
|
||||
|
||||
@Override public void onEnable() {
|
||||
core = new CRCore(this).enable();
|
||||
TeamSettings.register(CITES_PVP_ROUND_END);
|
||||
}
|
||||
```
|
||||
|
||||
→ La clé apparaîtra automatiquement dans les GUI globaux et per-team, et
|
||||
sera persistée en SQLite + YAML comme les standards.
|
||||
|
||||
### Commande GUI
|
||||
|
||||
`/core team settings [team]` — player-only, ouvre l'interface graphique.
|
||||
|
||||
- Sans argument → **GUI globaux** (perm `crcore.team.settings.global`).
|
||||
Modif → écrit dans `<plugin>-team-config.yml`.
|
||||
- Avec argument → **GUI per-team** (perm `crcore.team.settings`). Modif →
|
||||
écrit en SQLite (overrides). Bouton "Reset tous les overrides" pour
|
||||
remettre toutes les valeurs au global.
|
||||
|
||||
### Mécaniques GUI
|
||||
|
||||
- **Inventaire 27 slots**, settings sur la ligne du milieu (slots 10..16).
|
||||
- **Booléens** : lampe verte (ON) / grise (OFF), clic = toggle.
|
||||
- **Entiers** : item livre.
|
||||
- Clic gauche = +1, shift = +10
|
||||
- Clic droit = -1, shift = -10
|
||||
- Clamp à 0 minimum
|
||||
- **Strings/Enums** : affichage seul (édition via YAML — pas dans la V1
|
||||
du GUI).
|
||||
- **Per-team** : indication visuelle "Override per-team actif" dans la
|
||||
lore quand une valeur est différente du global.
|
||||
- **Slot 22** : bouton Fermer.
|
||||
- **Slot 18** (per-team uniquement) : bouton "Reset tous les overrides".
|
||||
|
||||
### Framework GUI réutilisable
|
||||
|
||||
Le module `fr.luc.crcore.gui` est **générique** — réutilisable pour tout
|
||||
futur GUI CR-Core ou game plugin :
|
||||
|
||||
- `AbstractInventoryGui implements InventoryHolder` — base abstraite,
|
||||
`rebuild()`, `setButton(slot, item, handler)`, `setDecoration(...)`,
|
||||
`clearSlot(...)`, hook `onClose(...)`.
|
||||
- `GuiClickHandler` (FunctionalInterface) — handler de clic par slot.
|
||||
- `GuiListener` — un seul Listener Bukkit qui route les clics et les
|
||||
fermetures vers le bon GUI via `inventory.getHolder()`.
|
||||
- `GuiItems` — builder fluide `named(material, "&aTitre").lore(...).build()`,
|
||||
filler décoratif gris.
|
||||
|
||||
Pour faire un GUI custom : `extends AbstractInventoryGui`, créer
|
||||
l'inventaire dans le constructeur, override `rebuild()`. Le `GuiListener`
|
||||
est déjà enregistré par `CRCore.enable()`.
|
||||
|
||||
### Diagrammes
|
||||
|
||||
- [team-config-class-diagram.puml](diagrams/team-config-class-diagram.puml)
|
||||
- [gui-class-diagram.puml](diagrams/gui-class-diagram.puml)
|
||||
|
||||
---
|
||||
|
||||
## 11. Bootstrap `CRCore`
|
||||
|
||||
**Statut** : implémenté. Point d'entrée unique pour les plugins de jeu.
|
||||
|
||||
|
||||
+3
-1
@@ -267,12 +267,13 @@ CitesPlugin/ # dossier IntelliJ (renommer plus t
|
||||
|
||||
## Fichiers de config générés au premier `enable()`
|
||||
|
||||
Au premier démarrage, CR-Core crée DEUX fichiers dans le dataFolder :
|
||||
Au premier démarrage, CR-Core crée TROIS fichiers dans le dataFolder :
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---|---|
|
||||
| `<plugin-name-lowercase>-messages.yml` | Templates de tous les messages (commandes + broadcasts) |
|
||||
| `<plugin-name-lowercase>-broadcasts.yml` | Routes : qui reçoit quel event |
|
||||
| `<plugin-name-lowercase>-team-config.yml` | Paramètres globaux d'équipe (defaults appliqués à toutes les teams) |
|
||||
|
||||
Les deux suivent le même pattern : si ton plugin de jeu bundle un fichier
|
||||
au même nom dans ses ressources, c'est lui qui sert de template initial à
|
||||
@@ -320,6 +321,7 @@ Au premier `enable()`, les tables suivantes sont créées (en `IF NOT EXISTS`) :
|
||||
visibility, spawn_world/x/y/z/yaw/pitch)
|
||||
- `crcore_team_members` — un membre = (team_id, player_id, role, joined_at)
|
||||
- `crcore_team_scores` — (team_id, score_name, value)
|
||||
- `crcore_team_settings` — (team_id, key, value, type) — overrides per-team
|
||||
- `crcore_player_profiles` — un profil = (id)
|
||||
- `crcore_player_scores` — (profile_id, score_name, value)
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 <plugin>-messages.yml} et {@code <plugin>-broadcasts.yml} depuis
|
||||
* le disque, sans restart du serveur.
|
||||
* {@code /core reload} — recharge tous les fichiers user du dataFolder :
|
||||
* {@code <plugin>-messages.yml}, {@code <plugin>-broadcasts.yml} et
|
||||
* {@code <plugin>-team-config.yml}, sans restart.
|
||||
*
|
||||
* <p>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.
|
||||
* <p>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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Pour overrider une sous-commande, un plugin de jeu fait :
|
||||
* <p>Override d'une feuille :
|
||||
* <pre>{@code
|
||||
* core.getCoreCommand().findSubCommand("team")
|
||||
* .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc, msgs)));
|
||||
* .ifPresent(team -> team.replaceSubCommand("create",
|
||||
* new MyCustomCreate(svc, msgs)));
|
||||
* }</pre>
|
||||
*/
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 Set<TeamMember> members;
|
||||
private final Map<String, Integer> scores;
|
||||
private final Map<String, Object> 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, …).
|
||||
*
|
||||
* <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. */
|
||||
protected TeamMember newMember(UUID playerId, TeamRole 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_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).
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user