Files
Cites_Plugins/docs/setup.md
T
Antone Barbaud 4efaa5bbde 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>
2026-06-10 12:19:21 +02:00

12 KiB

Setup technique

Stack

  • Type : librairie Java (jar) — pas un plugin Bukkit
  • artifactId Maven : CR-Core
  • Build : Maven, Java 11
  • Intégrations optionnelles : PlaceholderAPI (auto-détectée si installée)
  • API serveur (provided) : Paper 1.16.5
  • SQLite (compile) : org.xerial:sqlite-jdbc:3.45.3.0
  • Package racine : fr.luc.crcore

Dépôts Maven

Build & install local

mvn clean install

Publie fr.luc:CR-Core:1.0-SNAPSHOT dans le repo Maven local ~/.m2/, prêt à être consommé par les plugins de jeu.

Intégration dans un plugin de jeu

pom.xml

<dependency>
    <groupId>fr.luc</groupId>
    <artifactId>CR-Core</artifactId>
    <version>1.0-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>

Le plugin de jeu doit shader CR-Core dans son jar final (avec maven-shade-plugin) pour que sqlite-jdbc + le code du noyau soient bien embarqués sur le serveur.

plugin.yml du plugin de jeu

name: MyGame
main: fr.exemple.mygame.MyGamePlugin
version: 1.0
api-version: 1.16

Pas besoin de déclarer la commande core : CR-Core l'enregistre dynamiquement via le CommandMap du serveur quand elle est absente du plugin.yml. Si tu préfères la déclarer quand même (pour customiser la description ou les aliases côté Bukkit), tu peux ajouter :

commands:
  core:
    description: Commandes CR-Core
    aliases: [cr, crcore]

Dans ce cas, CR-Core détecte la commande déclarée et s'y branche normalement via setExecutor (pas d'enregistrement dynamique).

Code minimal — setup modulaire

⚠️ Important : depuis 2026-06-10, les features (teams, players, placeholders) sont opt-in via CRCoreConfig.setupX(). Une instance new CRCoreConfig() n'active rien par défaut — il faut appeler setupAll() ou les setupX() individuels.

public class MyGamePlugin extends JavaPlugin {

    private CRCore core;

    @Override
    public void onEnable() {
        // OPTION A — tout activer en une ligne (teams + players + placeholders)
        this.core = new CRCore(this, new CRCoreConfig().setupAll()).enable();

        // OPTION B — granularité, n'activer que ce qu'on veut
        // this.core = new CRCore(this, new CRCoreConfig()
        //         .setupTeams()
        //         .setupPlaceholders()        // pas de players
        //         .setupModeration())         // ajoute le mod mode
        //         .enable();

        // OPTION C — par défaut + options
        // this.core = new CRCore(this, new CRCoreConfig()
        //         .withSqliteFile("mygame.db")
        //         .withCommandName("game")
        //         .setupTeams())
        //         .enable();

        // Listener custom sur les events team (nécessite setupTeams)
        getServer().getPluginManager().registerEvents(new MyTeamListener(), this);

        // Table custom pour stocker des données spécifiques au jeu
        // (database util est toujours disponible)
        core.getDatabase().table("my_kills")
                .ifNotExists()
                .column("player_id", ColumnType.UUID).primaryKey()
                .column("kills", ColumnType.INTEGER).notNull().defaultValue("0")
                .create();
    }

    @Override
    public void onDisable() {
        if (core != null) core.disable();
    }
}

Écouter les évènements

public class MyTeamListener implements Listener {

    @EventHandler
    public void onTeamCreate(TeamCreateEvent event) {
        Team team = event.getTeam();
        Bukkit.broadcastMessage("Nouvelle équipe : " + team.getName());
    }

    @EventHandler
    public void onPlayerJoinTeam(PlayerJoinTeamEvent event) {
        // Auto-join uniquement (chef qui ajoute = TeamMemberAddEvent)
    }

    @EventHandler
    public void onScoreChange(TeamScoreChangeEvent event) {
        // event.getScoreName(), event.getOldValue(), event.getNewValue()
    }
}

Overrider une commande par défaut

public class MyCreateCommand extends TeamCreateSubCommand {
    public MyCreateCommand(TeamService service) {
        super(service);
        permission("mygame.team.create"); // permission custom
    }

    @Override
    public CommandResult execute(CommandContext ctx) {
        // logique custom
        return super.execute(ctx);
    }
}

// Dans onEnable() :
core.getCoreCommand().findSubCommand("team")
    .ifPresent(team -> team.replaceSubCommand("create", new MyCreateCommand(core.getTeamService())));

Stocker / récupérer des données custom via SQLite

Database db = core.getDatabase();

db.update("INSERT OR REPLACE INTO my_kills (player_id, kills) VALUES (?, ?)",
          playerUuid, 42);

int kills = db.queryOne(
        "SELECT kills FROM my_kills WHERE player_id = ?",
        rs -> rs.getInt("kills"),
        playerUuid
).orElse(0);

Arborescence du projet

