4efaa5bbde
New feature fr.luc.crcore.features.moderation, opt-in via CRCoreConfig.setupModeration() (also enabled by setupAll()). Core abstractions: - ModerationState: full player snapshot (inv + armor + offhand, XP, health, food, location, gamemode, allowFlight/flying, walk/fly speed). Immutable, restoreTo(player) restores everything. - ModerationService interface + ModerationServiceImpl (with protected onAfterEnter/onAfterExit hooks) + BukkitEventFiringModerationServiceImpl (fires ModerationEnterEvent / ModerationExitEvent). - ModerationRepository interface + InMemoryModerationRepository (skeleton — SQLite impl with BukkitObjectOutputStream serialization planned). - ModeratorTool interface: getKey/getSlot(0..8)/buildIcon + onLeftClick/onRightClick/onInteractEntity. ModeratorToolRegistry preserves registration order, slot collision = replace. - Exceptions: ModerationException base + AlreadyActive + NotActive. - Events: ModerationEvent base + Enter + Exit. 5 skeleton tools in the hotbar: - slot 0: TeleportRandomPlayerTool (compass, right-click → tp random) - slot 1: InventorySpyTool (chest, right-click on player → open inv) - slot 2: FreezeTool (ice, right-click on player → toggle freeze) - slot 7: VanishToggleTool (ender eye, click → toggle vanish) - slot 8: ExitTool (barrier, click → exit mod mode) Slots 3-6 free for custom tools. ModerationListener routes interactions and locks hotbar: - PlayerInteractEvent → tool.onLeftClick / onRightClick (with cancel). - PlayerInteractEntityEvent → tool.onInteractEntity (with cancel). - PlayerDropItemEvent / PlayerSwapHandItemsEvent: cancel for mods. - InventoryClickEvent: cancel only when top inv is the mod's own inv (preserves InventorySpyTool's ability to manipulate target's inv). - PlayerJoinEvent: re-applies vanish for already-vanished mods. - PlayerQuitEvent: cleanup freeze state. - PlayerMoveEvent: cancel block-position changes for frozen players, keeping head rotation free. Mod mode lifecycle: - enter: snapshot + clear inv + populate hotbar + CREATIVE + allowFlight + vanish + ModerationEnterEvent. - exit: state.restoreTo(player) + unvanish + unfreeze + repo delete + ModerationExitEvent. /core admin (perm crcore.admin, player-only): toggle on/off. Messages moderation.enter.success / moderation.exit.success added to crcore-messages.yml. CRCoreConfig.setupModeration() + isModerationEnabled() flag. CRCore: buildModerationService() and registerDefaultModeratorTools() override points, moderation() / getModerationService() getters with IllegalStateException guard. Builds + registers ModerationListener at enable() when feature on. CoreCommand extended to take ModerationService; registers AdminToggleSubCommand only when service non-null. Skeleton limitations documented in features.md: - In-memory repo only (server crash = lost inv) — SQLite planned. - InventorySpyTool opens real inv (no read-only wrapping yet). - TeleportRandomPlayerTool is a placeholder for a future player-picker GUI. - No moderation.*.broadcast keys yet. - No /core admin <player> (self-toggle only). Docs: section 11 in features.md, decision logged in decisions.md (skeleton scope + rationale), setup.md snippet updated, new moderation-class-diagram.puml, README.md updated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
742 lines
41 KiB
Markdown
742 lines
41 KiB
Markdown
# Journal des décisions
|
||
|
||
Format léger : une décision = un titre + contexte + choix + raison.
|
||
|
||
## 2026-06-08 — Cible Minecraft 1.16.5 / Paper
|
||
|
||
- **Choix** : utiliser l'API Paper 1.16.5 (`paper-api`) plutôt que Spigot.
|
||
- **Raison** : Paper est un sur-ensemble de Spigot, expose plus d'API utiles
|
||
pour les évènements, et reste compatible avec un serveur Spigot 1.16.5.
|
||
- **Conséquence** : un serveur Paper 1.16.5 est recommandé pour le test.
|
||
|
||
## 2026-06-08 — Java 16
|
||
|
||
- **Choix** : `maven.compiler.source/target = 16`.
|
||
- **Raison** : Paper 1.16.5 tourne avec un JDK 8–16. Java 16 donne accès aux
|
||
records, pattern matching simple, etc., tout en restant exécutable sur un
|
||
serveur 1.16.5.
|
||
|
||
## 2026-06-08 — `docs/` = source de vérité
|
||
|
||
- **Choix** : toutes les décisions, règles de gameplay, commandes et idées
|
||
doivent être notées dans `docs/` avant ou pendant l'implémentation.
|
||
- **Raison** : éviter la dérive entre intention et code, garder une trace
|
||
partageable des échanges.
|
||
|
||
## 2026-06-08 — Code en anglais standard
|
||
|
||
- **Choix** : tout le code (classes, méthodes, attributs, variables) est écrit
|
||
en anglais. La doc utilisateur (`docs/`, messages joueurs) reste en français.
|
||
- **Raison** : conventions standards du monde Java/Bukkit, lisibilité par tout
|
||
développeur, cohérence avec l'API Paper.
|
||
|
||
## 2026-06-08 — Architecture en couches pour le domaine
|
||
|
||
- **Choix** : séparer chaque domaine fonctionnel (ex. `team`) en :
|
||
- **Interfaces** d'abstraction (ex. `Identifiable`, `Named`, `Repository<T>`,
|
||
`TeamRepository`, `TeamService`).
|
||
- **Enums** pour les ensembles fermés (`TeamRole`, `TeamColor`).
|
||
- **Classe abstraite** commune `AbstractEntity` (gère `id` + `equals/hashCode`).
|
||
- **Classes concrètes** : entités (`Team`, `TeamMember`), implémentations
|
||
(`InMemoryTeamRepository`, `TeamServiceImpl`).
|
||
- **Exceptions** dédiées avec hiérarchie (`TeamException` ➜
|
||
`TeamAlreadyExistsException`, `TeamNotFoundException`).
|
||
- **Raison** : testabilité (mocker une interface), évolutivité (changer le
|
||
backend de persistance sans toucher au service), lisibilité.
|
||
|
||
## 2026-06-08 — Persistence en mémoire pour démarrer
|
||
|
||
- **Choix** : `InMemoryTeamRepository` (Map<UUID, Team>) comme première
|
||
implémentation.
|
||
- **Raison** : permet d'avancer sur le gameplay sans dépendre d'un schéma de
|
||
stockage. À remplacer par une implémentation YAML/SQLite/Postgres plus tard
|
||
sans toucher au service.
|
||
|
||
## 2026-06-08 — `Team` = entité mutable, `TeamMember` = quasi-immutable
|
||
|
||
- **Choix** : `Team` mute (ajouts/retraits de membres, transfert de leadership)
|
||
; `TeamMember` est immuable, sa transition de rôle passe par `withRole(...)`
|
||
qui renvoie une nouvelle instance.
|
||
- **Raison** : un `TeamMember` est identifié par son `playerId` ; il est plus
|
||
simple de raisonner sur des états successifs en remplaçant l'instance. Le
|
||
`Set<TeamMember>` reste cohérent car `equals/hashCode` ne dépend que du
|
||
`playerId`.
|
||
|
||
## 2026-06-08 — Renommage du projet : CitesPlugin ➜ CR-Core
|
||
|
||
- **Choix** : le projet devient **CR-Core**, un plugin "noyau" réutilisable.
|
||
Les anciens packages `fr.luc.citesplugin.*` sont déplacés sous `fr.luc.crcore.*`.
|
||
L'ancien `CitesPlugin` (le jeu lui-même) deviendra un futur plugin séparé qui
|
||
déclarera `depend: [CR-Core]`.
|
||
- **Raison** : centraliser les briques transverses (équipes, futurs scores,
|
||
profils joueurs, etc.) pour pouvoir les réutiliser sur plusieurs plugins de
|
||
jeu sans dupliquer le code.
|
||
- **Conséquence** : downstream consomme CR-Core soit via la façade statique
|
||
`fr.luc.crcore.CR` (ex. `CR.teams()`), soit via le `ServicesManager` Bukkit
|
||
(`getServicesManager().load(TeamService.class)`).
|
||
|
||
## 2026-06-08 — Distribution : CR-Core devient une librairie Maven pure (révision)
|
||
|
||
- **Révision** des décisions précédentes "plugin Bukkit autonome" et
|
||
"architecture en modules (PluginModule / ModuleRegistry)".
|
||
- **Choix** : CR-Core n'est plus un plugin Bukkit, c'est une **librairie**
|
||
(`jar`) sans `plugin.yml` ni `JavaPlugin`. Chaque plugin de jeu consomme la
|
||
lib en dépendance Maven, instancie lui-même ses services (`new TeamServiceImpl(...)`)
|
||
et garde son propre registre.
|
||
- **Raison** : la complexité du système de modules + façade statique +
|
||
`ServicesManager` n'apporte rien quand on est dans un contexte d'events
|
||
ponctuels où chaque jeu vit dans sa propre session. La lib est nettement plus
|
||
simple à utiliser et à tester. Le partage d'état entre jeux n'est pas un
|
||
besoin réel pour les events entre amis.
|
||
- **Conséquence** : suppression de `CRCorePlugin`, `CR` (façade), `plugin.yml`,
|
||
`PluginModule`, `AbstractModule`, `ModuleRegistry`, `TeamModule`. Suppression
|
||
aussi de `maven-shade-plugin` côté core (c'est le plugin de jeu qui décide
|
||
de shader ou non).
|
||
|
||
## 2026-06-08 — Overridabilité par défaut
|
||
|
||
- **Choix** : toutes les classes du noyau sont conçues pour être étendues.
|
||
Pas de `final` sur les classes ; méthodes-clés en `protected` ; factories
|
||
`newXxx(...)` pour substituer des sous-classes ; hooks `onBeforeXxx` /
|
||
`onAfterXxx` autour des opérations importantes.
|
||
- **Exemples sur `TeamServiceImpl`** : `newTeam`, `validateName`, `validateTag`,
|
||
`validateLeader`, `onBeforeSave`, `onAfterCreate`, `onBeforeDissolve`,
|
||
`onAfterDissolve`, `onMemberAdded`, `onMemberRemoved`,
|
||
`onLeadershipTransferred`. Sur `Team` : `newMember`.
|
||
- **Raison** : le noyau doit fournir un comportement par défaut "qui marche",
|
||
mais chaque jeu doit pouvoir greffer ses propres règles (logging, persistance
|
||
custom, validations supplémentaires, hooks d'events Bukkit) sans réécrire
|
||
toute la classe.
|
||
|
||
## 2026-06-08 — Framework de commandes intégré
|
||
|
||
- **Choix** : CR-Core fournit `Command` (interface), `AbstractCommand`
|
||
(classe abstraite avec tous les builders), `BaseCommand` (top-level
|
||
Bukkit-aware, conteneur de sous-commandes), `SubCommand` (feuille), plus
|
||
`CommandContext`, `CommandResult`, `ArgumentType<T>` et un jeu de types
|
||
built-in (`STRING`, `INTEGER`, `DOUBLE`, `BOOLEAN`, `ONLINE_PLAYER`,
|
||
`enumOf(...)`, `choice(...)`).
|
||
- **Raison** : chaque plugin de jeu aura ses commandes ; mutualiser le routage
|
||
args[0]→sous-commande, les checks permission / player-only, le parsing des
|
||
arguments et la tab-completion évite de redévelopper le même squelette à
|
||
chaque fois.
|
||
- **Non-choix** : pas d'annotations (`@Command`, `@Argument`) — la résolution
|
||
par réflexion ajouterait une dépendance d'outillage et compliquerait le
|
||
debug. L'API builder reste assez concise.
|
||
- **Découplage** : le framework ne contient pas de commande "team" prête à
|
||
l'emploi. C'est au plugin de jeu de définir ses commandes en utilisant les
|
||
briques. Ça permet à chaque jeu d'avoir ses propres permissions, messages,
|
||
et règles métier.
|
||
|
||
## 2026-06-08 — Visibilité publique / privée des équipes
|
||
|
||
- **Choix** : ajout d'un enum `TeamVisibility { PUBLIC, PRIVATE }` porté par
|
||
`Team`. Une équipe `PUBLIC` peut être rejointe par un joueur via
|
||
`TeamService.joinTeam(teamId, playerId)` ; une équipe `PRIVATE` ne peut
|
||
recevoir des membres que via `addMember` appelé par le chef.
|
||
- **Défaut** : `PRIVATE` à la création (le chef garde le contrôle ; il faut
|
||
une action explicite pour ouvrir l'équipe au public). Une surcharge
|
||
`createTeam(..., visibility)` permet de créer directement en `PUBLIC`.
|
||
- **Nouvelle exception** : `TeamAccessException extends TeamException` —
|
||
levée quand un auto-join est refusé (team privée, ou joueur déjà dans une
|
||
équipe, ou refus custom dans `validateJoinable`).
|
||
- **Nouveaux hooks** : `validateJoinable(team, playerId)`,
|
||
`onPlayerJoined(team, member)`, `onVisibilityChanged(team, oldV, newV)`.
|
||
- **Décision écartée pour l'instant** : un mode `INVITE_ONLY` avec un système
|
||
d'invitations pendantes (Player A invite Player B → B accepte). Pas
|
||
indispensable pour démarrer ; le chef peut déjà ajouter directement via
|
||
`addMember`. À reconsidérer si le besoin remonte.
|
||
|
||
## 2026-06-08 — Scores nommés (Map<String, Integer>) plutôt qu'un score unique
|
||
|
||
- **Choix** : chaque équipe porte un `Map<String, Integer>` de scores nommés
|
||
(`"kills"`, `"objectives"`, `"global"`, …) plutôt qu'un seul `int score`.
|
||
- **Raison** : tous les jeux n'ont pas la même métrique. BedWars a "beds_broken"
|
||
+ "final_kills" ; un mode Capture the Flag a "flags" + "kills" ; un mode
|
||
simple peut n'utiliser que `"global"`. Un Map évite d'imposer un schéma fixe
|
||
et reste compact pour les cas mono-score.
|
||
- **Conséquence** : les noms de scores sont libres et non typés au niveau du
|
||
noyau ; chaque jeu choisit ses propres noms. Pour de la sûreté, un jeu peut
|
||
exposer un enum ou des constantes côté plugin.
|
||
- **Type** : `Integer` plutôt que `Long` ou `Double`. Suffisant pour des
|
||
scores de match (limite ~2 milliards). Si un jeu a besoin de Long ou Double,
|
||
il peut wrapper et stocker un encodage custom ; ou bien on étendra l'API
|
||
plus tard.
|
||
|
||
## 2026-06-08 — Classements : ranking par score + ranking global (= somme)
|
||
|
||
- **Choix** : `TeamService` expose `getRankingByScore(scoreName)` et
|
||
`getGlobalRanking()`. Le ranking global est calculé comme la **somme** de
|
||
tous les scores nommés de chaque équipe.
|
||
- **Raison** : couvre les deux cas usuels (« qui a le plus de kills ? » et
|
||
« qui a la meilleure perf globale ? ») sans imposer de pondération par
|
||
défaut. Un jeu qui veut une formule custom (pondérée, ratio, …) override
|
||
`rank(ToIntFunction<Team>)` ou ajoute une méthode de service dans sa propre
|
||
sous-classe.
|
||
- **Format du résultat** : `record TeamRanking(int rank, Team team, int score)`.
|
||
Le `rank` est 1-based, le tri est descendant sur le score, le tiebreaker est
|
||
alphabétique (case-insensitive) sur le nom de l'équipe.
|
||
- **Records** : choix d'un record Java 16 plutôt qu'une classe immutable
|
||
manuelle — moins de boilerplate, `equals/hashCode/toString` gratuits. Les
|
||
records étant `final`, un jeu qui veut un type custom devra wrapper et
|
||
override `newRanking(...)` au niveau du service.
|
||
|
||
## 2026-06-08 — Spawn point par équipe (Bukkit Location)
|
||
|
||
- **Choix** : chaque `Team` peut avoir un `Location` Bukkit optionnel comme
|
||
point de spawn. Stocké en mémoire pour l'instant.
|
||
- **Clonage défensif** : `getSpawnPoint()` retourne un `Optional<Location>` où
|
||
la `Location` est **clonée** ; idem à l'entrée dans `setSpawnPoint`.
|
||
`Location` étant mutable côté Bukkit, ça évite que du code externe modifie
|
||
accidentellement le spawn en faisant `team.getSpawnPoint().get().setX(...)`.
|
||
- **Persistance différée** : `Location` n'est pas trivialement sérialisable
|
||
(référence au `World`). On utilisera `ConfigurationSerializable` quand on
|
||
branchera un repo fichier ; pour l'instant, le `InMemoryTeamRepository`
|
||
s'en moque.
|
||
- **Pas de téléport intégré** : le noyau ne fournit pas `teleportToSpawn(...)`.
|
||
C'est au plugin de jeu d'enchaîner `player.teleport(team.getSpawnPoint())`
|
||
s'il veut. La lib reste purement "data + règles".
|
||
|
||
## 2026-06-08 — Scores joueurs : domaine `player` indépendant du domaine `team`
|
||
|
||
- **Choix** : ajout d'un domaine `fr.luc.crcore.player` complet et parallèle au
|
||
domaine `team` : `PlayerProfile` (entité identifiée par l'UUID Bukkit),
|
||
`PlayerProfileService`, `PlayerProfileRepository`,
|
||
`InMemoryPlayerProfileRepository`, `PlayerRanking` (record),
|
||
`PlayerException` / `PlayerProfileNotFoundException`.
|
||
- **Pourquoi pas sur `TeamMember`** : `TeamMember` est immuable (transitions de
|
||
rôle via `withRole`), et un joueur peut changer/quitter une équipe — son
|
||
profil doit persister. Mettre les scores sur `TeamMember` aurait couplé la
|
||
durée de vie du score à l'appartenance à l'équipe.
|
||
- **Auto-création** : `addScore` / `setScore` créent le profil automatiquement
|
||
s'il n'existe pas (`getOrCreateProfile(playerId)`). Pas besoin d'appeler
|
||
explicitement un `register(playerId)` avant de tracker un score.
|
||
- **Symétrie** : les noms de méthodes, hooks et factories reflètent
|
||
exactement le domaine team (`newProfile`/`newTeam`,
|
||
`newRanking`/`newRanking`, `rank(scoreFn)`, `onScoreChanged`,
|
||
`getRankingByScore`, `getGlobalRanking`, etc.).
|
||
|
||
## 2026-06-08 — Interface `ScoreHolder` mutualisée
|
||
|
||
- **Choix** : extraction d'une interface `fr.luc.crcore.common.ScoreHolder`
|
||
qui déclare le contrat de scoring (`getScore`, `addScore`, `setScore`,
|
||
`resetScore`, `getTotalScore`, etc.). `Team` et `PlayerProfile`
|
||
l'implémentent.
|
||
- **Raison** : documenter le contrat et permettre du code générique côté
|
||
plugin de jeu (ex. `ScoreHolder.getTotalScore()` traité uniformément pour
|
||
l'affichage). Pas de classe abstraite partagée pour éviter le couplage
|
||
serré (Team et PlayerProfile ont des cycles de vie très différents) ; on
|
||
reste sur deux implémentations indépendantes mais avec un contrat commun.
|
||
- **Pas de `Scoreboard` aggregate** : envisagé un composant `Scoreboard`
|
||
composé dans Team et PlayerProfile, mais ça aurait imposé une indirection
|
||
pour ~8 méthodes simples. Choix actuel : duplication contrôlée des
|
||
implémentations (Map + getters/setters), interface commune pour le
|
||
contrat.
|
||
|
||
## 2026-06-09 — CRCore = bootstrap library, pas un plugin
|
||
|
||
- **Choix** : CR-Core reste une **librairie** (pas de `plugin.yml`, pas de
|
||
`JavaPlugin`). Le plugin de jeu downstream instancie `new CRCore(this)` dans
|
||
son `onEnable()` et appelle `.enable()` — c'est ce qui câble SQLite,
|
||
services, commandes et events.
|
||
- **Alternative écartée** : faire de CR-Core un plugin standalone (à
|
||
installer côté serveur). Refusé pour deux raisons : (1) chaque jeu a son
|
||
propre état (registre d'équipes, scores) — on ne veut pas partager entre
|
||
jeux par défaut ; (2) la friction de déploiement (2 jars sur le serveur)
|
||
est inutile pour des plugins shadés.
|
||
- **Conséquence** : chaque plugin de jeu shade CR-Core, a sa propre DB SQLite
|
||
dans son `dataFolder`, et déclare la commande Bukkit racine (`core` par
|
||
défaut) dans son `plugin.yml`.
|
||
|
||
## 2026-06-09 — Sous-commandes imbriquées récursives
|
||
|
||
- **Choix** : `AbstractCommand` porte la table des sous-commandes
|
||
(pas seulement `BaseCommand`). `SubCommand` peut donc avoir ses propres
|
||
sous-commandes (récursion). Routage via la méthode `dispatch(...)`
|
||
récursive.
|
||
- **Raison** : c'est ce qui permet `/core team create` (3 niveaux : root /
|
||
group / leaf). Sans ça, il faudrait flatter en `/core team-create` ou faire
|
||
du routage manuel dans chaque groupe.
|
||
- **Conséquence** : `BaseCommand` ne fait plus que pont Bukkit
|
||
(`CommandExecutor`/`TabCompleter` → `dispatch`) ; toute la logique de
|
||
routage vit dans `AbstractCommand`.
|
||
|
||
## 2026-06-09 — Override par sous-classe + `replaceSubCommand`
|
||
|
||
- **Choix** : `AbstractCommand.replaceSubCommand(name, newSub)` permet aux
|
||
plugins de jeu de remplacer une feuille (ex. `TeamCreateSubCommand`) par
|
||
leur propre implémentation, sans tout recâbler.
|
||
- **Raison** : le user a explicitement demandé "Les futures plugins ne
|
||
feront qu'override les fonctions si besoin". Cette méthode + le fait que
|
||
les classes ne soient pas `final` couvre les deux patterns :
|
||
- **Remplacement par instance** : `team.replaceSubCommand("create", new MyCreate(svc))`
|
||
- **Override par héritage** : `extends TeamCreateSubCommand` + `super.execute(ctx)`
|
||
|
||
## 2026-06-09 — Évènements Bukkit : post-only, non-cancellable
|
||
|
||
- **Choix** : tous les évènements CR-Core (team + player) sont **post-events**,
|
||
tirés via les hooks `on*` après commit. Aucun n'implémente `Cancellable`.
|
||
- **Raison** : la validation pré-action vit côté service dans les hooks
|
||
`validate*` (overridables). Mélanger pré-cancellable côté event et hooks
|
||
côté service dédoublerait les points de blocage. Pour bloquer un comportement,
|
||
le pattern est : override le hook `validate*` du service.
|
||
- **Boilerplate** : chaque event a sa propre `HandlerList` statique
|
||
(contrainte Bukkit, pas de moyen de partager via héritage). 12 events =
|
||
12 occurrences du même pattern, accepté pour rester idiomatique Bukkit.
|
||
|
||
## 2026-06-09 — SQLite write-through cache pour les repositories
|
||
|
||
- **Choix** : `SqliteTeamRepository` et `SqlitePlayerProfileRepository`
|
||
**étendent** leurs jumeaux `InMemory*` et overrident `save`/`delete` pour
|
||
persister synchronement vers SQLite. Au démarrage, `loadAll()` recharge
|
||
tout le state depuis la DB dans le cache mémoire.
|
||
- **Raison** : les lectures (findAll, findByName, classements en
|
||
`Collection<Team>.stream().sorted(...)`) restent rapides — pas de hit DB.
|
||
Les écritures vont en DB synchronement (acceptable au rythme des actions
|
||
joueur, qui sont rares à l'échelle d'un event entre amis).
|
||
- **Approche delete + reinsert pour les collections** : sur `save()`, on
|
||
remplace en bloc les `team_members` et `team_scores` d'une équipe (DELETE
|
||
puis INSERT). Plus simple et moins bug-prone qu'un diff fin, et négligeable
|
||
en perf pour des équipes de quelques joueurs.
|
||
|
||
## 2026-06-09 — Type Database minimaliste plutôt qu'un ORM
|
||
|
||
- **Choix** : `Database` expose 4 méthodes (execute / update / queryOne /
|
||
query) + un `TableBuilder` fluide. Pas d'ORM, pas d'annotations
|
||
d'entités, pas de DSL SQL.
|
||
- **Raison** : un ORM ajouterait une dépendance lourde (Hibernate / jOOQ /
|
||
…), un poids de classloading non négligeable côté serveur Bukkit, et
|
||
abstrairait des opérations qu'on veut garder triviales et lisibles. SQL
|
||
brut + PreparedStatement est largement suffisant pour les volumes d'un
|
||
serveur d'event.
|
||
- **Le `TableBuilder`** existe pour répondre au "pouvoir rapidement et
|
||
simplement créer des tables" — c'est l'API la plus user-friendly à proposer.
|
||
Pour les cas avancés (FOREIGN KEY, contraintes composites), l'utilisateur
|
||
passe par `db.execute("CREATE TABLE ...")` direct.
|
||
|
||
## 2026-06-09 — Préfixe `crcore_` sur toutes les tables internes
|
||
|
||
- **Choix** : `crcore_teams`, `crcore_team_members`, `crcore_team_scores`,
|
||
`crcore_player_profiles`, `crcore_player_scores`.
|
||
- **Raison** : éviter les collisions avec les tables custom que les plugins
|
||
de jeu créent dans la même DB. CR-Core et le plugin de jeu partagent le
|
||
même fichier SQLite (par défaut `<dataFolder>/crcore.db`) ; le préfixe
|
||
isole proprement.
|
||
|
||
## 2026-06-09 — Refonte permissions + modèle admin/chef/joueur
|
||
|
||
- **Choix** : chaque sous-commande `/core team <action>` a sa propre permission
|
||
`crcore.team.<action>`. Trois niveaux fonctionnels :
|
||
- **Admin** (permission seule, cible une team par argument) : `create`,
|
||
`delete`, `setleader`, `score`.
|
||
- **Chef** (permission + check chef dans `execute()`) : `add`, `remove`,
|
||
`transfer`, `visibility`, `setspawn`.
|
||
- **Joueur** (permission seule, défaut « tout le monde » côté LuckPerms si
|
||
voulu) : `join`, `leave`, `info`, `list`, `top`.
|
||
- **Aliases courts supprimés** : `c` (create), `i` (info), `t` (team), `j`
|
||
(join), `vis` (visibility), `disband`/`dissolve` (delete), `kick`/`expel`
|
||
(remove), etc. Plus que les noms longs. Raison : réduire la friction
|
||
d'apprentissage et la confusion (les game plugins ont leurs propres noms,
|
||
l'aliasing devient un bruit).
|
||
- **`delete` devient admin** : `/core team delete <team>` (au lieu de
|
||
l'ancien `/core team delete` qui ciblait l'équipe du chef). Cohérent avec
|
||
`create` qui est aussi admin.
|
||
|
||
## 2026-06-09 — Team peut être leaderless
|
||
|
||
- **Choix** : `Team.leaderId` devient nullable. `getLeaderId()` renvoie
|
||
`Optional<UUID>`, `getLeader()` renvoie `Optional<TeamMember>`. Nouveau
|
||
`hasLeader()` et `isLeader(UUID)` pour les checks.
|
||
- **Raison** : le modèle admin requiert qu'on puisse créer une équipe sans
|
||
chef et l'assigner ensuite via {@code setLeader}. Avant, créer une équipe
|
||
imposait de connaître l'UUID du chef.
|
||
- **Constructeurs ajoutés** :
|
||
- `new Team(id, name, tag, color)` — leaderless, PRIVATE
|
||
- `new Team(id, name, tag, color, visibility)` — leaderless avec visibilité
|
||
- Les constructeurs avec leaderId acceptent maintenant `null`.
|
||
- **`Team.setLeader(playerId)`** : assigne un chef à n'importe quel moment.
|
||
Si la team a déjà un chef, il est démis en MEMBER. Si le nouveau n'est
|
||
pas membre, il est auto-ajouté.
|
||
- **`Team.transferLeadership(playerId)`** : conserve sa sémantique stricte
|
||
(chef→chef, membre déjà existant). Lève `IllegalStateException` si la team
|
||
est leaderless. Utilisé par la commande `/core team transfer` (chef).
|
||
- **`TeamLeadershipTransferEvent.getOldLeaderId()`** renvoie maintenant
|
||
`Optional<UUID>` (vide si la team était leaderless avant l'opération).
|
||
- **Schéma SQLite** : la colonne `crcore_teams.leader_id` n'a plus la
|
||
contrainte `NOT NULL`. Migration automatique sur nouvelle base — pour les
|
||
bases existantes, ALTER TABLE manuel ou suppression du fichier
|
||
(les bases d'event sont jetables).
|
||
|
||
## 2026-06-10 — Feature modération (skeleton)
|
||
|
||
- **Choix** : nouveau module `fr.luc.crcore.features.moderation`,
|
||
opt-in via `CRCoreConfig.setupModeration()`. Skeleton complet :
|
||
- `ModerationState` (snapshot total : inv, armor, offhand, XP, health,
|
||
food, location, gamemode, flight/fly speed)
|
||
- `ModerationService` (interface) + `ModerationServiceImpl` +
|
||
`BukkitEventFiringModerationServiceImpl` (tire `ModerationEnterEvent`
|
||
/ `ModerationExitEvent`)
|
||
- `ModeratorTool` interface + `ModeratorToolRegistry`
|
||
- 5 outils squelette (slots 0, 1, 2, 7, 8) :
|
||
`TeleportRandomPlayerTool`, `InventorySpyTool`, `FreezeTool`,
|
||
`VanishToggleTool`, `ExitTool`
|
||
- `ModerationListener` unique : route les clics → tool, lock hotbar
|
||
(drop, swap, inventory click sur self), vanish-on-join,
|
||
freeze (PlayerMoveEvent cancel)
|
||
- `/core admin` toggle on/off (perm `crcore.admin`, player-only)
|
||
- **Vanish** : `Player.hidePlayer(plugin, vanished)` — propre, sans
|
||
modif visuelle, pas de fake-quit. Le listener re-applique
|
||
automatiquement aux joueurs qui join.
|
||
- **Persistance in-memory uniquement** (skeleton). Si le serveur crash
|
||
pendant qu'un mod est en mod mode, son inventaire est perdu. Une
|
||
impl `SqliteModerationRepository` avec sérialisation
|
||
`BukkitObjectOutputStream` → base64 est prévue pour plus tard. Pas
|
||
bloquant pour la première itération.
|
||
- **Mod mode → CREATIVE + allowFlight** par défaut. Choisi pour la
|
||
visibilité totale + mobility. Override possible en sous-classant
|
||
`ModerationServiceImpl.enter()`.
|
||
- **Outils en slots fixes** plutôt qu'avec PersistentDataContainer.
|
||
Plus simple ; le `ModerationListener` annule tout drop/swap pour
|
||
garantir que les outils restent en place. Pour identification croisée
|
||
(NBT) on pourra ajouter plus tard.
|
||
- **Tools extensibles** : `core.moderation().getToolRegistry().register(...)`
|
||
côté game plugin. Les slots libres (3, 4, 5, 6) sont prêts pour les
|
||
outils custom.
|
||
- **Listener responsibilities** : un seul `ModerationListener`
|
||
centralise toutes les hooks (clicks, freeze, vanish, lock hotbar).
|
||
Évite de disperser dans 5 listeners séparés.
|
||
|
||
## 2026-06-10 — Réorganisation : `util/` (toujours) vs `features/` (opt-in)
|
||
|
||
- **Choix** : séparation nette en deux couches :
|
||
- `fr.luc.crcore.util.*` — couche **utilitaire**, toujours active : common,
|
||
command framework, database, message, broadcast, gui, placeholder.
|
||
- `fr.luc.crcore.features.*` — couche **features**, opt-in : team, player.
|
||
- `fr.luc.crcore.builtin.*` — les commandes top-level CoreCommand +
|
||
CoreReloadSubCommand (pas un util, pas une feature, mais le routing
|
||
global).
|
||
- **Renames de FQN** (importer côté plugin de jeu si on les utilise) :
|
||
- `fr.luc.crcore.common.*` → `fr.luc.crcore.util.common.*`
|
||
- `fr.luc.crcore.command.*` → `fr.luc.crcore.util.command.*`
|
||
(le framework — Command, BaseCommand, SubCommand, ArgumentType…)
|
||
- `fr.luc.crcore.command.builtin.team.*`
|
||
→ `fr.luc.crcore.features.team.command.*`
|
||
(les 14+ Team*SubCommand)
|
||
- `fr.luc.crcore.command.builtin.CoreCommand` / `CoreReloadSubCommand`
|
||
→ `fr.luc.crcore.builtin.*`
|
||
- `fr.luc.crcore.database.*` → `fr.luc.crcore.util.database.*`
|
||
- `fr.luc.crcore.message.*` → `fr.luc.crcore.util.message.*`
|
||
- `fr.luc.crcore.broadcast.*` → `fr.luc.crcore.util.broadcast.*`
|
||
- `fr.luc.crcore.gui.*` → `fr.luc.crcore.util.gui.*`
|
||
- `fr.luc.crcore.placeholder.*` → `fr.luc.crcore.util.placeholder.*`
|
||
- `fr.luc.crcore.team.*` → `fr.luc.crcore.features.team.*`
|
||
(et sous-packages event/exception/impl/config)
|
||
- `fr.luc.crcore.player.*` → `fr.luc.crcore.features.player.*`
|
||
- **Raison** : prépare la modularisation à long terme. Chaque feature est
|
||
isolée dans son sous-package `features/<nom>/` et peut éventuellement
|
||
être extraite en module Maven séparé plus tard. Les utils sont
|
||
partagés. Le top-level reste minimal (CRCore, CRCoreConfig, builtin).
|
||
|
||
## 2026-06-10 — Setup modulaire via `CRCoreConfig.setupX()`
|
||
|
||
- **Choix** : les features sont désormais **opt-in**. Par défaut une
|
||
instance de `CRCoreConfig` n'active **aucune** feature ; le plugin de
|
||
jeu opt-in via :
|
||
- `setupTeams()` — active team service, repo, config + GUI, sous-cmds
|
||
- `setupPlayers()` — active player profile service + repo
|
||
- `setupPlaceholders()` — active la hook PAPI (no-op si PAPI absent)
|
||
- `setupAll()` — raccourci, active tout
|
||
- **Comportement quand off** : les getters de service (ex.
|
||
`core.getTeamService()`) lèvent `IllegalStateException` avec un
|
||
message explicite. Les commandes built-in correspondantes ne sont
|
||
simplement pas enregistrées (ex. `/core team` n'existe pas si teams
|
||
off).
|
||
- **Util toujours actif** : messages, broadcasts, GUI framework, command
|
||
framework, database sont systématiquement chargés. C'est la couche
|
||
infrastructure que les features et les game plugins consomment.
|
||
- **Listeners Bukkit toujours register** : `CRCoreBroadcastListener` et
|
||
`GuiListener` sont register unconditionnellement. Si aucune feature ne
|
||
tire d'event, ils sont idle — aucun coût.
|
||
- **Snippet d'usage** :
|
||
```java
|
||
this.core = new CRCore(this,
|
||
new CRCoreConfig().setupAll()).enable();
|
||
// ou granular :
|
||
this.core = new CRCore(this,
|
||
new CRCoreConfig().setupTeams()).enable();
|
||
```
|
||
- **Raison** : un game plugin qui n'a pas besoin des teams ne charge
|
||
pas le service ; pas de fichier `<plugin>-team-config.yml` créé, pas
|
||
de table `crcore_teams` créée, pas de sous-commande `/core team`. La
|
||
surface est minimale par défaut.
|
||
|
||
## 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
|
||
`BroadcastService` + `BroadcastAudience` enum + `BroadcastContext` data
|
||
class + `YamlBroadcastService` impl. Un listener Bukkit interne
|
||
(`CRCoreBroadcastListener`) écoute les 12 events CR-Core et les traduit
|
||
en appels `broadcast(eventKey, ctx)`.
|
||
- **Modèle « un seul fichier par plugin »** identique à messages :
|
||
`<plugin-dataFolder>/<plugin-name-lowercase>-broadcasts.yml`. Defaults
|
||
bundlés dans le jar à `crcore-broadcasts.yml`, copiés au premier boot
|
||
(avec priorité au template du plugin de jeu sous le même nom s'il en
|
||
fournit un).
|
||
- **Séparation routes / templates** :
|
||
- **Routes** = qui reçoit quoi = `<plugin>-broadcasts.yml` (liste
|
||
d'audiences par event)
|
||
- **Templates** = quel texte = `<plugin>-messages.yml` (clés
|
||
`<eventKey>.broadcast`)
|
||
- L'admin peut modifier l'un sans toucher à l'autre. Modulaire.
|
||
- **5 audiences** : `NONE`, `LEADER`, `TEAM`, `ADMIN`, `ALL`.
|
||
Multi-cibles via liste, union sans doublon.
|
||
- **Permission ADMIN** : `crcore.broadcast.admin` (granular,
|
||
configurable côté LuckPerms).
|
||
- **Listener Bukkit interne** : `CRCoreBroadcastListener` est instancié
|
||
et enregistré dans `CRCore.enable()`. Les game plugins n'ont rien à
|
||
faire pour bénéficier du broadcast des events natifs CR-Core ; pour
|
||
leurs propres events, ils appellent `core.broadcasts().broadcast(...)`.
|
||
- **Pas de cancellation** : le broadcast est post-event ; si une route
|
||
est mal configurée, on ne casse pas la logique métier — au pire un
|
||
message non envoyé ou envoyé trop large.
|
||
- **Nouvelle commande `/core reload`** : permission `crcore.reload`,
|
||
recharge `messages` + `broadcasts` depuis les fichiers user. Les
|
||
defaults en jar restent fixes. Hot reload utile en dev / pour ajuster
|
||
les routes sans restart.
|
||
- **Override de l'impl** : `CRCore.buildBroadcastService(messages)` est
|
||
`protected` — comme pour les autres services.
|
||
|
||
## 2026-06-09 — Réorganisation packages : `impl/` et `exception/` séparés
|
||
|
||
- **Choix** : pour chaque domaine (`team`, `player`, `message`), les
|
||
implémentations passent dans un sous-package `impl/` et les exceptions dans
|
||
un sous-package `exception/`. Le top-level du package ne contient plus que
|
||
les contrats publics (interfaces, entités, enums, values).
|
||
- **Conséquences sur les FQN publics** (importer côté plugin de jeu si on les
|
||
utilise) :
|
||
- `fr.luc.crcore.team.TeamException` → `fr.luc.crcore.team.exception.TeamException`
|
||
(et ses 3 sous-classes)
|
||
- `fr.luc.crcore.team.TeamServiceImpl` → `fr.luc.crcore.team.impl.TeamServiceImpl`
|
||
(idem `BukkitEventFiring*`, `InMemory*Repository`, `Sqlite*Repository`)
|
||
- `fr.luc.crcore.player.PlayerException` → `fr.luc.crcore.player.exception.PlayerException`
|
||
(et `PlayerProfileNotFoundException`)
|
||
- `fr.luc.crcore.player.*Impl` / `*Repository` impl → `fr.luc.crcore.player.impl.*`
|
||
- `fr.luc.crcore.message.YamlMessagesService` → `fr.luc.crcore.message.impl.YamlMessagesService`
|
||
- **Inchangés** : tous les enums, entités, interfaces de service et de repo,
|
||
ranking records, events (qui étaient déjà dans un sous-package
|
||
{@code event/}).
|
||
- **Raison** : lisibilité. Un dev qui ouvre `fr.luc.crcore.team/` voit
|
||
immédiatement les contrats (Team, TeamService, TeamRepository, enums,
|
||
events) sans se faire noyer par les impls. Pour overrider, il sait où
|
||
chercher (`impl/`). Pour catch une exception, il sait où chercher
|
||
(`exception/`).
|
||
- **Convention top-level vs impl/** :
|
||
- **Top-level** = ce qu'un consommateur doit connaître pour utiliser ou
|
||
étendre l'API : interfaces, entités, enums, values, events.
|
||
- **impl/** = ce que CR-Core fournit par défaut, qu'un game plugin peut
|
||
swap. C'est aussi là que vivent les sous-classes utilisées en interne
|
||
par le bootstrap (BukkitEventFiring*ServiceImpl, Sqlite*Repository).
|
||
- **Pas appliqué à `database/`, `command/` et `common/`** : ces packages
|
||
sont déjà petits et bien lisibles ; ajouter `impl/` à 3 fichiers serait
|
||
cosmétique.
|
||
|
||
## 2026-06-09 — `MessagesService` : YAML externalisable, un seul fichier par plugin
|
||
|
||
- **Choix** : nouveau module `fr.luc.crcore.message` avec une interface
|
||
`MessagesService` et une impl `YamlMessagesService`. Toutes les chaînes
|
||
utilisateur des commandes built-in passent par ce service.
|
||
- **Modèle « un seul fichier par plugin »** :
|
||
- Defaults CR-Core embarqués dans le jar à
|
||
`resources/crcore-messages.yml` — **jamais écrits sur disque**, juste
|
||
chargés en mémoire comme couche de fallback.
|
||
- Fichier user unique :
|
||
`<plugin-dataFolder>/<plugin-name-lowercase>-messages.yml`. Auto-créé
|
||
au premier `enable()` à partir du template du plugin de jeu s'il en
|
||
bundle un sous le même nom, sinon à partir des defaults CR-Core.
|
||
- Lecture : le fichier user écrase les defaults sur les mêmes clés ;
|
||
une clé manquante retombe automatiquement sur le default CR-Core (donc
|
||
une future release CR-Core qui ajoute une clé marche sans intervention
|
||
admin).
|
||
- **Pourquoi un seul fichier** : (1) UX admin — il édite un fichier, pas
|
||
deux ; (2) le plugin de jeu peut pré-remplir le template avec ses
|
||
overrides + ses propres messages en bundlant simplement son fichier
|
||
homonyme dans les ressources ; (3) zéro maintenance pour les clés
|
||
inchangées — elles restent en jar.
|
||
- **Substitution** : placeholders `{name}` style, varargs key/value,
|
||
codes couleur `&` traduits automatiquement.
|
||
- **Override de l'impl** : `CRCore.buildMessagesService()` protected,
|
||
surchargeable pour passer à une autre source (DB, microservice, etc.).
|
||
- **Pas de programmatique-only** : le service supporte `set(key, template)`
|
||
en mémoire pour des cas dynamiques, mais le mode principal reste le
|
||
YAML pour l'éditabilité par l'admin sans recompile.
|
||
|
||
## 2026-06-09 — Toutes les commandes "chef" deviennent admin (révision)
|
||
|
||
- **Révision** de la décision "Refonte permissions + modèle admin/chef/joueur"
|
||
prise plus tôt aujourd'hui.
|
||
- **Choix** : le rôle chef n'apporte plus aucun privilège de commande pour
|
||
l'instant. Toutes les opérations de gestion d'équipe (`add`, `remove`,
|
||
`transfer`, `visibility`, `setspawn`) deviennent **admin** :
|
||
- Signature avec `<team>` en argument (au lieu d'implicite "ma team").
|
||
- Permission `crcore.team.<action>` requise.
|
||
- Plus de check `isLeader(...)` dans `execute()`.
|
||
- **Raison** : le user a explicitement décidé que pour l'instant le chef
|
||
n'a pas plus de privilèges qu'un joueur lambda côté commandes. Le rôle
|
||
`LEADER` reste dans le modèle de données (utile pour les game plugins qui
|
||
pourraient l'exploiter via l'API, ou pour de futures commandes), mais il
|
||
ne gate plus rien au niveau du framework de commandes.
|
||
- **Conséquences** :
|
||
- `TeamRemoveSubCommand` : refuse de retirer le chef (l'admin doit
|
||
`setleader` d'abord). Pas un check chef, juste une garde de cohérence.
|
||
- `TeamTransferSubCommand` : devient l'équivalent admin "strict" de
|
||
`setleader` (membre existant uniquement). Les deux cohabitent ; doc dit
|
||
quand préférer l'un ou l'autre.
|
||
- `TeamSetSpawnSubCommand` : reste `playerOnly` car nécessite la
|
||
`Location` de l'exécutant — mais c'est désormais l'admin qui se place
|
||
à l'endroit voulu et tape `/core team setspawn <team>`.
|
||
|
||
## 2026-06-09 — Intégration PlaceholderAPI (optionnelle, auto-détectée)
|
||
|
||
- **Choix** : `CRCore.enable()` détecte la présence du plugin PlaceholderAPI
|
||
via `pluginManager.getPlugin("PlaceholderAPI")` et enregistre
|
||
automatiquement `CRCorePlaceholderExpansion` si présent. Aucune action
|
||
requise côté plugin de jeu.
|
||
- **Dépendance Maven** : `me.clip:placeholderapi:2.11.6` en scope
|
||
`provided` (depuis `https://repo.extendedclip.com/...`). Le jar PAPI
|
||
n'est PAS embarqué — c'est un plugin runtime indépendant.
|
||
- **Indirection de chargement** : la méthode privée
|
||
`doRegisterPlaceholderHook()` isole la référence à
|
||
`CRCorePlaceholderExpansion`. Si PAPI est absent, la méthode n'est jamais
|
||
appelée et le bytecode référençant `me.clip.placeholderapi.*` n'est pas
|
||
vérifié → pas de `NoClassDefFoundError`.
|
||
- **Placeholders exposés** :
|
||
- Team : `%crcore_team%`, `%crcore_team_name/tag/color/color_chat/size/`
|
||
`visibility/leader_name/total_score%`, `%crcore_team_score_<name>%`
|
||
- Player : `%crcore_player_score_<name>%`, `%crcore_player_score_total%`
|
||
- **Override** : `CRCore.registerPlaceholderHook()` est `protected` — une
|
||
sous-classe peut ajouter des placeholders ou skipper la hook.
|
||
|
||
## 2026-06-09 — Nouvelle commande `/core team setleader`
|
||
|
||
- **Choix** : ajout de `TeamSetLeaderSubCommand` (`/core team setleader
|
||
<team> <player>`). Permission `crcore.team.setleader`. Délègue à
|
||
`TeamService.setLeader(...)`.
|
||
- **Différence avec `/core team transfer`** :
|
||
- `transfer` : action **chef**, cible son équipe, le nouveau chef doit déjà
|
||
être membre.
|
||
- `setleader` : action **admin**, cible n'importe quelle équipe, le nouveau
|
||
chef peut être non-membre (auto-ajouté).
|
||
- **Use cases couverts** :
|
||
- Admin assigne un chef à une équipe leaderless fraîchement créée.
|
||
- Admin remplace le chef d'une équipe (le membre target est déjà dans
|
||
l'équipe ou pas — peu importe).
|
||
- Admin "promote member up to leader" (cas explicitement demandé).
|
||
|
||
## 2026-06-09 — Bascule Java 16 → Java 11 (révision)
|
||
|
||
- **Révision** de la décision "Java 16" du 2026-06-08.
|
||
- **Choix** : `maven.compiler.source/target = 11`. Le code se compile et
|
||
s'exécute sur tout JDK 11+.
|
||
- **Raison** : Java 11 reste très répandu côté serveurs Bukkit/Paper 1.16.5,
|
||
et le coût de revenir en arrière est faible. On garde une cible plus
|
||
conservatrice pour maximiser la compatibilité d'exécution.
|
||
- **Conséquences sur le code** :
|
||
- Les `record` (Java 16) → classes immutables manuelles, avec mêmes noms
|
||
d'accesseurs (`rank()`, `team()`, etc.) pour ne pas casser l'API
|
||
publique. Concerné : `TeamRanking`, `PlayerRanking`, plus deux tuples
|
||
internes (`TeamRow`, `MemberRow`) dans `SqliteTeamRepository`.
|
||
- Le **pattern matching `instanceof X x`** (Java 16) → classique
|
||
`instanceof X` + cast explicite. Concerné : `CommandContext.requirePlayer`,
|
||
`Database.normalize`.
|
||
- Les **switch expressions à flèche** (`case X -> ...`, Java 14) →
|
||
`switch (...) { case X: ...; break; }` classique, ou chaînes if/else.
|
||
Concerné : `BaseCommand.handleResult`, `ArgumentTypes.BOOLEAN.parse`,
|
||
`TeamScoreSubCommand.execute`.
|
||
- **Ce qui reste utilisé de Java 11** : `var` (Java 10+), `List.of()` /
|
||
`Map.of()` (Java 9+), interfaces avec méthodes `default`, lambdas, method
|
||
references.
|
||
|
||
## 2026-06-09 — Enregistrement dynamique de la commande (plugin.yml optionnel)
|
||
|
||
- **Choix** : `CRCore.registerCommand()` tente d'abord
|
||
`plugin.getCommand(name)`. Si la commande n'est pas déclarée dans le
|
||
`plugin.yml` du plugin hôte, fallback sur enregistrement dynamique via le
|
||
`CommandMap` interne du serveur (accédé par réflexion sur
|
||
`CraftServer.commandMap`).
|
||
- **Raison** : un plugin de jeu peut maintenant utiliser CR-Core en
|
||
**changeant uniquement le `pom.xml` + une instanciation de `CRCore`** —
|
||
zéro modification du `plugin.yml`. Si l'utilisateur veut customiser
|
||
description/aliases côté Bukkit, il peut quand même déclarer la commande
|
||
dans plugin.yml ; CR-Core détecte et utilise cette déclaration.
|
||
- **Wrapper Bukkit** : on crée une `org.bukkit.command.Command` anonyme
|
||
qui délègue `execute` / `tabComplete` au `CoreCommand` (qui est notre
|
||
`BaseCommand`). On copie nom + aliases + description depuis le
|
||
`CoreCommand` vers le wrapper.
|
||
- **Réflexion** : stable sur Paper 1.16.5 ; le champ `commandMap` de
|
||
`CraftServer` existe depuis longtemps. Si une version future cassait
|
||
l'accès, le code log un severe et continue (les autres features de
|
||
CRCore restent fonctionnelles).
|