feat: moderation feature skeleton (/core admin + mod mode + tools)

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>
This commit is contained in:
Antone Barbaud
2026-06-10 12:19:21 +02:00
parent 84735221f1
commit 4efaa5bbde
30 changed files with 1630 additions and 20 deletions
+8 -2
View File
@@ -44,8 +44,13 @@ d'initialisation côté plugin de jeu :
- **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é.
- **Modération (skeleton)** — `/core admin` toggle pour passer en
mod mode : snapshot complet du joueur (inv, XP, location, gamemode),
vanish, hotbar dotée d'outils (tp joueur, inv spy, freeze, vanish
toggle, exit). `ModeratorTool` + registry extensibles. Persistance
SQLite à venir.
- **Bootstrap unique** — `new CRCore(this, new CRCoreConfig().setupAll()).enable()`
dans le `onEnable()` du plugin de jeu, et tout est branché.
## Structure de la documentation
@@ -72,6 +77,7 @@ d'initialisation côté plugin de jeu :
| [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 |
| [moderation-class-diagram.puml](diagrams/moderation-class-diagram.puml) | Classe | Feature modération (skeleton) |
| [bootstrap-sequence.puml](diagrams/bootstrap-sequence.puml) | Séquence | `CRCore.enable()` côté plugin de jeu |
## Conventions
+39
View File
@@ -367,6 +367,45 @@ 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 — 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 :
+176
View File
@@ -0,0 +1,176 @@
@startuml moderation-class-diagram
title CR-Core — Moderation feature (class diagram, skeleton)
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.features.moderation" {
class ModerationState <<final>> {
- playerId: UUID
- enteredAt: Instant
- inventoryContents: ItemStack[]
- armorContents: ItemStack[]
- offhandItem: ItemStack
- xpLevel: int
- xpProgress: float
- health: double
- foodLevel: int
- saturation: float
- location: Location
- gameMode: GameMode
- allowFlight / flying / walkSpeed / flySpeed
--
+ ModerationState(player)
+ restoreTo(player): void
}
interface ModerationRepository {
+ findByPlayer(uuid): Optional<ModerationState>
+ exists(uuid): boolean
+ save(state): void
+ delete(uuid): boolean
+ findAll(): Collection<ModerationState>
}
interface ModeratorTool {
+ getKey(): String
+ getSlot(): int ' 0..8
+ buildIcon(): ItemStack
+ onLeftClick(player): void
+ onRightClick(player): void
+ onInteractEntity(player, target): void
}
class ModeratorToolRegistry {
- bySlot: Map<Integer, ModeratorTool>
- byKey: Map<String, ModeratorTool>
+ register(tool): void
+ unregister(key): boolean
+ get(key): Optional<ModeratorTool>
+ getBySlot(slot): Optional<ModeratorTool>
+ all(): Collection<ModeratorTool>
}
interface ModerationService {
+ enter(player): void
+ exit(player): void
+ isInModeration(uuid): boolean
+ getState(uuid): Optional<ModerationState>
+ getActiveModerators(): Set<UUID>
--
+ vanish(player) / unvanish / isVanished / getVanishedPlayers
+ freeze(uuid) / unfreeze / isFrozen / getFrozenPlayers
+ getToolRegistry(): ModeratorToolRegistry
}
package "fr.luc.crcore.features.moderation.impl" {
class InMemoryModerationRepository
InMemoryModerationRepository ..|> ModerationRepository
class ModerationServiceImpl {
# plugin: Plugin
# repository: ModerationRepository
# toolRegistry: ModeratorToolRegistry
# vanished: Set<UUID>
# frozen: Set<UUID>
--
# onAfterEnter(player) ' hook
# onAfterExit(player)
}
ModerationServiceImpl ..|> ModerationService
class BukkitEventFiringModerationServiceImpl
BukkitEventFiringModerationServiceImpl --|> ModerationServiceImpl
class ModerationListener {
+ registerOn(plugin): void
--
@ onInteract ' route → tool.onLeftClick / onRightClick
@ onInteractEntity ' route → tool.onInteractEntity
@ onDrop / onSwap ' lock hotbar
@ onInventoryClick ' cancel sur self-inv
@ onJoin ' re-hide vanished players
@ onQuit ' unfreeze
@ onMove ' cancel si frozen
}
ModerationListener ..|> "org.bukkit.event.Listener"
ModerationListener --> ModerationService
}
package "fr.luc.crcore.features.moderation.tool" {
class ExitTool ' slot 8
class VanishToggleTool ' slot 7
class FreezeTool ' slot 2 (entity right-click)
class InventorySpyTool ' slot 1 (entity right-click)
class TeleportRandomPlayerTool ' slot 0
ExitTool ..|> ModeratorTool
VanishToggleTool ..|> ModeratorTool
FreezeTool ..|> ModeratorTool
InventorySpyTool ..|> ModeratorTool
TeleportRandomPlayerTool ..|> ModeratorTool
}
package "fr.luc.crcore.features.moderation.event" {
abstract class ModerationEvent {
- moderator: Player
+ getModerator(): Player
}
ModerationEvent --|> "org.bukkit.event.Event"
class ModerationEnterEvent
class ModerationExitEvent
ModerationEnterEvent --|> ModerationEvent
ModerationExitEvent --|> ModerationEvent
}
package "fr.luc.crcore.features.moderation.exception" {
class ModerationException
class ModerationAlreadyActiveException
class ModerationNotActiveException
ModerationException --|> RuntimeException
ModerationAlreadyActiveException --|> ModerationException
ModerationNotActiveException --|> ModerationException
}
package "fr.luc.crcore.features.moderation.command" {
class AdminToggleSubCommand {
+ execute(ctx): CommandResult
}
AdminToggleSubCommand ..> ModerationService
}
ModerationService ..> ModerationState : reads/writes
ModerationService o--> ModerationRepository
ModerationService o--> ModeratorToolRegistry
ModeratorToolRegistry o--> "*" ModeratorTool
}
package "fr.luc.crcore" {
class CRCore {
+ moderation(): ModerationService
# buildModerationService(repo, registry): ModerationService
# registerDefaultModeratorTools(registry, mod): void
}
CRCore "1" *-- "0..1" ModerationService : owns (if setupModeration)
CRCore ..> ModerationListener : registers
}
note bottom of ModerationServiceImpl
enter(player) :
1. ModerationState snapshot → repo.save
2. clear inventory, set tools in hotbar
3. gamemode CREATIVE + allowFlight
4. vanish(player)
5. onAfterEnter() → event fired
exit(player) :
1. state.restoreTo(player) ' inv + xp + loc + gm + flight
2. unvanish(player)
3. unfreeze + repo.delete
4. onAfterExit() → event fired
Skeleton : in-memory repository only.
Persistance SQLite à venir.
end note
@enduml
+118 -1
View File
@@ -920,7 +920,124 @@ est déjà enregistré par `CRCore.enable()`.
---
## 11. Bootstrap `CRCore`
## 11. Modération (`fr.luc.crcore.features.moderation`)
**Statut** : skeleton. Architecture complète, 5 outils squelette, vanish
+ freeze fonctionnels. Persistance SQLite et outils avancés à venir.
### Mod mode
`/core admin` (perm `crcore.admin`, player-only) toggle on/off le mod
mode pour l'exécutant :
- **À l'entrée** :
1. Snapshot complet du joueur (`ModerationState`) : inventory + armor
+ offhand, XP level + progress, health, food, location, gamemode,
allowFlight + flying, walk/fly speed.
2. Inventaire vidé, hotbar peuplée avec les outils du
`ModeratorToolRegistry`.
3. Passage en `CREATIVE` + `allowFlight=true`.
4. Vanish actif (caché de tous les joueurs en ligne).
5. Event Bukkit `ModerationEnterEvent` tiré.
- **À la sortie** : restauration intégrale du snapshot + retrait du
vanish + cleanup freeze + `ModerationExitEvent`.
### Outils squelette (hotbar)
| Slot | Outil | Action |
|---|---|---|
| 0 | `TeleportRandomPlayerTool` (compass) | clic droit → tp à un joueur aléatoire |
| 1 | `InventorySpyTool` (chest) | clic droit sur joueur → ouvre son inv |
| 2 | `FreezeTool` (ice) | clic droit sur joueur → toggle freeze |
| 7 | `VanishToggleTool` (ender eye) | clic → toggle vanish |
| 8 | `ExitTool` (barrier) | clic → exit mod mode |
Slots libres (3, 4, 5, 6) prêts pour des outils custom du game plugin.
### Ajouter un outil custom
```java
public class WarnTool implements ModeratorTool {
@Override public String getKey() { return "warn"; }
@Override public int getSlot() { return 4; }
@Override public ItemStack buildIcon() {
return GuiItems.named(Material.PAPER, "&eWarn").build();
}
@Override public void onInteractEntity(Player mod, Entity target) {
if (target instanceof Player) {
((Player) target).sendMessage("§eTu as reçu un warn !");
}
}
}
// Côté game plugin onEnable() :
core.moderation().getToolRegistry().register(new WarnTool());
```
### Vanish
Le vanish utilise `Player.hidePlayer(plugin, vanished)` pour cacher de
tous les autres joueurs. Le `ModerationListener` re-applique
automatiquement le vanish aux joueurs qui rejoignent le serveur.
### Freeze
`moderation.freeze(uuid)` ajoute l'UUID à un `Set` en mémoire. Le
`ModerationListener` cancel les `PlayerMoveEvent` qui changent de bloc.
La rotation de la tête reste libre (UX). Le freeze est retiré
automatiquement à la déconnexion du joueur.
### `ModerationListener` — verrouillage hotbar
Tant qu'un joueur est en mod mode :
- Les drops d'items sont annulés.
- Le swap main/offhand est annulé.
- Les clics dans son propre inventaire sont annulés. Les clics dans un
inventaire ouvert par un outil (ex. `InventorySpy`) restent libres.
- Les `PlayerInteractEvent` sur la hotbar routent vers le tool du slot
tenu, avec annulation pour éviter toute interaction réelle avec le
monde.
### Events Bukkit
- `ModerationEnterEvent` — après snapshot + vanish + équipement
- `ModerationExitEvent` — après restore + retrait vanish
### Limites du skeleton — TODO
- **Persistance** : `InMemoryModerationRepository` uniquement. Si le
serveur crash pendant qu'un mod est en mod mode, son inventaire est
perdu. À ajouter : `SqliteModerationRepository` avec sérialisation
Bukkit (`BukkitObjectOutputStream` → base64) des `ItemStack[]`.
- **InventorySpyTool** : ouvre l'inventaire réel — toute modif est
appliquée. Pour de l'audit pur, wrap dans une `Inventory` cloné en
read-only.
- **TeleportRandomPlayerTool** : placeholder. Remplacer par un GUI
sélecteur de joueurs.
- **Pas de broadcasts** : pas encore d'entrées
`moderation.enter.broadcast` / `moderation.exit.broadcast`. Trivial à
ajouter au système broadcasts en suivant le pattern existant.
- **Pas d'autre toggle** que self — `/core admin <player>` n'existe pas
encore.
### Setup côté plugin de jeu
```java
this.core = new CRCore(this,
new CRCoreConfig().setupModeration()) // ou setupAll()
.enable();
// Ajouter un outil custom (optionnel)
core.moderation().getToolRegistry().register(new MyWarnTool());
```
### Diagramme
- [moderation-class-diagram.puml](diagrams/moderation-class-diagram.puml)
---
## 12. Bootstrap `CRCore`
**Statut** : implémenté. Point d'entrée unique pour les plugins de jeu.
+2 -1
View File
@@ -86,7 +86,8 @@ public class MyGamePlugin extends JavaPlugin {
// OPTION B — granularité, n'activer que ce qu'on veut
// this.core = new CRCore(this, new CRCoreConfig()
// .setupTeams()
// .setupPlaceholders()) // pas de players
// .setupPlaceholders() // pas de players
// .setupModeration()) // ajoute le mod mode
// .enable();
// OPTION C — par défaut + options
+61 -2
View File
@@ -1,6 +1,17 @@
package fr.luc.crcore;
import fr.luc.crcore.builtin.CoreCommand;
import fr.luc.crcore.features.moderation.ModerationRepository;
import fr.luc.crcore.features.moderation.ModerationService;
import fr.luc.crcore.features.moderation.ModeratorToolRegistry;
import fr.luc.crcore.features.moderation.impl.BukkitEventFiringModerationServiceImpl;
import fr.luc.crcore.features.moderation.impl.InMemoryModerationRepository;
import fr.luc.crcore.features.moderation.impl.ModerationListener;
import fr.luc.crcore.features.moderation.tool.ExitTool;
import fr.luc.crcore.features.moderation.tool.FreezeTool;
import fr.luc.crcore.features.moderation.tool.InventorySpyTool;
import fr.luc.crcore.features.moderation.tool.TeleportRandomPlayerTool;
import fr.luc.crcore.features.moderation.tool.VanishToggleTool;
import fr.luc.crcore.features.player.PlayerProfileRepository;
import fr.luc.crcore.features.player.PlayerProfileService;
import fr.luc.crcore.features.player.impl.BukkitEventFiringPlayerProfileServiceImpl;
@@ -90,6 +101,9 @@ public class CRCore {
// Features player
private PlayerProfileRepository playerProfileRepository;
private PlayerProfileService playerProfileService;
// Features moderation
private ModerationRepository moderationRepository;
private ModerationService moderationService;
// Command routing
private CoreCommand coreCommand;
private boolean enabled = false;
@@ -139,6 +153,14 @@ public class CRCore {
: new InMemoryPlayerProfileRepository();
this.playerProfileService = buildPlayerProfileService(playerProfileRepository);
}
if (config.isModerationEnabled()) {
// Skeleton : repo in-memory pour l'instant (SQLite à venir).
this.moderationRepository = new InMemoryModerationRepository();
ModeratorToolRegistry toolRegistry = new ModeratorToolRegistry();
this.moderationService = buildModerationService(moderationRepository, toolRegistry);
registerDefaultModeratorTools(toolRegistry, moderationService);
new ModerationListener(plugin, moderationService).registerOn(plugin);
}
// ---- 4. Listeners Bukkit ----
// Broadcast listener : écoute les events team + player. S'il n'y a aucune
@@ -159,7 +181,8 @@ public class CRCore {
plugin.getLogger().info("CR-Core activé"
+ " (teams=" + config.isTeamsEnabled()
+ ", players=" + config.isPlayersEnabled()
+ ", placeholders=" + config.isPlaceholdersEnabled() + ").");
+ ", placeholders=" + config.isPlaceholdersEnabled()
+ ", moderation=" + config.isModerationEnabled() + ").");
enabled = true;
return this;
}
@@ -224,6 +247,25 @@ public class CRCore {
return new YamlTeamConfigService(plugin, repository);
}
/** Construit le {@link ModerationService}. Override pour une impl custom. */
protected ModerationService buildModerationService(ModerationRepository repository,
ModeratorToolRegistry toolRegistry) {
return new BukkitEventFiringModerationServiceImpl(plugin, repository, toolRegistry);
}
/**
* Enregistre les outils de modération par défaut. Override pour
* remplacer / compléter le set CR-Core fourni.
*/
protected void registerDefaultModeratorTools(ModeratorToolRegistry registry,
ModerationService moderation) {
registry.register(new TeleportRandomPlayerTool());
registry.register(new InventorySpyTool());
registry.register(new FreezeTool(moderation));
registry.register(new VanishToggleTool(moderation));
registry.register(new ExitTool(moderation));
}
/** Construit le {@link CoreCommand} avec les services des features activées. */
protected CoreCommand buildCoreCommand() {
return new CoreCommand(
@@ -231,7 +273,8 @@ public class CRCore {
config.isPlayersEnabled() ? playerProfileService : null,
messages,
broadcasts,
config.isTeamsEnabled() ? teamConfig : null);
config.isTeamsEnabled() ? teamConfig : null,
config.isModerationEnabled() ? moderationService : null);
}
// ---- Command registration (plugin.yml ou CommandMap dynamique) ----
@@ -324,6 +367,15 @@ public class CRCore {
return playerProfileService;
}
public ModerationService getModerationService() {
requireModerationEnabled();
return moderationService;
}
public ModerationService moderation() {
return getModerationService();
}
public CoreCommand getCoreCommand() { return coreCommand; }
public boolean isEnabled() { return enabled; }
@@ -342,4 +394,11 @@ public class CRCore {
"Players feature is not enabled — appelez CRCoreConfig.setupPlayers() avant enable().");
}
}
private void requireModerationEnabled() {
if (!config.isModerationEnabled()) {
throw new IllegalStateException(
"Moderation feature is not enabled — appelez CRCoreConfig.setupModeration() avant enable().");
}
}
}
+12 -1
View File
@@ -54,6 +54,7 @@ public class CRCoreConfig {
private boolean teamsEnabled = false;
private boolean playersEnabled = false;
private boolean placeholdersEnabled = false;
private boolean moderationEnabled = false;
// ---- Util / infra setters ----
@@ -115,9 +116,18 @@ public class CRCoreConfig {
return this;
}
/**
* Active la feature <b>moderation</b> : service mod mode (snapshot +
* vanish + freeze + hotbar d'outils), commande {@code /core admin}.
*/
public CRCoreConfig setupModeration() {
this.moderationEnabled = true;
return this;
}
/** Active toutes les features en une fois. */
public CRCoreConfig setupAll() {
return setupTeams().setupPlayers().setupPlaceholders();
return setupTeams().setupPlayers().setupPlaceholders().setupModeration();
}
// ---- Getters ----
@@ -129,4 +139,5 @@ public class CRCoreConfig {
public boolean isTeamsEnabled() { return teamsEnabled; }
public boolean isPlayersEnabled() { return playersEnabled; }
public boolean isPlaceholdersEnabled() { return placeholdersEnabled; }
public boolean isModerationEnabled() { return moderationEnabled; }
}
@@ -1,5 +1,7 @@
package fr.luc.crcore.builtin;
import fr.luc.crcore.features.moderation.ModerationService;
import fr.luc.crcore.features.moderation.command.AdminToggleSubCommand;
import fr.luc.crcore.features.player.PlayerProfileService;
import fr.luc.crcore.features.team.TeamService;
import fr.luc.crcore.features.team.command.TeamGroupSubCommand;
@@ -15,13 +17,10 @@ import java.util.Objects;
/**
* Commande racine {@code /core}. Container des features actives.
*
* <p>Les services {@code teamService}, {@code playerProfileService} et
* {@code teamConfig} peuvent être {@code null} si la feature correspondante
* n'a pas été activée via {@link fr.luc.crcore.CRCoreConfig#setupTeams()}
* etc. — la sous-commande associée n'est alors simplement pas enregistrée.
*
* <p>{@code /core reload} et le rendu des messages communs (no-permission,
* etc.) restent toujours disponibles car ils ne dépendent que d'util.
* <p>Les services des features peuvent être {@code null} si la feature
* correspondante n'a pas été activée via les
* {@link fr.luc.crcore.CRCoreConfig#setupTeams()} etc. — la sous-commande
* associée n'est alors simplement pas enregistrée.
*/
public class CoreCommand extends BaseCommand {
@@ -30,19 +29,21 @@ public class CoreCommand extends BaseCommand {
protected final MessagesService messages;
protected final BroadcastService broadcasts;
protected final TeamConfigService teamConfig;
protected final ModerationService moderation;
public CoreCommand(TeamService teamService,
PlayerProfileService playerProfileService,
MessagesService messages,
BroadcastService broadcasts,
TeamConfigService teamConfig) {
TeamConfigService teamConfig,
ModerationService moderation) {
super("core");
// Les services de features peuvent être null (feature off).
this.teamService = teamService;
this.playerProfileService = playerProfileService;
this.messages = Objects.requireNonNull(messages, "messages");
this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts");
this.teamConfig = teamConfig;
this.moderation = moderation;
description("Commandes du noyau CR-Core");
registerDefaults();
}
@@ -52,13 +53,12 @@ public class CoreCommand extends BaseCommand {
if (teamService != null && teamConfig != null) {
addSubCommand(new TeamGroupSubCommand(teamService, messages, teamConfig));
}
if (moderation != null) {
addSubCommand(new AdminToggleSubCommand(moderation, messages));
}
addSubCommand(new CoreReloadSubCommand(messages, broadcasts, teamConfig));
}
/**
* Override de {@link BaseCommand#handleResult} pour utiliser
* {@link MessagesService} sur les cas génériques.
*/
@Override
protected void handleResult(CommandSender sender, CommandResult result) {
switch (result.getType()) {
@@ -0,0 +1,28 @@
package fr.luc.crcore.features.moderation;
import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
/**
* Repository des sessions de modération actives. Un mod actif a une
* entrée ici ; en sortir → entry supprimée.
*
* <p><b>Skeleton</b> : seule l'impl in-memory est fournie pour
* l'instant. Une impl SQLite (table {@code crcore_moderation_states})
* suivra pour persister les snapshots à travers les restarts serveur.
*/
public interface ModerationRepository {
Optional<ModerationState> findByPlayer(UUID playerId);
boolean exists(UUID playerId);
/** Crée ou remplace l'entrée pour ce joueur. */
void save(ModerationState state);
/** @return true si une entrée a été supprimée. */
boolean delete(UUID playerId);
Collection<ModerationState> findAll();
}
@@ -0,0 +1,97 @@
package fr.luc.crcore.features.moderation;
import org.bukkit.entity.Player;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
/**
* Service de modération CR-Core. Orchestre l'entrée / sortie du mod mode,
* le vanish et le freeze.
*
* <h2>Mod mode</h2>
*
* <ol>
* <li>{@link #enter(Player)} : snapshot complet du joueur
* ({@link ModerationState}), vidage de l'inventaire, dotation de
* la hotbar avec les outils du
* {@link fr.luc.crcore.features.moderation.ModeratorToolRegistry},
* passage en SPECTATOR ou CREATIVE + vanish.</li>
* <li>Pendant le mod mode, les outils sont actifs ; les actions du
* joueur (drop item, déplacement d'item) sont bloquées par
* {@code ModerationListener}.</li>
* <li>{@link #exit(Player)} : restauration intégrale du snapshot,
* retrait du vanish, retrait des outils.</li>
* </ol>
*
* <h2>Vanish</h2>
*
* <p>{@link #vanish(Player)} cache le joueur de tous les autres joueurs
* via {@link Player#hidePlayer(org.bukkit.plugin.Plugin, Player)}.
* Automatique à l'enter, retiré à l'exit. Peut être toggle pendant le
* mod mode via le {@link fr.luc.crcore.features.moderation.tool.VanishToggleTool}.
*
* <h2>Freeze</h2>
*
* <p>{@link #freeze(UUID)} marque un joueur comme gelé — son
* {@code PlayerMoveEvent} est cancel par {@code ModerationListener}.
* Stockage en mémoire (set d'UUIDs).
*/
public interface ModerationService {
// ---- Mod mode lifecycle ----
/**
* Bascule {@code player} en mod mode. Snapshot + équipement
* d'outils + vanish.
*
* @throws fr.luc.crcore.features.moderation.exception.ModerationAlreadyActiveException
* si le joueur est déjà en mod mode.
*/
void enter(Player player);
/**
* Fait sortir {@code player} du mod mode et le restaure intégralement.
*
* @throws fr.luc.crcore.features.moderation.exception.ModerationNotActiveException
* si le joueur n'est pas en mod mode.
*/
void exit(Player player);
boolean isInModeration(UUID playerId);
Optional<ModerationState> getState(UUID playerId);
Set<UUID> getActiveModerators();
// ---- Vanish ----
/** Cache le joueur de tous les autres joueurs en ligne. */
void vanish(Player player);
void unvanish(Player player);
boolean isVanished(UUID playerId);
Set<UUID> getVanishedPlayers();
// ---- Freeze ----
/** Bloque {@code playerId} sur sa position courante (PlayerMoveEvent canceled). */
void freeze(UUID playerId);
void unfreeze(UUID playerId);
boolean isFrozen(UUID playerId);
Set<UUID> getFrozenPlayers();
// ---- Tools ----
/**
* Registry des outils dotés dans la hotbar à l'enter. Un game plugin
* peut y ajouter ses outils custom avant {@link #enter(Player)}.
*/
ModeratorToolRegistry getToolRegistry();
}
@@ -0,0 +1,122 @@
package fr.luc.crcore.features.moderation;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.time.Instant;
import java.util.Objects;
import java.util.UUID;
/**
* Snapshot complet de l'état d'un joueur avant qu'il n'entre en mode
* modération. Restauré tel quel quand il en sort.
*
* <p>Capture les éléments classiques : inventaire (main + armor + offhand),
* XP, vie, faim, gamemode, location, vol. Toute extension (effets de
* potion, statuts custom) se fait en ajoutant un champ ici et en
* traitant le snapshot/restore dans {@link ModerationService}.
*
* <p>Immutable. Construit via le constructeur depuis un {@link Player}.
*
* <p><b>Skeleton</b> : pour l'instant ce snapshot est en mémoire
* uniquement. Une persistance SQLite est prévue (table
* {@code crcore_moderation_states}) pour survivre à un crash serveur
* pendant qu'un modérateur est en mod mode.
*/
public final class ModerationState {
private final UUID playerId;
private final Instant enteredAt;
private final ItemStack[] inventoryContents;
private final ItemStack[] armorContents;
private final ItemStack offhandItem;
private final int xpLevel;
private final float xpProgress;
private final double health;
private final int foodLevel;
private final float saturation;
private final Location location;
private final GameMode gameMode;
private final boolean allowFlight;
private final boolean flying;
private final float walkSpeed;
private final float flySpeed;
/** Construit le snapshot depuis l'état courant du joueur. */
public ModerationState(Player player) {
Objects.requireNonNull(player, "player");
this.playerId = player.getUniqueId();
this.enteredAt = Instant.now();
// Clone des items pour ne pas être affecté par les modifs ultérieures.
this.inventoryContents = cloneItems(player.getInventory().getContents());
this.armorContents = cloneItems(player.getInventory().getArmorContents());
ItemStack off = player.getInventory().getItemInOffHand();
this.offhandItem = (off != null) ? off.clone() : null;
this.xpLevel = player.getLevel();
this.xpProgress = player.getExp();
this.health = player.getHealth();
this.foodLevel = player.getFoodLevel();
this.saturation = player.getSaturation();
this.location = player.getLocation().clone();
this.gameMode = player.getGameMode();
this.allowFlight = player.getAllowFlight();
this.flying = player.isFlying();
this.walkSpeed = player.getWalkSpeed();
this.flySpeed = player.getFlySpeed();
}
private static ItemStack[] cloneItems(ItemStack[] src) {
if (src == null) return new ItemStack[0];
ItemStack[] copy = new ItemStack[src.length];
for (int i = 0; i < src.length; i++) {
copy[i] = (src[i] != null) ? src[i].clone() : null;
}
return copy;
}
/**
* Restaure le joueur dans son état initial. Le joueur doit toujours
* être en ligne. Suppose qu'on l'a déjà fait sortir du vanish, etc. —
* cette méthode ne gère que les attributs du snapshot.
*/
public void restoreTo(Player player) {
Objects.requireNonNull(player, "player");
// Téléport AVANT toute autre restauration (la TP modifie le state).
if (location != null) player.teleport(location);
player.getInventory().setContents(cloneItems(inventoryContents));
player.getInventory().setArmorContents(cloneItems(armorContents));
player.getInventory().setItemInOffHand(offhandItem != null ? offhandItem.clone() : null);
player.setLevel(xpLevel);
player.setExp(xpProgress);
player.setHealth(Math.min(health, player.getMaxHealth()));
player.setFoodLevel(foodLevel);
player.setSaturation(saturation);
player.setGameMode(gameMode);
player.setAllowFlight(allowFlight);
player.setFlying(flying && allowFlight);
player.setWalkSpeed(walkSpeed);
player.setFlySpeed(flySpeed);
player.updateInventory();
}
// ---- Getters ----
public UUID getPlayerId() { return playerId; }
public Instant getEnteredAt() { return enteredAt; }
public ItemStack[] getInventoryContents() { return cloneItems(inventoryContents); }
public ItemStack[] getArmorContents() { return cloneItems(armorContents); }
public ItemStack getOffhandItem() { return offhandItem != null ? offhandItem.clone() : null; }
public int getXpLevel() { return xpLevel; }
public float getXpProgress() { return xpProgress; }
public double getHealth() { return health; }
public int getFoodLevel() { return foodLevel; }
public float getSaturation() { return saturation; }
public Location getLocation() { return location.clone(); }
public GameMode getGameMode() { return gameMode; }
public boolean isAllowFlight() { return allowFlight; }
public boolean isFlying() { return flying; }
public float getWalkSpeed() { return walkSpeed; }
public float getFlySpeed() { return flySpeed; }
}
@@ -0,0 +1,52 @@
package fr.luc.crcore.features.moderation;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
/**
* Outil de la hotbar d'un modérateur (skeleton).
*
* <p>Chaque outil :
* <ul>
* <li>a une clé unique ({@link #getKey()}) — utile pour identification,</li>
* <li>occupe un slot fixe ({@link #getSlot()}, 0..8) sur la hotbar,</li>
* <li>fournit un icône {@link ItemStack} affiché dans ce slot,</li>
* <li>répond à un click gauche, un click droit (en l'air ou sur un
* bloc), et à une interaction avec une entité.</li>
* </ul>
*
* <p>Le routing est fait par {@code ModerationListener} sur les events
* Bukkit {@code PlayerInteractEvent} et {@code PlayerInteractEntityEvent}.
*
* <p><b>Skeleton</b> : seul un set minimal d'outils est livré. Pour ajouter
* un outil custom, implémenter cette interface puis l'enregistrer via
* {@link ModeratorToolRegistry#register(ModeratorTool)} avant l'appel à
* {@link ModerationService#enter(Player)} (typiquement dans {@code onEnable()}
* du game plugin).
*/
public interface ModeratorTool {
/** Clé unique de l'outil (ex. {@code "exit"}, {@code "vanish"}, {@code "freeze"}). */
String getKey();
/** Slot fixe sur la hotbar (0..8). Les conflits sont gérés par {@link ModeratorToolRegistry}. */
int getSlot();
/** Construit l'icône à afficher (nouvelle instance à chaque appel). */
ItemStack buildIcon();
// ---- Hooks (default no-op) ----
/** Appelé sur clic gauche en l'air ou sur un bloc. */
default void onLeftClick(Player moderator) {
}
/** Appelé sur clic droit en l'air ou sur un bloc. */
default void onRightClick(Player moderator) {
}
/** Appelé sur clic droit sur une entité (typiquement un joueur). */
default void onInteractEntity(Player moderator, Entity target) {
}
}
@@ -0,0 +1,71 @@
package fr.luc.crcore.features.moderation;
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 ModeratorTool} disponibles dans la hotbar de
* modération. Préserve l'ordre d'enregistrement.
*
* <p>Un game plugin peut enregistrer ses propres outils avant l'appel à
* {@code CRCore.enable()} :
* <pre>{@code
* core.getModerationService().getToolRegistry()
* .register(new MyCustomTool());
* }</pre>
*
* <p>Skeleton CR-Core fournit {@code ExitTool}, {@code VanishToggleTool},
* {@code FreezeTool}, {@code InventorySpyTool}, {@code TeleportRandomPlayerTool}.
*/
public class ModeratorToolRegistry {
/** Indexé par slot pour conflict detection. */
private final Map<Integer, ModeratorTool> bySlot = new LinkedHashMap<>();
/** Indexé par clé pour lookup. */
private final Map<String, ModeratorTool> byKey = new LinkedHashMap<>();
/**
* Enregistre un outil. Si le slot est déjà occupé, l'ancien est
* remplacé (utile pour customiser le set par défaut).
*/
public synchronized void register(ModeratorTool tool) {
Objects.requireNonNull(tool, "tool");
int slot = tool.getSlot();
if (slot < 0 || slot > 8) {
throw new IllegalArgumentException(
"Slot must be in 0..8, got " + slot + " for tool " + tool.getKey());
}
// Supprime l'ancien occupant du même slot (s'il existe).
ModeratorTool previous = bySlot.put(slot, tool);
if (previous != null) {
byKey.remove(previous.getKey());
}
byKey.put(tool.getKey(), tool);
}
public synchronized boolean unregister(String key) {
ModeratorTool tool = byKey.remove(key);
if (tool != null) {
bySlot.remove(tool.getSlot());
return true;
}
return false;
}
public Optional<ModeratorTool> get(String key) {
return Optional.ofNullable(byKey.get(key));
}
public Optional<ModeratorTool> getBySlot(int slot) {
return Optional.ofNullable(bySlot.get(slot));
}
/** Tous les outils enregistrés, ordre d'insertion. */
public Collection<ModeratorTool> all() {
return Collections.unmodifiableCollection(byKey.values());
}
}
@@ -0,0 +1,45 @@
package fr.luc.crcore.features.moderation.command;
import fr.luc.crcore.features.moderation.ModerationService;
import fr.luc.crcore.util.command.CommandContext;
import fr.luc.crcore.util.command.CommandResult;
import fr.luc.crcore.util.command.SubCommand;
import fr.luc.crcore.util.message.MessagesService;
import org.bukkit.entity.Player;
import java.util.Objects;
/**
* {@code /core admin} — toggle on/off du mode modération pour le joueur
* exécutant.
*
* <p>Permission : {@code crcore.admin}. Player-only.
*
* <p>Squelette : un simple toggle. Pas d'arg pour basculer un autre
* joueur (à ajouter plus tard, ex. {@code /core admin <player>}).
*/
public class AdminToggleSubCommand extends SubCommand {
protected final ModerationService moderation;
protected final MessagesService messages;
public AdminToggleSubCommand(ModerationService moderation, MessagesService messages) {
super("admin");
this.moderation = Objects.requireNonNull(moderation, "moderation");
this.messages = Objects.requireNonNull(messages, "messages");
description("Basculer en/sortir du mode modération");
permission("crcore.admin");
playerOnly();
}
@Override
public CommandResult execute(CommandContext ctx) {
Player player = ctx.requirePlayer();
if (moderation.isInModeration(player.getUniqueId())) {
moderation.exit(player);
return CommandResult.success(messages.get("moderation.exit.success"));
}
moderation.enter(player);
return CommandResult.success(messages.get("moderation.enter.success"));
}
}
@@ -0,0 +1,27 @@
package fr.luc.crcore.features.moderation.event;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
/**
* Tiré APRÈS l'entrée en mod mode (snapshot enregistré, hotbar dotée, vanish actif).
* Le game plugin peut s'y brancher pour des hooks custom (ex. annonce
* staff, log audit).
*/
public class ModerationEnterEvent extends ModerationEvent {
private static final HandlerList HANDLERS = new HandlerList();
public ModerationEnterEvent(Player moderator) {
super(moderator);
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -0,0 +1,20 @@
package fr.luc.crcore.features.moderation.event;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import java.util.Objects;
/** Base des events Bukkit du module modération. */
public abstract class ModerationEvent extends Event {
private final Player moderator;
protected ModerationEvent(Player moderator) {
this.moderator = Objects.requireNonNull(moderator, "moderator");
}
public Player getModerator() {
return moderator;
}
}
@@ -0,0 +1,26 @@
package fr.luc.crcore.features.moderation.event;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
/**
* Tiré APRÈS la sortie du mod mode (état restauré, vanish désactivé). Le
* game plugin peut s'y brancher (annonce, log).
*/
public class ModerationExitEvent extends ModerationEvent {
private static final HandlerList HANDLERS = new HandlerList();
public ModerationExitEvent(Player moderator) {
super(moderator);
}
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -0,0 +1,9 @@
package fr.luc.crcore.features.moderation.exception;
/** Lancée si on tente d'enter alors que le joueur est déjà en mod mode. */
public class ModerationAlreadyActiveException extends ModerationException {
public ModerationAlreadyActiveException(String message) {
super(message);
}
}
@@ -0,0 +1,9 @@
package fr.luc.crcore.features.moderation.exception;
/** Base des exceptions du module modération. */
public class ModerationException extends RuntimeException {
public ModerationException(String message) {
super(message);
}
}
@@ -0,0 +1,9 @@
package fr.luc.crcore.features.moderation.exception;
/** Lancée si on tente d'exit alors que le joueur n'est pas en mod mode. */
public class ModerationNotActiveException extends ModerationException {
public ModerationNotActiveException(String message) {
super(message);
}
}
@@ -0,0 +1,32 @@
package fr.luc.crcore.features.moderation.impl;
import fr.luc.crcore.features.moderation.ModerationRepository;
import fr.luc.crcore.features.moderation.ModeratorToolRegistry;
import fr.luc.crcore.features.moderation.event.ModerationEnterEvent;
import fr.luc.crcore.features.moderation.event.ModerationExitEvent;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
/**
* Variante par défaut : tire les events Bukkit
* {@link ModerationEnterEvent} et {@link ModerationExitEvent} aux moments
* appropriés (après que l'état effectif a été mis à jour).
*/
public class BukkitEventFiringModerationServiceImpl extends ModerationServiceImpl {
public BukkitEventFiringModerationServiceImpl(Plugin plugin,
ModerationRepository repository,
ModeratorToolRegistry toolRegistry) {
super(plugin, repository, toolRegistry);
}
@Override
protected void onAfterEnter(Player player) {
plugin.getServer().getPluginManager().callEvent(new ModerationEnterEvent(player));
}
@Override
protected void onAfterExit(Player player) {
plugin.getServer().getPluginManager().callEvent(new ModerationExitEvent(player));
}
}
@@ -0,0 +1,49 @@
package fr.luc.crcore.features.moderation.impl;
import fr.luc.crcore.features.moderation.ModerationRepository;
import fr.luc.crcore.features.moderation.ModerationState;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
/**
* Impl en mémoire — perd les sessions au reload/stop. Skeleton.
*
* <p>Une impl SQLite (avec sérialisation Bukkit des ItemStacks via
* {@code BukkitObjectOutputStream}) suivra pour persister les snapshots.
*/
public class InMemoryModerationRepository implements ModerationRepository {
private final Map<UUID, ModerationState> states = new HashMap<>();
@Override
public Optional<ModerationState> findByPlayer(UUID playerId) {
return Optional.ofNullable(states.get(playerId));
}
@Override
public boolean exists(UUID playerId) {
return states.containsKey(playerId);
}
@Override
public void save(ModerationState state) {
Objects.requireNonNull(state, "state");
states.put(state.getPlayerId(), state);
}
@Override
public boolean delete(UUID playerId) {
return states.remove(playerId) != null;
}
@Override
public Collection<ModerationState> findAll() {
return Collections.unmodifiableCollection(states.values());
}
}
@@ -0,0 +1,147 @@
package fr.luc.crcore.features.moderation.impl;
import fr.luc.crcore.features.moderation.ModerationService;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Objects;
import java.util.UUID;
/**
* Routing des interactions Bukkit pour le module modération.
*
* <ul>
* <li>Clic gauche / droit (bloc ou air) → {@code tool.onLeftClick / onRightClick}.</li>
* <li>Clic sur entité → {@code tool.onInteractEntity}.</li>
* <li>Hotbar des modérateurs verrouillée (pas de drop, pas de swap, pas
* de déplacement d'item via inventory click sur leur propre inv).</li>
* <li>Vanish appliqué automatiquement aux joueurs qui join (re-hide).</li>
* <li>Cleanup automatique des state freeze/vanish sur quit.</li>
* <li>Mouvement bloqué pour les joueurs gelés.</li>
* </ul>
*
* <p>Enregistré une fois par {@code CRCore.enable()} si la feature
* modération est active.
*/
public class ModerationListener implements Listener {
private final JavaPlugin plugin;
private final ModerationService moderation;
public ModerationListener(JavaPlugin plugin, ModerationService moderation) {
this.plugin = Objects.requireNonNull(plugin, "plugin");
this.moderation = Objects.requireNonNull(moderation, "moderation");
}
public void registerOn(JavaPlugin plugin) {
Objects.requireNonNull(plugin, "plugin").getServer()
.getPluginManager().registerEvents(this, plugin);
}
// ---- Outils : clic gauche / droit / sur entité ----
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false)
public void onInteract(PlayerInteractEvent event) {
Player player = event.getPlayer();
if (!moderation.isInModeration(player.getUniqueId())) return;
int slot = player.getInventory().getHeldItemSlot();
moderation.getToolRegistry().getBySlot(slot).ifPresent(tool -> {
Action a = event.getAction();
if (a == Action.LEFT_CLICK_AIR || a == Action.LEFT_CLICK_BLOCK) {
tool.onLeftClick(player);
} else if (a == Action.RIGHT_CLICK_AIR || a == Action.RIGHT_CLICK_BLOCK) {
tool.onRightClick(player);
}
// Annule pour éviter toute interaction réelle avec le monde
// (placer/casser un bloc, utiliser un compass, etc.).
event.setCancelled(true);
});
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = false)
public void onInteractEntity(PlayerInteractEntityEvent event) {
Player player = event.getPlayer();
if (!moderation.isInModeration(player.getUniqueId())) return;
int slot = player.getInventory().getHeldItemSlot();
moderation.getToolRegistry().getBySlot(slot).ifPresent(tool -> {
tool.onInteractEntity(player, event.getRightClicked());
event.setCancelled(true);
});
}
// ---- Hotbar verrouillée ----
@EventHandler
public void onDrop(PlayerDropItemEvent event) {
if (moderation.isInModeration(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
}
}
@EventHandler
public void onSwap(PlayerSwapHandItemsEvent event) {
if (moderation.isInModeration(event.getPlayer().getUniqueId())) {
event.setCancelled(true);
}
}
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player)) return;
Player player = (Player) event.getWhoClicked();
if (!moderation.isInModeration(player.getUniqueId())) return;
// Si le top inventory est l'inventaire du modérateur lui-même
// (cas "ouvre son inv via E"), on bloque toute manipulation. Sinon
// (ex. InventorySpy ouvre l'inv d'une cible), on laisse passer.
if (event.getView().getTopInventory().getHolder() == player) {
event.setCancelled(true);
}
}
// ---- Vanish ----
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player joiner = event.getPlayer();
for (UUID vanishedId : moderation.getVanishedPlayers()) {
if (vanishedId.equals(joiner.getUniqueId())) continue;
Player vp = plugin.getServer().getPlayer(vanishedId);
if (vp != null) {
joiner.hidePlayer(plugin, vp);
}
}
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
UUID id = event.getPlayer().getUniqueId();
// Retire le freeze (skeleton — l'état de mod mode est conservé, le
// joueur le retrouvera à sa reconnexion si nécessaire ; à raffiner
// selon les besoins).
if (moderation.isFrozen(id)) moderation.unfreeze(id);
}
// ---- Freeze ----
@EventHandler
public void onMove(PlayerMoveEvent event) {
if (!moderation.isFrozen(event.getPlayer().getUniqueId())) return;
if (event.getFrom().getBlockX() != event.getTo().getBlockX()
|| event.getFrom().getBlockY() != event.getTo().getBlockY()
|| event.getFrom().getBlockZ() != event.getTo().getBlockZ()) {
event.setTo(event.getFrom());
}
}
}
@@ -0,0 +1,189 @@
package fr.luc.crcore.features.moderation.impl;
import fr.luc.crcore.features.moderation.ModerationRepository;
import fr.luc.crcore.features.moderation.ModerationService;
import fr.luc.crcore.features.moderation.ModerationState;
import fr.luc.crcore.features.moderation.ModeratorTool;
import fr.luc.crcore.features.moderation.ModeratorToolRegistry;
import fr.luc.crcore.features.moderation.exception.ModerationAlreadyActiveException;
import fr.luc.crcore.features.moderation.exception.ModerationNotActiveException;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
/**
* Impl par défaut. Hooks {@link #onAfterEnter}/{@link #onAfterExit} pour
* les sous-classes (typiquement la {@code BukkitEventFiring*} qui tire
* les events).
*/
public class ModerationServiceImpl implements ModerationService {
protected final Plugin plugin;
protected final ModerationRepository repository;
protected final ModeratorToolRegistry toolRegistry;
protected final Set<UUID> vanished = new HashSet<>();
protected final Set<UUID> frozen = new HashSet<>();
public ModerationServiceImpl(Plugin plugin,
ModerationRepository repository,
ModeratorToolRegistry toolRegistry) {
this.plugin = Objects.requireNonNull(plugin, "plugin");
this.repository = Objects.requireNonNull(repository, "repository");
this.toolRegistry = Objects.requireNonNull(toolRegistry, "toolRegistry");
}
// ---- Lifecycle ----
@Override
public void enter(Player player) {
Objects.requireNonNull(player, "player");
if (repository.exists(player.getUniqueId())) {
throw new ModerationAlreadyActiveException(
player.getName() + " est déjà en mode modération.");
}
// 1. Snapshot
ModerationState state = new ModerationState(player);
repository.save(state);
// 2. Vidage de l'inventaire + dotation des outils
player.getInventory().clear();
for (ModeratorTool tool : toolRegistry.all()) {
player.getInventory().setItem(tool.getSlot(), tool.buildIcon());
}
player.getInventory().setHeldItemSlot(0);
// 3. Game mode + flight (par défaut CREATIVE — visibilité totale + mobility)
player.setGameMode(GameMode.CREATIVE);
player.setAllowFlight(true);
// Restaure XP visible neutre
player.setLevel(0);
player.setExp(0f);
// 4. Vanish
vanish(player);
// 5. Hook (event-firing)
onAfterEnter(player);
}
@Override
public void exit(Player player) {
Objects.requireNonNull(player, "player");
Optional<ModerationState> stateOpt = repository.findByPlayer(player.getUniqueId());
if (stateOpt.isEmpty()) {
throw new ModerationNotActiveException(
player.getName() + " n'est pas en mode modération.");
}
// 1. Restore (téléport + inv + xp + gamemode + flight + walk/fly speed)
stateOpt.get().restoreTo(player);
// 2. Sortir du vanish
unvanish(player);
// 3. Effacer aussi un éventuel freeze (cohérence)
unfreeze(player.getUniqueId());
// 4. Supprimer l'entrée
repository.delete(player.getUniqueId());
// 5. Hook
onAfterExit(player);
}
@Override
public boolean isInModeration(UUID playerId) {
return repository.exists(playerId);
}
@Override
public Optional<ModerationState> getState(UUID playerId) {
return repository.findByPlayer(playerId);
}
@Override
public Set<UUID> getActiveModerators() {
Set<UUID> ids = new HashSet<>();
for (ModerationState s : repository.findAll()) {
ids.add(s.getPlayerId());
}
return Collections.unmodifiableSet(ids);
}
// ---- Vanish ----
@Override
public void vanish(Player player) {
Objects.requireNonNull(player, "player");
vanished.add(player.getUniqueId());
for (Player other : Bukkit.getOnlinePlayers()) {
if (other.getUniqueId().equals(player.getUniqueId())) continue;
other.hidePlayer(plugin, player);
}
}
@Override
public void unvanish(Player player) {
Objects.requireNonNull(player, "player");
vanished.remove(player.getUniqueId());
for (Player other : Bukkit.getOnlinePlayers()) {
if (other.getUniqueId().equals(player.getUniqueId())) continue;
other.showPlayer(plugin, player);
}
}
@Override
public boolean isVanished(UUID playerId) {
return vanished.contains(playerId);
}
@Override
public Set<UUID> getVanishedPlayers() {
return Collections.unmodifiableSet(vanished);
}
// ---- Freeze ----
@Override
public void freeze(UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
frozen.add(playerId);
}
@Override
public void unfreeze(UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
frozen.remove(playerId);
}
@Override
public boolean isFrozen(UUID playerId) {
return frozen.contains(playerId);
}
@Override
public Set<UUID> getFrozenPlayers() {
return Collections.unmodifiableSet(frozen);
}
@Override
public ModeratorToolRegistry getToolRegistry() {
return toolRegistry;
}
// ---- Hooks pour les sous-classes ----
protected void onAfterEnter(Player player) {
}
protected void onAfterExit(Player player) {
}
}
@@ -0,0 +1,50 @@
package fr.luc.crcore.features.moderation.tool;
import fr.luc.crcore.features.moderation.ModerationService;
import fr.luc.crcore.features.moderation.ModeratorTool;
import fr.luc.crcore.util.gui.GuiItems;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Objects;
/**
* {@code Exit} — slot 8 (extrémité droite de la hotbar). Click → exit
* mod mode (restaure le snapshot).
*/
public class ExitTool implements ModeratorTool {
public static final String KEY = "exit";
public static final int SLOT = 8;
private final ModerationService moderation;
public ExitTool(ModerationService moderation) {
this.moderation = Objects.requireNonNull(moderation, "moderation");
}
@Override
public String getKey() { return KEY; }
@Override
public int getSlot() { return SLOT; }
@Override
public ItemStack buildIcon() {
return GuiItems.named(Material.BARRIER, "&cQuitter le mode modération")
.lore("&7Restaure ton inventaire, ta XP",
"&7et ta location d'origine.")
.build();
}
@Override
public void onLeftClick(Player moderator) {
moderation.exit(moderator);
}
@Override
public void onRightClick(Player moderator) {
moderation.exit(moderator);
}
}
@@ -0,0 +1,55 @@
package fr.luc.crcore.features.moderation.tool;
import fr.luc.crcore.features.moderation.ModerationService;
import fr.luc.crcore.features.moderation.ModeratorTool;
import fr.luc.crcore.util.gui.GuiItems;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Objects;
/**
* {@code Freeze} — slot 2. Click droit sur un joueur → toggle freeze.
*/
public class FreezeTool implements ModeratorTool {
public static final String KEY = "freeze";
public static final int SLOT = 2;
private final ModerationService moderation;
public FreezeTool(ModerationService moderation) {
this.moderation = Objects.requireNonNull(moderation, "moderation");
}
@Override
public String getKey() { return KEY; }
@Override
public int getSlot() { return SLOT; }
@Override
public ItemStack buildIcon() {
return GuiItems.named(Material.ICE, "&bFreeze")
.lore("&7Clic droit sur un joueur :",
"&7toggle freeze on/off.")
.build();
}
@Override
public void onInteractEntity(Player moderator, Entity target) {
if (!(target instanceof Player)) return;
Player victim = (Player) target;
if (moderation.isFrozen(victim.getUniqueId())) {
moderation.unfreeze(victim.getUniqueId());
moderator.sendMessage("§e" + victim.getName() + " §rdégelé.");
victim.sendMessage("§eTu es dégelé.");
} else {
moderation.freeze(victim.getUniqueId());
moderator.sendMessage("§b" + victim.getName() + " §rgelé.");
victim.sendMessage("§bTu as été gelé par la modération.");
}
}
}
@@ -0,0 +1,41 @@
package fr.luc.crcore.features.moderation.tool;
import fr.luc.crcore.features.moderation.ModeratorTool;
import fr.luc.crcore.util.gui.GuiItems;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
/**
* {@code Inventory spy} — slot 1. Click droit sur un joueur → ouvre son
* inventaire pour inspection (skeleton — pour le moment lecture/écriture
* sur l'inventaire réel ; à wrappe-r en read-only plus tard).
*/
public class InventorySpyTool implements ModeratorTool {
public static final String KEY = "inventory_spy";
public static final int SLOT = 1;
@Override
public String getKey() { return KEY; }
@Override
public int getSlot() { return SLOT; }
@Override
public ItemStack buildIcon() {
return GuiItems.named(Material.CHEST, "&eInspecter l'inventaire")
.lore("&7Clic droit sur un joueur →",
"&7ouvre son inventaire.",
"&8(skeleton : lecture/écriture)")
.build();
}
@Override
public void onInteractEntity(Player moderator, Entity target) {
if (!(target instanceof Player)) return;
Player victim = (Player) target;
moderator.openInventory(victim.getInventory());
}
}
@@ -0,0 +1,54 @@
package fr.luc.crcore.features.moderation.tool;
import fr.luc.crcore.features.moderation.ModeratorTool;
import fr.luc.crcore.util.gui.GuiItems;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
/**
* {@code Teleport random player} — slot 0. Click droit → tp à un joueur
* aléatoire en ligne (skeleton ; à remplacer par un GUI sélecteur de
* joueurs plus tard).
*/
public class TeleportRandomPlayerTool implements ModeratorTool {
public static final String KEY = "teleport_random";
public static final int SLOT = 0;
@Override
public String getKey() { return KEY; }
@Override
public int getSlot() { return SLOT; }
@Override
public ItemStack buildIcon() {
return GuiItems.named(Material.COMPASS, "&aTéléport joueur")
.lore("&7Clic droit → téléporte à un joueur",
"&7au hasard en ligne.",
"&8(skeleton : remplacer par un GUI)")
.build();
}
@Override
public void onRightClick(Player moderator) {
List<Player> candidates = new ArrayList<>();
for (Player p : Bukkit.getOnlinePlayers()) {
if (!p.getUniqueId().equals(moderator.getUniqueId())) candidates.add(p);
}
if (candidates.isEmpty()) {
moderator.sendMessage("§7Aucun autre joueur en ligne.");
return;
}
// Pas de Math.random() pour reproductibilité — modulo de millis suffit pour un skeleton.
int idx = (int) (System.currentTimeMillis() % candidates.size());
Player target = candidates.get(idx);
moderator.teleport(target.getLocation());
moderator.sendMessage("§a→ téléporté à §f" + target.getName());
}
}
@@ -0,0 +1,60 @@
package fr.luc.crcore.features.moderation.tool;
import fr.luc.crcore.features.moderation.ModerationService;
import fr.luc.crcore.features.moderation.ModeratorTool;
import fr.luc.crcore.util.gui.GuiItems;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.Objects;
/**
* {@code Vanish toggle} — slot 7. Click → toggle vanish on/off (le
* modérateur reste en mod mode, mais visible/invisible).
*/
public class VanishToggleTool implements ModeratorTool {
public static final String KEY = "vanish_toggle";
public static final int SLOT = 7;
private final ModerationService moderation;
public VanishToggleTool(ModerationService moderation) {
this.moderation = Objects.requireNonNull(moderation, "moderation");
}
@Override
public String getKey() { return KEY; }
@Override
public int getSlot() { return SLOT; }
@Override
public ItemStack buildIcon() {
return GuiItems.named(Material.ENDER_EYE, "&dToggle vanish")
.lore("&7Clic gauche : devenir visible / invisible",
"&7État actuel mis à jour à chaque clic.")
.build();
}
@Override
public void onLeftClick(Player moderator) {
toggle(moderator);
}
@Override
public void onRightClick(Player moderator) {
toggle(moderator);
}
private void toggle(Player moderator) {
if (moderation.isVanished(moderator.getUniqueId())) {
moderation.unvanish(moderator);
moderator.sendMessage("§eVisible.");
} else {
moderation.vanish(moderator);
moderator.sendMessage("§7Vanish.");
}
}
}
+9
View File
@@ -128,3 +128,12 @@ team.spawn.change.broadcast: "&7[CR] &fSpawn de {color}{team_name}&f mis à jour
player.profile.create.broadcast: "&7[CR] &f{player} a maintenant un profil CR-Core."
player.profile.delete.broadcast: "&7[CR] &fProfil de {player} supprimé."
player.score.change.broadcast: "&7[CR] &f{player} : {score_name} {old_value} → &f{new_value}"
# -----------------------------------------------------------------------------
# Module modération (/core admin)
# -----------------------------------------------------------------------------
moderation:
enter:
success: "&aTu es maintenant en &emodération&a — vanish actif, outils dans la hotbar."
exit:
success: "&aTu es sorti de la modération — état restauré."