CitesPlugin/                                 # dossier IntelliJ (renommer plus tard si voulu)
├── pom.xml
├── GEMINI.md
├── docs/
│   ├── README.md
│   ├── setup.md
│   ├── features.md
│   ├── decisions.md
│   └── diagrams/*.puml
└── src/main/java/fr/luc/crcore/
    ├── CRCore.java                          # bootstrap (setupAll/setupTeams/...)
    ├── CRCoreConfig.java                    # config builder + flags features
    ├── builtin/                             # commandes top-level (toutes features)
    │   ├── CoreCommand.java                 # /core
    │   └── CoreReloadSubCommand.java        # /core reload
    ├── util/                                # ◆ couche util — toujours active ◆
    │   ├── common/                          # abstractions partagées
    │   │   ├── Identifiable.java
    │   │   ├── Named.java
    │   │   ├── ScoreHolder.java
    │   │   ├── AbstractEntity.java
    │   │   └── Repository.java
    │   ├── database/                        # wrapper SQLite générique
    │   │   ├── Database.java
    │   │   ├── TableBuilder.java
    │   │   ├── ColumnType.java
    │   │   ├── RowMapper.java
    │   │   └── DatabaseException.java
    │   ├── command/                         # framework de commandes
    │   │   ├── Command.java                 (interface)
    │   │   ├── AbstractCommand.java
    │   │   ├── BaseCommand.java
    │   │   ├── SubCommand.java
    │   │   ├── CommandContext.java
    │   │   ├── CommandResult.java
    │   │   ├── CommandException.java
    │   │   ├── ArgumentType.java
    │   │   ├── ArgumentTypes.java
    │   │   └── ArgumentDef.java             (package-private)
    │   ├── message/                         # service messages YAML
    │   │   ├── MessagesService.java
    │   │   └── impl/YamlMessagesService.java
    │   ├── broadcast/                       # service broadcasts YAML + listener
    │   │   ├── BroadcastService.java
    │   │   ├── BroadcastAudience.java
    │   │   ├── BroadcastContext.java
    │   │   ├── CRCoreBroadcastListener.java
    │   │   └── impl/YamlBroadcastService.java
    │   ├── gui/                             # framework GUI (InventoryHolder)
    │   │   ├── AbstractInventoryGui.java
    │   │   ├── GuiClickHandler.java
    │   │   ├── GuiListener.java
    │   │   └── GuiItems.java
    │   └── placeholder/                     # PAPI expansion (opt-in)
    │       └── CRCorePlaceholderExpansion.java
    └── features/                            # ◆ features opt-in via setupX() ◆
        ├── team/                            (setupTeams())
        │   ├── Team.java, TeamMember, enums, TeamRanking
        │   ├── TeamService.java             (interface)
        │   ├── TeamRepository.java          (interface)
        │   ├── event/                       (9 events Bukkit)
        │   ├── exception/                   (4 exceptions)
        │   ├── impl/                        (TeamServiceImpl, BukkitEventFiring*, InMemory*, Sqlite*)
        │   ├── config/                      (settings typés)
        │   │   ├── TeamSetting.java
        │   │   ├── TeamSettings.java        (registry)
        │   │   ├── TeamConfigService.java
        │   │   ├── impl/YamlTeamConfigService.java
        │   │   └── gui/                     (Global + TeamSettingsGui)
        │   │       ├── AbstractSettingsGui.java
        │   │       ├── GlobalSettingsGui.java
        │   │       └── TeamSettingsGui.java
        │   └── command/                     (14 sous-commandes /core team)
        │       ├── TeamGroupSubCommand.java
        │       ├── TeamArgumentTypes.java
        │       └── Team*SubCommand.java
        └── player/                          (setupPlayers())
            ├── PlayerProfile.java
            ├── PlayerRanking.java
            ├── PlayerProfileService.java    (interface)
            ├── PlayerProfileRepository.java (interface)
            ├── event/                       (3 events)
            ├── exception/                   (2 exceptions)
            └── impl/                        (services + repositories)

Fichiers de config générés au premier enable()

Au premier démarrage, CR-Core crée TROIS fichiers dans le dataFolder :

Fichier Rôle
<plugin-name-lowercase>-messages.yml Templates de tous les messages (commandes + broadcasts)
<plugin-name-lowercase>-broadcasts.yml Routes : qui reçoit quel event
<plugin-name-lowercase>-team-config.yml Paramètres globaux d'équipe (defaults appliqués à toutes les teams)

Les deux suivent le même pattern : si ton plugin de jeu bundle un fichier au même nom dans ses ressources, c'est lui qui sert de template initial à la place des defaults CR-Core. Les defaults restent en mémoire en fallback — donc les clés non présentes dans le fichier user marchent quand même.

Hot reload : /core reload (permission crcore.reload) relit les deux fichiers sans restart.

Fichier messages

Au premier enable(), CR-Core crée :

<plugin-dataFolder>/<plugin-name-lowercase>-messages.yml

(ex. cites-messages.yml si ton plugin s'appelle Cites)

Édite ce fichier pour customiser tous les messages des commandes /core team .... Les clés non présentes retombent sur les defaults CR-Core (dans le jar) — donc tu peux n'inclure que ce que tu veux changer.

Pour pré-remplir ce fichier avec tes overrides + tes propres messages dès la première exécution, bundle un fichier au même nom dans tes ressources plugin (src/main/resources/cites-messages.yml). Si présent, c'est lui qui sert de template au lieu des defaults CR-Core.

API en code :

core.messages().get("team.create.success",
    "name", team.getName(), "tag", team.getTag());

core.messages().set("mygame.custom", "&aHello {player} !");
core.messages().loadAdditional("my-extras.yml");
core.messages().reload();   // hot reload depuis le disque

Tables SQLite créées par CR-Core

Au premier enable(), les tables suivantes sont créées (en IF NOT EXISTS) :

  • crcore_teams — métadonnées par équipe (id, name, tag, color, leader_id, visibility, spawn_world/x/y/z/yaw/pitch)
  • crcore_team_members — un membre = (team_id, player_id, role, joined_at)
  • crcore_team_scores — (team_id, score_name, value)
  • crcore_team_settings — (team_id, key, value, type) — overrides per-team
  • crcore_player_profiles — un profil = (id)
  • crcore_player_scores — (profile_id, score_name, value)

Le préfixe crcore_ évite les collisions avec les tables custom du plugin de jeu.