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>
329 lines
12 KiB
Markdown
329 lines
12 KiB
Markdown
# Setup technique
|
|
|
|
## Stack
|
|
|
|
- **Type** : librairie Java (`jar`) — pas un plugin Bukkit
|
|
- **artifactId Maven** : `CR-Core`
|
|
- **Build** : Maven, Java 11
|
|
- **Intégrations optionnelles** : PlaceholderAPI (auto-détectée si installée)
|
|
- **API serveur (provided)** : Paper 1.16.5
|
|
- **SQLite (compile)** : `org.xerial:sqlite-jdbc:3.45.3.0`
|
|
- **Package racine** : `fr.luc.crcore`
|
|
|
|
## Dépôts Maven
|
|
|
|
- `papermc` — https://repo.papermc.io/repository/maven-public/
|
|
- `spigot-repo` — https://hub.spigotmc.org/nexus/content/repositories/snapshots/
|
|
- `sonatype` — https://oss.sonatype.org/content/groups/public/
|
|
|
|
## Build & install local
|
|
|
|
```bash
|
|
mvn clean install
|
|
```
|
|
|
|
Publie `fr.luc:CR-Core:1.0-SNAPSHOT` dans le repo Maven local `~/.m2/`,
|
|
prêt à être consommé par les plugins de jeu.
|
|
|
|
## Intégration dans un plugin de jeu
|
|
|
|
### `pom.xml`
|
|
|
|
```xml
|
|
<dependency>
|
|
<groupId>fr.luc</groupId>
|
|
<artifactId>CR-Core</artifactId>
|
|
<version>1.0-SNAPSHOT</version>
|
|
<scope>compile</scope>
|
|
</dependency>
|
|
```
|
|
|
|
Le plugin de jeu doit **shader** CR-Core dans son jar final (avec
|
|
`maven-shade-plugin`) pour que sqlite-jdbc + le code du noyau soient bien
|
|
embarqués sur le serveur.
|
|
|
|
### `plugin.yml` du plugin de jeu
|
|
|
|
```yaml
|
|
name: MyGame
|
|
main: fr.exemple.mygame.MyGamePlugin
|
|
version: 1.0
|
|
api-version: 1.16
|
|
```
|
|
|
|
> **Pas besoin de déclarer la commande `core`** : CR-Core l'enregistre
|
|
> dynamiquement via le `CommandMap` du serveur quand elle est absente du
|
|
> plugin.yml. Si tu préfères la déclarer quand même (pour customiser la
|
|
> description ou les aliases côté Bukkit), tu peux ajouter :
|
|
>
|
|
> ```yaml
|
|
> commands:
|
|
> core:
|
|
> description: Commandes CR-Core
|
|
> aliases: [cr, crcore]
|
|
> ```
|
|
>
|
|
> Dans ce cas, CR-Core détecte la commande déclarée et s'y branche
|
|
> normalement via `setExecutor` (pas d'enregistrement dynamique).
|
|
|
|
### Code minimal — setup modulaire
|
|
|
|
> ⚠️ **Important** : depuis 2026-06-10, les features (teams, players,
|
|
> placeholders) sont **opt-in** via `CRCoreConfig.setupX()`. Une instance
|
|
> `new CRCoreConfig()` n'active rien par défaut — il faut appeler
|
|
> `setupAll()` ou les `setupX()` individuels.
|
|
|
|
```java
|
|
public class MyGamePlugin extends JavaPlugin {
|
|
|
|
private CRCore core;
|
|
|
|
@Override
|
|
public void onEnable() {
|
|
// OPTION A — tout activer en une ligne (teams + players + placeholders)
|
|
this.core = new CRCore(this, new CRCoreConfig().setupAll()).enable();
|
|
|
|
// OPTION B — granularité, n'activer que ce qu'on veut
|
|
// this.core = new CRCore(this, new CRCoreConfig()
|
|
// .setupTeams()
|
|
// .setupPlaceholders() // pas de players
|
|
// .setupModeration()) // ajoute le mod mode
|
|
// .enable();
|
|
|
|
// OPTION C — par défaut + options
|
|
// this.core = new CRCore(this, new CRCoreConfig()
|
|
// .withSqliteFile("mygame.db")
|
|
// .withCommandName("game")
|
|
// .setupTeams())
|
|
// .enable();
|
|
|
|
// Listener custom sur les events team (nécessite setupTeams)
|
|
getServer().getPluginManager().registerEvents(new MyTeamListener(), this);
|
|
|
|
// Table custom pour stocker des données spécifiques au jeu
|
|
// (database util est toujours disponible)
|
|
core.getDatabase().table("my_kills")
|
|
.ifNotExists()
|
|
.column("player_id", ColumnType.UUID).primaryKey()
|
|
.column("kills", ColumnType.INTEGER).notNull().defaultValue("0")
|
|
.create();
|
|
}
|
|
|
|
@Override
|
|
public void onDisable() {
|
|
if (core != null) core.disable();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Écouter les évènements
|
|
|
|
```java
|
|
public class MyTeamListener implements Listener {
|
|
|
|
@EventHandler
|
|
public void onTeamCreate(TeamCreateEvent event) {
|
|
Team team = event.getTeam();
|
|
Bukkit.broadcastMessage("Nouvelle équipe : " + team.getName());
|
|
}
|
|
|
|
@EventHandler
|
|
public void onPlayerJoinTeam(PlayerJoinTeamEvent event) {
|
|
// Auto-join uniquement (chef qui ajoute = TeamMemberAddEvent)
|
|
}
|
|
|
|
@EventHandler
|
|
public void onScoreChange(TeamScoreChangeEvent event) {
|
|
// event.getScoreName(), event.getOldValue(), event.getNewValue()
|
|
}
|
|
}
|
|
```
|
|
|
|
### Overrider une commande par défaut
|
|
|
|
```java
|
|
public class MyCreateCommand extends TeamCreateSubCommand {
|
|
public MyCreateCommand(TeamService service) {
|
|
super(service);
|
|
permission("mygame.team.create"); // permission custom
|
|
}
|
|
|
|
@Override
|
|
public CommandResult execute(CommandContext ctx) {
|
|
// logique custom
|
|
return super.execute(ctx);
|
|
}
|
|
}
|
|
|
|
// Dans onEnable() :
|
|
core.getCoreCommand().findSubCommand("team")
|
|
.ifPresent(team -> team.replaceSubCommand("create", new MyCreateCommand(core.getTeamService())));
|
|
```
|
|
|
|
### Stocker / récupérer des données custom via SQLite
|
|
|
|
```java
|
|
Database db = core.getDatabase();
|
|
|
|
db.update("INSERT OR REPLACE INTO my_kills (player_id, kills) VALUES (?, ?)",
|
|
playerUuid, 42);
|
|
|
|
int kills = db.queryOne(
|
|
"SELECT kills FROM my_kills WHERE player_id = ?",
|
|
rs -> rs.getInt("kills"),
|
|
playerUuid
|
|
).orElse(0);
|
|
```
|
|
|
|
## Arborescence du projet
|
|
|
|
```
|
|
CitesPlugin/ # dossier IntelliJ (renommer plus tard si voulu)
|
|
├── pom.xml
|
|
├── GEMINI.md
|
|
├── docs/
|
|
│ ├── README.md
|
|
│ ├── setup.md
|
|
│ ├── features.md
|
|
│ ├── decisions.md
|
|
│ └── diagrams/*.puml
|
|
└── src/main/java/fr/luc/crcore/
|
|
├── CRCore.java # bootstrap (setupAll/setupTeams/...)
|
|
├── CRCoreConfig.java # config builder + flags features
|
|
├── builtin/ # commandes top-level (toutes features)
|
|
│ ├── CoreCommand.java # /core
|
|
│ └── CoreReloadSubCommand.java # /core reload
|
|
├── util/ # ◆ couche util — toujours active ◆
|
|
│ ├── common/ # abstractions partagées
|
|
│ │ ├── Identifiable.java
|
|
│ │ ├── Named.java
|
|
│ │ ├── ScoreHolder.java
|
|
│ │ ├── AbstractEntity.java
|
|
│ │ └── Repository.java
|
|
│ ├── database/ # wrapper SQLite générique
|
|
│ │ ├── Database.java
|
|
│ │ ├── TableBuilder.java
|
|
│ │ ├── ColumnType.java
|
|
│ │ ├── RowMapper.java
|
|
│ │ └── DatabaseException.java
|
|
│ ├── command/ # framework de commandes
|
|
│ │ ├── Command.java (interface)
|
|
│ │ ├── AbstractCommand.java
|
|
│ │ ├── BaseCommand.java
|
|
│ │ ├── SubCommand.java
|
|
│ │ ├── CommandContext.java
|
|
│ │ ├── CommandResult.java
|
|
│ │ ├── CommandException.java
|
|
│ │ ├── ArgumentType.java
|
|
│ │ ├── ArgumentTypes.java
|
|
│ │ └── ArgumentDef.java (package-private)
|
|
│ ├── message/ # service messages YAML
|
|
│ │ ├── MessagesService.java
|
|
│ │ └── impl/YamlMessagesService.java
|
|
│ ├── broadcast/ # service broadcasts YAML + listener
|
|
│ │ ├── BroadcastService.java
|
|
│ │ ├── BroadcastAudience.java
|
|
│ │ ├── BroadcastContext.java
|
|
│ │ ├── CRCoreBroadcastListener.java
|
|
│ │ └── impl/YamlBroadcastService.java
|
|
│ ├── gui/ # framework GUI (InventoryHolder)
|
|
│ │ ├── AbstractInventoryGui.java
|
|
│ │ ├── GuiClickHandler.java
|
|
│ │ ├── GuiListener.java
|
|
│ │ └── GuiItems.java
|
|
│ └── placeholder/ # PAPI expansion (opt-in)
|
|
│ └── CRCorePlaceholderExpansion.java
|
|
└── features/ # ◆ features opt-in via setupX() ◆
|
|
├── team/ (setupTeams())
|
|
│ ├── Team.java, TeamMember, enums, TeamRanking
|
|
│ ├── TeamService.java (interface)
|
|
│ ├── TeamRepository.java (interface)
|
|
│ ├── event/ (9 events Bukkit)
|
|
│ ├── exception/ (4 exceptions)
|
|
│ ├── impl/ (TeamServiceImpl, BukkitEventFiring*, InMemory*, Sqlite*)
|
|
│ ├── config/ (settings typés)
|
|
│ │ ├── TeamSetting.java
|
|
│ │ ├── TeamSettings.java (registry)
|
|
│ │ ├── TeamConfigService.java
|
|
│ │ ├── impl/YamlTeamConfigService.java
|
|
│ │ └── gui/ (Global + TeamSettingsGui)
|
|
│ │ ├── AbstractSettingsGui.java
|
|
│ │ ├── GlobalSettingsGui.java
|
|
│ │ └── TeamSettingsGui.java
|
|
│ └── command/ (14 sous-commandes /core team)
|
|
│ ├── TeamGroupSubCommand.java
|
|
│ ├── TeamArgumentTypes.java
|
|
│ └── Team*SubCommand.java
|
|
└── player/ (setupPlayers())
|
|
├── PlayerProfile.java
|
|
├── PlayerRanking.java
|
|
├── PlayerProfileService.java (interface)
|
|
├── PlayerProfileRepository.java (interface)
|
|
├── event/ (3 events)
|
|
├── exception/ (2 exceptions)
|
|
└── impl/ (services + repositories)
|
|
```
|
|
|
|
## Fichiers de config générés au premier `enable()`
|
|
|
|
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 à
|
|
la place des defaults CR-Core. Les defaults restent en mémoire en
|
|
fallback — donc les clés non présentes dans le fichier user marchent quand
|
|
même.
|
|
|
|
Hot reload : `/core reload` (permission `crcore.reload`) relit les deux
|
|
fichiers sans restart.
|
|
|
|
## Fichier messages
|
|
|
|
Au premier `enable()`, CR-Core crée :
|
|
|
|
```
|
|
<plugin-dataFolder>/<plugin-name-lowercase>-messages.yml
|
|
```
|
|
|
|
(ex. `cites-messages.yml` si ton plugin s'appelle `Cites`)
|
|
|
|
Édite ce fichier pour customiser tous les messages des commandes `/core team
|
|
...`. Les clés non présentes retombent sur les defaults CR-Core (dans le jar)
|
|
— donc tu peux n'inclure que ce que tu veux changer.
|
|
|
|
Pour pré-remplir ce fichier avec tes overrides + tes propres messages dès la
|
|
première exécution, bundle un fichier au **même nom** dans tes ressources
|
|
plugin (`src/main/resources/cites-messages.yml`). Si présent, c'est lui qui
|
|
sert de template au lieu des defaults CR-Core.
|
|
|
|
API en code :
|
|
```java
|
|
core.messages().get("team.create.success",
|
|
"name", team.getName(), "tag", team.getTag());
|
|
|
|
core.messages().set("mygame.custom", "&aHello {player} !");
|
|
core.messages().loadAdditional("my-extras.yml");
|
|
core.messages().reload(); // hot reload depuis le disque
|
|
```
|
|
|
|
## Tables SQLite créées par CR-Core
|
|
|
|
Au premier `enable()`, les tables suivantes sont créées (en `IF NOT EXISTS`) :
|
|
|
|
- `crcore_teams` — métadonnées par équipe (id, name, tag, color, leader_id,
|
|
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)
|
|
|
|
Le préfixe `crcore_` évite les collisions avec les tables custom du plugin
|
|
de jeu.
|