diff --git a/docs/README.md b/docs/README.md index b06c620..eef0ca7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/docs/decisions.md b/docs/decisions.md index dcb9bc1..cc1ad6e 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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 : diff --git a/docs/diagrams/moderation-class-diagram.puml b/docs/diagrams/moderation-class-diagram.puml new file mode 100644 index 0000000..25292e7 --- /dev/null +++ b/docs/diagrams/moderation-class-diagram.puml @@ -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 <> { + - 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 + + exists(uuid): boolean + + save(state): void + + delete(uuid): boolean + + findAll(): Collection + } + + interface ModeratorTool { + + getKey(): String + + getSlot(): int ' 0..8 + + buildIcon(): ItemStack + + onLeftClick(player): void + + onRightClick(player): void + + onInteractEntity(player, target): void + } + + class ModeratorToolRegistry { + - bySlot: Map + - byKey: Map + + register(tool): void + + unregister(key): boolean + + get(key): Optional + + getBySlot(slot): Optional + + all(): Collection + } + + interface ModerationService { + + enter(player): void + + exit(player): void + + isInModeration(uuid): boolean + + getState(uuid): Optional + + getActiveModerators(): Set + -- + + 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 + # frozen: Set + -- + # 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 diff --git a/docs/features.md b/docs/features.md index 6c13928..0cead2b 100644 --- a/docs/features.md +++ b/docs/features.md @@ -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 ` 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. diff --git a/docs/setup.md b/docs/setup.md index 843798b..a0de79c 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -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 diff --git a/src/main/java/fr/luc/crcore/CRCore.java b/src/main/java/fr/luc/crcore/CRCore.java index 61d6c3c..49853fa 100644 --- a/src/main/java/fr/luc/crcore/CRCore.java +++ b/src/main/java/fr/luc/crcore/CRCore.java @@ -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()."); + } + } } diff --git a/src/main/java/fr/luc/crcore/CRCoreConfig.java b/src/main/java/fr/luc/crcore/CRCoreConfig.java index 09cc978..3c92688 100644 --- a/src/main/java/fr/luc/crcore/CRCoreConfig.java +++ b/src/main/java/fr/luc/crcore/CRCoreConfig.java @@ -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 moderation : 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; } } diff --git a/src/main/java/fr/luc/crcore/builtin/CoreCommand.java b/src/main/java/fr/luc/crcore/builtin/CoreCommand.java index 0755c90..d52ddf2 100644 --- a/src/main/java/fr/luc/crcore/builtin/CoreCommand.java +++ b/src/main/java/fr/luc/crcore/builtin/CoreCommand.java @@ -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. * - *

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. - * - *

{@code /core reload} et le rendu des messages communs (no-permission, - * etc.) restent toujours disponibles car ils ne dépendent que d'util. + *

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()) { diff --git a/src/main/java/fr/luc/crcore/features/moderation/ModerationRepository.java b/src/main/java/fr/luc/crcore/features/moderation/ModerationRepository.java new file mode 100644 index 0000000..e2b5586 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/ModerationRepository.java @@ -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. + * + *

Skeleton : 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 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 findAll(); +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/ModerationService.java b/src/main/java/fr/luc/crcore/features/moderation/ModerationService.java new file mode 100644 index 0000000..8eb798a --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/ModerationService.java @@ -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. + * + *

Mod mode

+ * + *
    + *
  1. {@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.
  2. + *
  3. Pendant le mod mode, les outils sont actifs ; les actions du + * joueur (drop item, déplacement d'item) sont bloquées par + * {@code ModerationListener}.
  4. + *
  5. {@link #exit(Player)} : restauration intégrale du snapshot, + * retrait du vanish, retrait des outils.
  6. + *
+ * + *

Vanish

+ * + *

{@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}. + * + *

Freeze

+ * + *

{@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 getState(UUID playerId); + + Set 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 getVanishedPlayers(); + + // ---- Freeze ---- + + /** Bloque {@code playerId} sur sa position courante (PlayerMoveEvent canceled). */ + void freeze(UUID playerId); + + void unfreeze(UUID playerId); + + boolean isFrozen(UUID playerId); + + Set 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(); +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/ModerationState.java b/src/main/java/fr/luc/crcore/features/moderation/ModerationState.java new file mode 100644 index 0000000..0f8e63e --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/ModerationState.java @@ -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. + * + *

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}. + * + *

Immutable. Construit via le constructeur depuis un {@link Player}. + * + *

Skeleton : 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; } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/ModeratorTool.java b/src/main/java/fr/luc/crcore/features/moderation/ModeratorTool.java new file mode 100644 index 0000000..67d7dd3 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/ModeratorTool.java @@ -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). + * + *

Chaque outil : + *

    + *
  • a une clé unique ({@link #getKey()}) — utile pour identification,
  • + *
  • occupe un slot fixe ({@link #getSlot()}, 0..8) sur la hotbar,
  • + *
  • fournit un icône {@link ItemStack} affiché dans ce slot,
  • + *
  • répond à un click gauche, un click droit (en l'air ou sur un + * bloc), et à une interaction avec une entité.
  • + *
+ * + *

Le routing est fait par {@code ModerationListener} sur les events + * Bukkit {@code PlayerInteractEvent} et {@code PlayerInteractEntityEvent}. + * + *

Skeleton : 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) { + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/ModeratorToolRegistry.java b/src/main/java/fr/luc/crcore/features/moderation/ModeratorToolRegistry.java new file mode 100644 index 0000000..0126b1a --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/ModeratorToolRegistry.java @@ -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. + * + *

Un game plugin peut enregistrer ses propres outils avant l'appel à + * {@code CRCore.enable()} : + *

{@code
+ * core.getModerationService().getToolRegistry()
+ *     .register(new MyCustomTool());
+ * }
+ * + *

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 bySlot = new LinkedHashMap<>(); + /** Indexé par clé pour lookup. */ + private final Map 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 get(String key) { + return Optional.ofNullable(byKey.get(key)); + } + + public Optional getBySlot(int slot) { + return Optional.ofNullable(bySlot.get(slot)); + } + + /** Tous les outils enregistrés, ordre d'insertion. */ + public Collection all() { + return Collections.unmodifiableCollection(byKey.values()); + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/command/AdminToggleSubCommand.java b/src/main/java/fr/luc/crcore/features/moderation/command/AdminToggleSubCommand.java new file mode 100644 index 0000000..06b6419 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/command/AdminToggleSubCommand.java @@ -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. + * + *

Permission : {@code crcore.admin}. Player-only. + * + *

Squelette : un simple toggle. Pas d'arg pour basculer un autre + * joueur (à ajouter plus tard, ex. {@code /core admin }). + */ +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")); + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/event/ModerationEnterEvent.java b/src/main/java/fr/luc/crcore/features/moderation/event/ModerationEnterEvent.java new file mode 100644 index 0000000..f60f053 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/event/ModerationEnterEvent.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/event/ModerationEvent.java b/src/main/java/fr/luc/crcore/features/moderation/event/ModerationEvent.java new file mode 100644 index 0000000..eb357dd --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/event/ModerationEvent.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/event/ModerationExitEvent.java b/src/main/java/fr/luc/crcore/features/moderation/event/ModerationExitEvent.java new file mode 100644 index 0000000..d437457 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/event/ModerationExitEvent.java @@ -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; + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/exception/ModerationAlreadyActiveException.java b/src/main/java/fr/luc/crcore/features/moderation/exception/ModerationAlreadyActiveException.java new file mode 100644 index 0000000..c698135 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/exception/ModerationAlreadyActiveException.java @@ -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); + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/exception/ModerationException.java b/src/main/java/fr/luc/crcore/features/moderation/exception/ModerationException.java new file mode 100644 index 0000000..952d425 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/exception/ModerationException.java @@ -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); + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/exception/ModerationNotActiveException.java b/src/main/java/fr/luc/crcore/features/moderation/exception/ModerationNotActiveException.java new file mode 100644 index 0000000..e8aec57 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/exception/ModerationNotActiveException.java @@ -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); + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/impl/BukkitEventFiringModerationServiceImpl.java b/src/main/java/fr/luc/crcore/features/moderation/impl/BukkitEventFiringModerationServiceImpl.java new file mode 100644 index 0000000..050c52e --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/impl/BukkitEventFiringModerationServiceImpl.java @@ -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)); + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/impl/InMemoryModerationRepository.java b/src/main/java/fr/luc/crcore/features/moderation/impl/InMemoryModerationRepository.java new file mode 100644 index 0000000..e63ca21 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/impl/InMemoryModerationRepository.java @@ -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. + * + *

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 states = new HashMap<>(); + + @Override + public Optional 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 findAll() { + return Collections.unmodifiableCollection(states.values()); + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/impl/ModerationListener.java b/src/main/java/fr/luc/crcore/features/moderation/impl/ModerationListener.java new file mode 100644 index 0000000..b11d971 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/impl/ModerationListener.java @@ -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. + * + *

    + *
  • Clic gauche / droit (bloc ou air) → {@code tool.onLeftClick / onRightClick}.
  • + *
  • Clic sur entité → {@code tool.onInteractEntity}.
  • + *
  • 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).
  • + *
  • Vanish appliqué automatiquement aux joueurs qui join (re-hide).
  • + *
  • Cleanup automatique des state freeze/vanish sur quit.
  • + *
  • Mouvement bloqué pour les joueurs gelés.
  • + *
+ * + *

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()); + } + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/impl/ModerationServiceImpl.java b/src/main/java/fr/luc/crcore/features/moderation/impl/ModerationServiceImpl.java new file mode 100644 index 0000000..3b0115c --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/impl/ModerationServiceImpl.java @@ -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 vanished = new HashSet<>(); + protected final Set 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 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 getState(UUID playerId) { + return repository.findByPlayer(playerId); + } + + @Override + public Set getActiveModerators() { + Set 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 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 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) { + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/tool/ExitTool.java b/src/main/java/fr/luc/crcore/features/moderation/tool/ExitTool.java new file mode 100644 index 0000000..8482041 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/tool/ExitTool.java @@ -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); + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/tool/FreezeTool.java b/src/main/java/fr/luc/crcore/features/moderation/tool/FreezeTool.java new file mode 100644 index 0000000..9673655 --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/tool/FreezeTool.java @@ -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."); + } + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/tool/InventorySpyTool.java b/src/main/java/fr/luc/crcore/features/moderation/tool/InventorySpyTool.java new file mode 100644 index 0000000..92e936a --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/tool/InventorySpyTool.java @@ -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()); + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/tool/TeleportRandomPlayerTool.java b/src/main/java/fr/luc/crcore/features/moderation/tool/TeleportRandomPlayerTool.java new file mode 100644 index 0000000..62db58f --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/tool/TeleportRandomPlayerTool.java @@ -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 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()); + } +} diff --git a/src/main/java/fr/luc/crcore/features/moderation/tool/VanishToggleTool.java b/src/main/java/fr/luc/crcore/features/moderation/tool/VanishToggleTool.java new file mode 100644 index 0000000..c28696f --- /dev/null +++ b/src/main/java/fr/luc/crcore/features/moderation/tool/VanishToggleTool.java @@ -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."); + } + } +} diff --git a/src/main/resources/crcore-messages.yml b/src/main/resources/crcore-messages.yml index 74439f5..a4977d7 100644 --- a/src/main/resources/crcore-messages.yml +++ b/src/main/resources/crcore-messages.yml @@ -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é."