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:
+8
-2
@@ -44,8 +44,13 @@ d'initialisation côté plugin de jeu :
|
|||||||
- **Framework GUI** — `AbstractInventoryGui` + `GuiListener` réutilisable
|
- **Framework GUI** — `AbstractInventoryGui` + `GuiListener` réutilisable
|
||||||
pour tout GUI custom. Détection par holder, clic toujours annulé,
|
pour tout GUI custom. Détection par holder, clic toujours annulé,
|
||||||
`GuiItems` builder fluide avec codes couleur `&`.
|
`GuiItems` builder fluide avec codes couleur `&`.
|
||||||
- **Bootstrap unique** — `new CRCore(this).enable()` dans le `onEnable()`
|
- **Modération (skeleton)** — `/core admin` toggle pour passer en
|
||||||
du plugin de jeu, et tout est branché.
|
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
|
## 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 |
|
| [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) |
|
| [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 |
|
| [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 |
|
| [bootstrap-sequence.puml](diagrams/bootstrap-sequence.puml) | Séquence | `CRCore.enable()` côté plugin de jeu |
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|||||||
@@ -367,6 +367,45 @@ Format léger : une décision = un titre + contexte + choix + raison.
|
|||||||
bases existantes, ALTER TABLE manuel ou suppression du fichier
|
bases existantes, ALTER TABLE manuel ou suppression du fichier
|
||||||
(les bases d'event sont jetables).
|
(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)
|
## 2026-06-10 — Réorganisation : `util/` (toujours) vs `features/` (opt-in)
|
||||||
|
|
||||||
- **Choix** : séparation nette en deux couches :
|
- **Choix** : séparation nette en deux couches :
|
||||||
|
|||||||
@@ -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
@@ -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.
|
**Statut** : implémenté. Point d'entrée unique pour les plugins de jeu.
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -86,7 +86,8 @@ public class MyGamePlugin extends JavaPlugin {
|
|||||||
// OPTION B — granularité, n'activer que ce qu'on veut
|
// OPTION B — granularité, n'activer que ce qu'on veut
|
||||||
// this.core = new CRCore(this, new CRCoreConfig()
|
// this.core = new CRCore(this, new CRCoreConfig()
|
||||||
// .setupTeams()
|
// .setupTeams()
|
||||||
// .setupPlaceholders()) // pas de players
|
// .setupPlaceholders() // pas de players
|
||||||
|
// .setupModeration()) // ajoute le mod mode
|
||||||
// .enable();
|
// .enable();
|
||||||
|
|
||||||
// OPTION C — par défaut + options
|
// OPTION C — par défaut + options
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
package fr.luc.crcore;
|
package fr.luc.crcore;
|
||||||
|
|
||||||
import fr.luc.crcore.builtin.CoreCommand;
|
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.PlayerProfileRepository;
|
||||||
import fr.luc.crcore.features.player.PlayerProfileService;
|
import fr.luc.crcore.features.player.PlayerProfileService;
|
||||||
import fr.luc.crcore.features.player.impl.BukkitEventFiringPlayerProfileServiceImpl;
|
import fr.luc.crcore.features.player.impl.BukkitEventFiringPlayerProfileServiceImpl;
|
||||||
@@ -90,6 +101,9 @@ public class CRCore {
|
|||||||
// Features player
|
// Features player
|
||||||
private PlayerProfileRepository playerProfileRepository;
|
private PlayerProfileRepository playerProfileRepository;
|
||||||
private PlayerProfileService playerProfileService;
|
private PlayerProfileService playerProfileService;
|
||||||
|
// Features moderation
|
||||||
|
private ModerationRepository moderationRepository;
|
||||||
|
private ModerationService moderationService;
|
||||||
// Command routing
|
// Command routing
|
||||||
private CoreCommand coreCommand;
|
private CoreCommand coreCommand;
|
||||||
private boolean enabled = false;
|
private boolean enabled = false;
|
||||||
@@ -139,6 +153,14 @@ public class CRCore {
|
|||||||
: new InMemoryPlayerProfileRepository();
|
: new InMemoryPlayerProfileRepository();
|
||||||
this.playerProfileService = buildPlayerProfileService(playerProfileRepository);
|
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 ----
|
// ---- 4. Listeners Bukkit ----
|
||||||
// Broadcast listener : écoute les events team + player. S'il n'y a aucune
|
// 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é"
|
plugin.getLogger().info("CR-Core activé"
|
||||||
+ " (teams=" + config.isTeamsEnabled()
|
+ " (teams=" + config.isTeamsEnabled()
|
||||||
+ ", players=" + config.isPlayersEnabled()
|
+ ", players=" + config.isPlayersEnabled()
|
||||||
+ ", placeholders=" + config.isPlaceholdersEnabled() + ").");
|
+ ", placeholders=" + config.isPlaceholdersEnabled()
|
||||||
|
+ ", moderation=" + config.isModerationEnabled() + ").");
|
||||||
enabled = true;
|
enabled = true;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -224,6 +247,25 @@ public class CRCore {
|
|||||||
return new YamlTeamConfigService(plugin, repository);
|
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. */
|
/** Construit le {@link CoreCommand} avec les services des features activées. */
|
||||||
protected CoreCommand buildCoreCommand() {
|
protected CoreCommand buildCoreCommand() {
|
||||||
return new CoreCommand(
|
return new CoreCommand(
|
||||||
@@ -231,7 +273,8 @@ public class CRCore {
|
|||||||
config.isPlayersEnabled() ? playerProfileService : null,
|
config.isPlayersEnabled() ? playerProfileService : null,
|
||||||
messages,
|
messages,
|
||||||
broadcasts,
|
broadcasts,
|
||||||
config.isTeamsEnabled() ? teamConfig : null);
|
config.isTeamsEnabled() ? teamConfig : null,
|
||||||
|
config.isModerationEnabled() ? moderationService : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Command registration (plugin.yml ou CommandMap dynamique) ----
|
// ---- Command registration (plugin.yml ou CommandMap dynamique) ----
|
||||||
@@ -324,6 +367,15 @@ public class CRCore {
|
|||||||
return playerProfileService;
|
return playerProfileService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ModerationService getModerationService() {
|
||||||
|
requireModerationEnabled();
|
||||||
|
return moderationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModerationService moderation() {
|
||||||
|
return getModerationService();
|
||||||
|
}
|
||||||
|
|
||||||
public CoreCommand getCoreCommand() { return coreCommand; }
|
public CoreCommand getCoreCommand() { return coreCommand; }
|
||||||
public boolean isEnabled() { return enabled; }
|
public boolean isEnabled() { return enabled; }
|
||||||
|
|
||||||
@@ -342,4 +394,11 @@ public class CRCore {
|
|||||||
"Players feature is not enabled — appelez CRCoreConfig.setupPlayers() avant enable().");
|
"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().");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ public class CRCoreConfig {
|
|||||||
private boolean teamsEnabled = false;
|
private boolean teamsEnabled = false;
|
||||||
private boolean playersEnabled = false;
|
private boolean playersEnabled = false;
|
||||||
private boolean placeholdersEnabled = false;
|
private boolean placeholdersEnabled = false;
|
||||||
|
private boolean moderationEnabled = false;
|
||||||
|
|
||||||
// ---- Util / infra setters ----
|
// ---- Util / infra setters ----
|
||||||
|
|
||||||
@@ -115,9 +116,18 @@ public class CRCoreConfig {
|
|||||||
return this;
|
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. */
|
/** Active toutes les features en une fois. */
|
||||||
public CRCoreConfig setupAll() {
|
public CRCoreConfig setupAll() {
|
||||||
return setupTeams().setupPlayers().setupPlaceholders();
|
return setupTeams().setupPlayers().setupPlaceholders().setupModeration();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Getters ----
|
// ---- Getters ----
|
||||||
@@ -129,4 +139,5 @@ public class CRCoreConfig {
|
|||||||
public boolean isTeamsEnabled() { return teamsEnabled; }
|
public boolean isTeamsEnabled() { return teamsEnabled; }
|
||||||
public boolean isPlayersEnabled() { return playersEnabled; }
|
public boolean isPlayersEnabled() { return playersEnabled; }
|
||||||
public boolean isPlaceholdersEnabled() { return placeholdersEnabled; }
|
public boolean isPlaceholdersEnabled() { return placeholdersEnabled; }
|
||||||
|
public boolean isModerationEnabled() { return moderationEnabled; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package fr.luc.crcore.builtin;
|
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.player.PlayerProfileService;
|
||||||
import fr.luc.crcore.features.team.TeamService;
|
import fr.luc.crcore.features.team.TeamService;
|
||||||
import fr.luc.crcore.features.team.command.TeamGroupSubCommand;
|
import fr.luc.crcore.features.team.command.TeamGroupSubCommand;
|
||||||
@@ -15,13 +17,10 @@ import java.util.Objects;
|
|||||||
/**
|
/**
|
||||||
* Commande racine {@code /core}. Container des features actives.
|
* Commande racine {@code /core}. Container des features actives.
|
||||||
*
|
*
|
||||||
* <p>Les services {@code teamService}, {@code playerProfileService} et
|
* <p>Les services des features peuvent être {@code null} si la feature
|
||||||
* {@code teamConfig} peuvent être {@code null} si la feature correspondante
|
* correspondante n'a pas été activée via les
|
||||||
* n'a pas été activée via {@link fr.luc.crcore.CRCoreConfig#setupTeams()}
|
* {@link fr.luc.crcore.CRCoreConfig#setupTeams()} etc. — la sous-commande
|
||||||
* etc. — la sous-commande associée n'est alors simplement pas enregistrée.
|
* 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.
|
|
||||||
*/
|
*/
|
||||||
public class CoreCommand extends BaseCommand {
|
public class CoreCommand extends BaseCommand {
|
||||||
|
|
||||||
@@ -30,19 +29,21 @@ public class CoreCommand extends BaseCommand {
|
|||||||
protected final MessagesService messages;
|
protected final MessagesService messages;
|
||||||
protected final BroadcastService broadcasts;
|
protected final BroadcastService broadcasts;
|
||||||
protected final TeamConfigService teamConfig;
|
protected final TeamConfigService teamConfig;
|
||||||
|
protected final ModerationService moderation;
|
||||||
|
|
||||||
public CoreCommand(TeamService teamService,
|
public CoreCommand(TeamService teamService,
|
||||||
PlayerProfileService playerProfileService,
|
PlayerProfileService playerProfileService,
|
||||||
MessagesService messages,
|
MessagesService messages,
|
||||||
BroadcastService broadcasts,
|
BroadcastService broadcasts,
|
||||||
TeamConfigService teamConfig) {
|
TeamConfigService teamConfig,
|
||||||
|
ModerationService moderation) {
|
||||||
super("core");
|
super("core");
|
||||||
// Les services de features peuvent être null (feature off).
|
|
||||||
this.teamService = teamService;
|
this.teamService = teamService;
|
||||||
this.playerProfileService = playerProfileService;
|
this.playerProfileService = playerProfileService;
|
||||||
this.messages = Objects.requireNonNull(messages, "messages");
|
this.messages = Objects.requireNonNull(messages, "messages");
|
||||||
this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts");
|
this.broadcasts = Objects.requireNonNull(broadcasts, "broadcasts");
|
||||||
this.teamConfig = teamConfig;
|
this.teamConfig = teamConfig;
|
||||||
|
this.moderation = moderation;
|
||||||
description("Commandes du noyau CR-Core");
|
description("Commandes du noyau CR-Core");
|
||||||
registerDefaults();
|
registerDefaults();
|
||||||
}
|
}
|
||||||
@@ -52,13 +53,12 @@ public class CoreCommand extends BaseCommand {
|
|||||||
if (teamService != null && teamConfig != null) {
|
if (teamService != null && teamConfig != null) {
|
||||||
addSubCommand(new TeamGroupSubCommand(teamService, messages, teamConfig));
|
addSubCommand(new TeamGroupSubCommand(teamService, messages, teamConfig));
|
||||||
}
|
}
|
||||||
|
if (moderation != null) {
|
||||||
|
addSubCommand(new AdminToggleSubCommand(moderation, messages));
|
||||||
|
}
|
||||||
addSubCommand(new CoreReloadSubCommand(messages, broadcasts, teamConfig));
|
addSubCommand(new CoreReloadSubCommand(messages, broadcasts, teamConfig));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Override de {@link BaseCommand#handleResult} pour utiliser
|
|
||||||
* {@link MessagesService} sur les cas génériques.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleResult(CommandSender sender, CommandResult result) {
|
protected void handleResult(CommandSender sender, CommandResult result) {
|
||||||
switch (result.getType()) {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.create.broadcast: "&7[CR] &f{player} a maintenant un profil CR-Core."
|
||||||
player.profile.delete.broadcast: "&7[CR] &fProfil de {player} supprimé."
|
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}"
|
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é."
|
||||||
|
|||||||
Reference in New Issue
Block a user