7ee349f206
CRCore.registerCommand() now falls back to dynamic registration via the server's internal CommandMap (reflection on CraftServer.commandMap) when the command is absent from the host plugin's plugin.yml. Game plugins can now use CR-Core with zero plugin.yml changes — just instantiate CRCore. If the command IS declared in plugin.yml, CR-Core detects it and uses setExecutor/setTabCompleter as before. pom.xml: distributionManagement targeting Gitea Packages (https://gitea.luc-rival.fr/api/packages/admin/maven), plus maven-source-plugin (3.3.0) and maven-javadoc-plugin (3.6.3) so each mvn deploy publishes the main jar, a -sources.jar and a -javadoc.jar. doclint=none on javadoc to tolerate partial doc. docs/setup.md: clarifies that the commands: entry in plugin.yml is now optional. docs/decisions.md: new decision logged for the dynamic registration approach. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
346 lines
19 KiB
Markdown
346 lines
19 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 — 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).
|