feat: SQLite persistence, default /core commands, Bukkit events, bootstrap
CRCore bootstrap class: one-line setup for game plugins (new CRCore(this).enable()).
Wires SQLite, services with event firing, and the /core command tree.
SQLite layer (fr.luc.crcore.database): Database wrapper exposing execute/update/
queryOne/query plus a fluent TableBuilder. ColumnType enum, RowMapper interface,
DatabaseException. Game plugins create their own tables in 2 lines via
db.table("foo").ifNotExists().column(...).create().
Repositories: SqliteTeamRepository and SqlitePlayerProfileRepository extend their
InMemory counterparts (write-through cache). 5 internal tables prefixed crcore_.
Command framework refactored for nested sub-commands: subcommand storage moved
from BaseCommand to AbstractCommand, recursive dispatch() and tabComplete(),
replaceSubCommand() for plugin overrides.
Default /core team commands (13 leaf sub-commands): create, delete, add, remove,
join, leave, info, list, transfer, visibility, score, top, setspawn. Each in its
own class under fr.luc.crcore.command.builtin.team, fully substitutable.
Bukkit events: 9 team events (Create/Dissolve/MemberAdd/MemberRemove/PlayerJoin/
LeadershipTransfer/VisibilityChange/ScoreChange/SpawnPointChange) + 3 player
events (ProfileCreate/Delete/ScoreChange). All post-only, non-cancellable.
BukkitEventFiringTeamServiceImpl and BukkitEventFiringPlayerProfileServiceImpl
override the on* hooks to call Bukkit.getPluginManager().callEvent.
JavaDoc on all new public classes and key existing ones. docs/, GEMINI.md and
PUML diagrams synced: new sections (built-in commands, events, database,
bootstrap), 4 new diagrams (builtin-commands, events, database, bootstrap-
sequence), and 7 new architecture decisions logged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package fr.luc.crcore;
|
||||
|
||||
import fr.luc.crcore.command.builtin.CoreCommand;
|
||||
import fr.luc.crcore.database.Database;
|
||||
import fr.luc.crcore.player.BukkitEventFiringPlayerProfileServiceImpl;
|
||||
import fr.luc.crcore.player.InMemoryPlayerProfileRepository;
|
||||
import fr.luc.crcore.player.PlayerProfileRepository;
|
||||
import fr.luc.crcore.player.PlayerProfileService;
|
||||
import fr.luc.crcore.player.SqlitePlayerProfileRepository;
|
||||
import fr.luc.crcore.team.BukkitEventFiringTeamServiceImpl;
|
||||
import fr.luc.crcore.team.InMemoryTeamRepository;
|
||||
import fr.luc.crcore.team.SqliteTeamRepository;
|
||||
import fr.luc.crcore.team.TeamRepository;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Point d'entrée unique de CR-Core pour un plugin de jeu downstream.
|
||||
*
|
||||
* <p>Instanciée une fois dans {@code onEnable()}, branche en cascade :
|
||||
* <ol>
|
||||
* <li>la base SQLite (dans le dataFolder du plugin),</li>
|
||||
* <li>les repositories (SQLite ou in-memory selon {@link CRCoreConfig}),</li>
|
||||
* <li>les services team + player avec fire d'évènements Bukkit,</li>
|
||||
* <li>la commande {@code /core} avec tous ses sous-commandes par défaut.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h2>Utilisation minimale côté plugin de jeu</h2>
|
||||
* <pre>{@code
|
||||
* public class MyGamePlugin extends JavaPlugin {
|
||||
*
|
||||
* private CRCore core;
|
||||
*
|
||||
* @Override
|
||||
* public void onEnable() {
|
||||
* this.core = new CRCore(this).enable();
|
||||
* // /core team create/delete/add/remove/join/leave/... est prêt
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* public void onDisable() {
|
||||
* if (core != null) core.disable();
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>Le plugin de jeu doit avoir déclaré la commande dans son {@code plugin.yml} :
|
||||
* <pre>{@code
|
||||
* commands:
|
||||
* core:
|
||||
* description: Commandes CR-Core
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Override</h2>
|
||||
* Tout est accessible via les getters : {@link #getTeamService()},
|
||||
* {@link #getCoreCommand()}, {@link #getDatabase()}, etc. Pour remplacer une
|
||||
* sous-commande, voir {@link CoreCommand}. Pour remplacer un service complet,
|
||||
* sous-classer {@code CRCore} et override {@link #buildTeamService}.
|
||||
*/
|
||||
public class CRCore {
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final CRCoreConfig config;
|
||||
|
||||
private Database database;
|
||||
private TeamRepository teamRepository;
|
||||
private TeamService teamService;
|
||||
private PlayerProfileRepository playerProfileRepository;
|
||||
private PlayerProfileService playerProfileService;
|
||||
private CoreCommand coreCommand;
|
||||
private boolean enabled = false;
|
||||
|
||||
/** Construit CR-Core avec la config par défaut (SQLite activée, commande "core"). */
|
||||
public CRCore(JavaPlugin plugin) {
|
||||
this(plugin, new CRCoreConfig());
|
||||
}
|
||||
|
||||
public CRCore(JavaPlugin plugin, CRCoreConfig config) {
|
||||
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||
this.config = Objects.requireNonNull(config, "config");
|
||||
}
|
||||
|
||||
/**
|
||||
* Branche tout : ouvre la DB, instancie les services, enregistre la
|
||||
* commande. Idempotent : un second appel est no-op.
|
||||
*
|
||||
* @return {@code this} pour chaîner.
|
||||
*/
|
||||
public CRCore enable() {
|
||||
if (enabled) return this;
|
||||
if (config.isSqliteEnabled()) {
|
||||
File dbFile = new File(plugin.getDataFolder(), config.getSqliteFilename());
|
||||
if (!dbFile.getParentFile().exists() && !dbFile.getParentFile().mkdirs()) {
|
||||
plugin.getLogger().warning("Impossible de créer le dataFolder : " + dbFile.getParentFile());
|
||||
}
|
||||
this.database = new Database(dbFile);
|
||||
this.teamRepository = new SqliteTeamRepository(database);
|
||||
this.playerProfileRepository = new SqlitePlayerProfileRepository(database);
|
||||
} else {
|
||||
this.teamRepository = new InMemoryTeamRepository();
|
||||
this.playerProfileRepository = new InMemoryPlayerProfileRepository();
|
||||
}
|
||||
|
||||
this.teamService = buildTeamService(teamRepository);
|
||||
this.playerProfileService = buildPlayerProfileService(playerProfileRepository);
|
||||
|
||||
this.coreCommand = buildCoreCommand(teamService, playerProfileService);
|
||||
registerCommand();
|
||||
|
||||
plugin.getLogger().info("CR-Core activé.");
|
||||
enabled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Libère les ressources (ferme la DB notamment). Idempotent. */
|
||||
public void disable() {
|
||||
if (!enabled) return;
|
||||
if (database != null) {
|
||||
try {
|
||||
database.close();
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().warning("Erreur en fermant la DB : " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info("CR-Core désactivé.");
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
// ---- Override points ----
|
||||
|
||||
/** Construit le {@link TeamService}. Override pour utiliser une impl custom. */
|
||||
protected TeamService buildTeamService(TeamRepository repository) {
|
||||
return new BukkitEventFiringTeamServiceImpl(plugin, repository);
|
||||
}
|
||||
|
||||
/** Construit le {@link PlayerProfileService}. Override pour une impl custom. */
|
||||
protected PlayerProfileService buildPlayerProfileService(PlayerProfileRepository repository) {
|
||||
return new BukkitEventFiringPlayerProfileServiceImpl(plugin, repository);
|
||||
}
|
||||
|
||||
/** Construit le {@link CoreCommand}. Override pour ajouter des groupes top-level. */
|
||||
protected CoreCommand buildCoreCommand(TeamService teamService, PlayerProfileService playerProfileService) {
|
||||
return new CoreCommand(teamService, playerProfileService);
|
||||
}
|
||||
|
||||
private void registerCommand() {
|
||||
PluginCommand cmd = plugin.getCommand(config.getCommandName());
|
||||
if (cmd == null) {
|
||||
plugin.getLogger().warning("Commande '" + config.getCommandName() +
|
||||
"' absente du plugin.yml — /" + config.getCommandName() +
|
||||
" ne sera pas reconnue.");
|
||||
return;
|
||||
}
|
||||
cmd.setExecutor(coreCommand);
|
||||
cmd.setTabCompleter(coreCommand);
|
||||
}
|
||||
|
||||
// ---- Getters ----
|
||||
|
||||
public JavaPlugin getPlugin() { return plugin; }
|
||||
public CRCoreConfig getConfig() { return config; }
|
||||
public Database getDatabase() { return database; }
|
||||
public TeamRepository getTeamRepository() { return teamRepository; }
|
||||
public TeamService getTeamService() { return teamService; }
|
||||
public PlayerProfileRepository getPlayerProfileRepository() { return playerProfileRepository; }
|
||||
public PlayerProfileService getPlayerProfileService() { return playerProfileService; }
|
||||
public CoreCommand getCoreCommand() { return coreCommand; }
|
||||
public boolean isEnabled() { return enabled; }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package fr.luc.crcore;
|
||||
|
||||
/**
|
||||
* Configuration de {@link CRCore} fournie au constructeur. API builder : on
|
||||
* chaîne les {@code with...} pour modifier les valeurs par défaut.
|
||||
*
|
||||
* <pre>{@code
|
||||
* new CRCore(this, new CRCoreConfig()
|
||||
* .withSqliteFile("mydata.db")
|
||||
* .withCommandName("game"))
|
||||
* .enable();
|
||||
* }</pre>
|
||||
*
|
||||
* <p>Valeurs par défaut :
|
||||
* <ul>
|
||||
* <li>SQLite activé, fichier {@code crcore.db} dans le dataFolder du plugin</li>
|
||||
* <li>Commande Bukkit racine : {@code core}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class CRCoreConfig {
|
||||
|
||||
private boolean sqliteEnabled = true;
|
||||
private String sqliteFilename = "crcore.db";
|
||||
private String commandName = "core";
|
||||
|
||||
/** Désactive SQLite — toutes les données vivent en mémoire (perdues au reload/stop). */
|
||||
public CRCoreConfig withInMemoryStorage() {
|
||||
this.sqliteEnabled = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Active SQLite et fixe le nom du fichier (relatif au dataFolder du plugin). */
|
||||
public CRCoreConfig withSqliteFile(String filename) {
|
||||
this.sqliteEnabled = true;
|
||||
this.sqliteFilename = filename;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change le nom de la commande Bukkit racine. Doit matcher l'entrée du
|
||||
* {@code commands:} dans le {@code plugin.yml} du plugin de jeu.
|
||||
*/
|
||||
public CRCoreConfig withCommandName(String commandName) {
|
||||
this.commandName = commandName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isSqliteEnabled() { return sqliteEnabled; }
|
||||
public String getSqliteFilename() { return sqliteFilename; }
|
||||
public String getCommandName() { return commandName; }
|
||||
}
|
||||
@@ -1,19 +1,49 @@
|
||||
package fr.luc.crcore.command;
|
||||
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Base partagée par {@link BaseCommand} (top-level Bukkit) et {@link SubCommand}
|
||||
* (feuille ou groupe imbriqué). Porte tous les champs communs : nom, aliases,
|
||||
* permission, player-only, description, usage, arguments typés, et un registre
|
||||
* de sous-commandes imbriquées.
|
||||
*
|
||||
* <p>Une commande peut être :
|
||||
* <ul>
|
||||
* <li><b>feuille</b> : pas de sous-commandes, implémente {@link #execute}</li>
|
||||
* <li><b>groupe</b> : sous-commandes via {@link #addSubCommand}, le routage
|
||||
* est récursif. Si aucune sous-commande ne matche, {@link #execute} est
|
||||
* appelé en fallback (par défaut : liste les sous-commandes).</li>
|
||||
* <li><b>hybride</b> : groupe ET arguments propres — déconseillé, le
|
||||
* routage donne priorité aux sous-commandes.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>L'override par les plugins de jeu se fait via {@link #replaceSubCommand}
|
||||
* (remplacer une sous-commande par nom) ou en sous-classant et en redéfinissant
|
||||
* {@link #execute}.
|
||||
*/
|
||||
public abstract class AbstractCommand implements Command {
|
||||
|
||||
private final String name;
|
||||
private final List<String> aliases = new ArrayList<>();
|
||||
private final List<ArgumentDef> arguments = new ArrayList<>();
|
||||
private final Map<String, SubCommand> subCommandsByName = new LinkedHashMap<>();
|
||||
private final Map<String, SubCommand> subCommandsByAlias = new HashMap<>();
|
||||
|
||||
private String permission;
|
||||
private boolean playerOnly;
|
||||
private String description = "";
|
||||
@@ -26,65 +56,126 @@ public abstract class AbstractCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getName() {
|
||||
return name;
|
||||
}
|
||||
// ---- Command interface ----
|
||||
|
||||
@Override
|
||||
public final List<String> getAliases() {
|
||||
return Collections.unmodifiableList(aliases);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getPermission() {
|
||||
return permission;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean isPlayerOnly() {
|
||||
return playerOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getDescription() {
|
||||
return description;
|
||||
}
|
||||
@Override public final String getName() { return name; }
|
||||
@Override public final List<String> getAliases() { return Collections.unmodifiableList(aliases); }
|
||||
@Override public final String getPermission() { return permission; }
|
||||
@Override public final boolean isPlayerOnly() { return playerOnly; }
|
||||
@Override public final String getDescription() { return description; }
|
||||
|
||||
/** Usage explicite si défini via {@link #usage(String)}, sinon usage auto-construit. */
|
||||
public final String getUsage() {
|
||||
return usage != null ? usage : buildDefaultUsage();
|
||||
}
|
||||
|
||||
// ---- Builders (à appeler dans le constructeur des sous-classes) ----
|
||||
|
||||
/** Ajoute un ou plusieurs alias. Les aliases sont case-insensitive. */
|
||||
protected final void addAlias(String... aliases) {
|
||||
for (String alias : aliases) {
|
||||
this.aliases.add(alias.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
/** Définit la permission Bukkit requise (ex. {@code "crcore.team.create"}). */
|
||||
protected final void permission(String permission) {
|
||||
this.permission = permission;
|
||||
}
|
||||
|
||||
/** Restreint l'exécution aux joueurs (refus pour console). */
|
||||
protected final void playerOnly() {
|
||||
this.playerOnly = true;
|
||||
}
|
||||
|
||||
/** Description courte affichée dans l'aide. */
|
||||
protected final void description(String description) {
|
||||
this.description = Objects.requireNonNullElse(description, "");
|
||||
}
|
||||
|
||||
/** Usage explicite (sinon construit automatiquement à partir des arguments). */
|
||||
protected final void usage(String usage) {
|
||||
this.usage = usage;
|
||||
}
|
||||
|
||||
/** Déclare un argument positionnel obligatoire. */
|
||||
protected final void argument(String name, ArgumentType<?> type) {
|
||||
arguments.add(new ArgumentDef(name, type, true));
|
||||
}
|
||||
|
||||
/** Déclare un argument positionnel optionnel (peut être omis par l'utilisateur). */
|
||||
protected final void optionalArgument(String name, ArgumentType<?> type) {
|
||||
arguments.add(new ArgumentDef(name, type, false));
|
||||
}
|
||||
|
||||
// ---- Sub-command management ----
|
||||
|
||||
/**
|
||||
* Enregistre une sous-commande. Lève {@link IllegalStateException} si une
|
||||
* sous-commande du même nom existe déjà (utiliser {@link #replaceSubCommand}
|
||||
* pour overrider).
|
||||
*/
|
||||
protected final void addSubCommand(SubCommand sub) {
|
||||
Objects.requireNonNull(sub, "sub");
|
||||
if (subCommandsByName.containsKey(sub.getName())) {
|
||||
throw new IllegalStateException(
|
||||
"Sub-command '" + sub.getName() + "' already registered on '" + name + "'");
|
||||
}
|
||||
subCommandsByName.put(sub.getName(), sub);
|
||||
for (String alias : sub.getAliases()) {
|
||||
subCommandsByAlias.put(alias, sub);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remplace une sous-commande existante par son nom. Utilisé par les plugins
|
||||
* de jeu pour overrider un comportement par défaut.
|
||||
*
|
||||
* <pre>{@code
|
||||
* coreCommand.findSubCommand("team")
|
||||
* .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc)));
|
||||
* }</pre>
|
||||
*
|
||||
* @return l'ancienne sous-commande remplacée, ou {@link Optional#empty()}
|
||||
* si aucune n'existait sous ce nom.
|
||||
*/
|
||||
public final Optional<SubCommand> replaceSubCommand(String name, SubCommand replacement) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
Objects.requireNonNull(replacement, "replacement");
|
||||
String key = name.toLowerCase();
|
||||
SubCommand old = subCommandsByName.remove(key);
|
||||
if (old != null) {
|
||||
for (String alias : old.getAliases()) {
|
||||
subCommandsByAlias.remove(alias);
|
||||
}
|
||||
}
|
||||
subCommandsByName.put(replacement.getName(), replacement);
|
||||
for (String alias : replacement.getAliases()) {
|
||||
subCommandsByAlias.put(alias, replacement);
|
||||
}
|
||||
return Optional.ofNullable(old);
|
||||
}
|
||||
|
||||
/** Recherche une sous-commande par nom ou par alias (case-insensitive). */
|
||||
public final Optional<SubCommand> findSubCommand(String label) {
|
||||
if (label == null) return Optional.empty();
|
||||
String lc = label.toLowerCase();
|
||||
SubCommand sub = subCommandsByName.get(lc);
|
||||
if (sub != null) return Optional.of(sub);
|
||||
return Optional.ofNullable(subCommandsByAlias.get(lc));
|
||||
}
|
||||
|
||||
/** Toutes les sous-commandes enregistrées, dans l'ordre d'insertion. */
|
||||
public final Collection<SubCommand> getSubCommands() {
|
||||
return Collections.unmodifiableCollection(subCommandsByName.values());
|
||||
}
|
||||
|
||||
public final boolean hasSubCommands() {
|
||||
return !subCommandsByName.isEmpty();
|
||||
}
|
||||
|
||||
// ---- Argument introspection ----
|
||||
|
||||
public final int getRequiredArgumentCount() {
|
||||
return (int) arguments.stream().filter(ArgumentDef::isRequired).count();
|
||||
}
|
||||
@@ -97,6 +188,129 @@ public abstract class AbstractCommand implements Command {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
// ---- Dispatch (routage récursif vers sous-commandes) ----
|
||||
|
||||
/**
|
||||
* Achemine l'exécution : si {@code args[0]} matche une sous-commande, on
|
||||
* recurse dessus avec {@code args[1..]}. Sinon on appelle {@link #execute}
|
||||
* de cette commande. Gère les checks permission / player-only.
|
||||
*/
|
||||
public final CommandResult dispatch(CommandSender sender, String label, String[] args) {
|
||||
if (!checkAccess(sender)) {
|
||||
return permission != null && !sender.hasPermission(permission)
|
||||
? CommandResult.noPermission()
|
||||
: CommandResult.playerOnly();
|
||||
}
|
||||
|
||||
if (args.length > 0) {
|
||||
Optional<SubCommand> sub = findSubCommand(args[0]);
|
||||
if (sub.isPresent()) {
|
||||
String[] subArgs = Arrays.copyOfRange(args, 1, args.length);
|
||||
return sub.get().dispatch(sender, label, subArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length < getRequiredArgumentCount()) {
|
||||
return CommandResult.invalidUsage("Usage: " + buildDefaultUsage());
|
||||
}
|
||||
|
||||
CommandContext ctx;
|
||||
try {
|
||||
ctx = buildContext(sender, label, args);
|
||||
} catch (CommandException ex) {
|
||||
return CommandResult.failure(ex.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
return execute(ctx);
|
||||
} catch (CommandException ex) {
|
||||
return CommandResult.failure(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-complétion récursive : si {@code args} a une seule valeur, suggère
|
||||
* les sous-commandes accessibles + les arguments propres. Sinon recurse
|
||||
* vers la sous-commande qui matche.
|
||||
*/
|
||||
public final List<String> tabComplete(CommandSender sender, String[] args) {
|
||||
if (args.length == 0) return Collections.emptyList();
|
||||
|
||||
if (args.length == 1) {
|
||||
String partial = args[0].toLowerCase();
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
// Sous-commandes (filtrées par permission)
|
||||
for (SubCommand sub : subCommandsByName.values()) {
|
||||
if (sub.getPermission() != null && !sender.hasPermission(sub.getPermission())) continue;
|
||||
suggestions.add(sub.getName());
|
||||
suggestions.addAll(sub.getAliases());
|
||||
}
|
||||
// Argument 0 si on est une feuille avec arguments
|
||||
if (!arguments.isEmpty()) {
|
||||
suggestions.addAll(arguments.get(0).getType().suggestions(sender, args[0]));
|
||||
}
|
||||
return suggestions.stream()
|
||||
.filter(s -> s.toLowerCase().startsWith(partial))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// args.length > 1 : route vers sous-commande si match
|
||||
Optional<SubCommand> sub = findSubCommand(args[0]);
|
||||
if (sub.isPresent()) {
|
||||
if (sub.get().getPermission() != null && !sender.hasPermission(sub.get().getPermission())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String[] subArgs = Arrays.copyOfRange(args, 1, args.length);
|
||||
return sub.get().tabComplete(sender, subArgs);
|
||||
}
|
||||
|
||||
// Pas de sous-commande : complète l'argument courant
|
||||
int argIndex = args.length - 1;
|
||||
if (argIndex < arguments.size()) {
|
||||
return arguments.get(argIndex).getType().suggestions(sender, args[argIndex]);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// ---- Execution (à overrider par les feuilles) ----
|
||||
|
||||
/**
|
||||
* Logique métier de la commande. Override par les sous-classes.
|
||||
*
|
||||
* <p>Comportement par défaut :
|
||||
* <ul>
|
||||
* <li>Si cette commande a des sous-commandes → affiche la liste (aide).</li>
|
||||
* <li>Sinon → renvoie {@link CommandResult#invalidUsage()}.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public CommandResult execute(CommandContext context) {
|
||||
if (hasSubCommands()) {
|
||||
return listSubCommands(context);
|
||||
}
|
||||
return CommandResult.invalidUsage("Usage: " + buildDefaultUsage());
|
||||
}
|
||||
|
||||
// ---- Override helpers ----
|
||||
|
||||
/** Default {@code execute} pour les groupes : affiche la liste des sous-commandes accessibles. */
|
||||
protected CommandResult listSubCommands(CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder(ChatColor.YELLOW.toString())
|
||||
.append("Commandes disponibles pour ")
|
||||
.append(ChatColor.WHITE).append('/').append(context.getLabel());
|
||||
for (SubCommand sub : subCommandsByName.values()) {
|
||||
if (sub.getPermission() != null && !context.getSender().hasPermission(sub.getPermission())) continue;
|
||||
sb.append('\n').append(ChatColor.GRAY).append(" - ")
|
||||
.append(ChatColor.WHITE).append(sub.getName());
|
||||
if (!sub.getDescription().isEmpty()) {
|
||||
sb.append(ChatColor.GRAY).append(" — ").append(sub.getDescription());
|
||||
}
|
||||
}
|
||||
context.reply(sb.toString());
|
||||
return CommandResult.success();
|
||||
}
|
||||
|
||||
/** Construit un usage par défaut à partir du nom et des arguments déclarés. */
|
||||
protected String buildDefaultUsage() {
|
||||
StringBuilder sb = new StringBuilder("/").append(name);
|
||||
for (ArgumentDef def : arguments) {
|
||||
@@ -108,26 +322,26 @@ public abstract class AbstractCommand implements Command {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> tabComplete(CommandSender sender, int argIndex, String partial) {
|
||||
if (argIndex >= 0 && argIndex < arguments.size()) {
|
||||
return arguments.get(argIndex).getType().suggestions(sender, partial);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
/** Check standard de permission + player-only. */
|
||||
protected boolean checkAccess(CommandSender sender) {
|
||||
if (permission != null && !sender.hasPermission(permission)) return false;
|
||||
if (playerOnly && !(sender instanceof Player)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected CommandContext buildContext(CommandSender sender, String label, String[] subArgs) {
|
||||
/** Parse les arguments raw et construit le {@link CommandContext}. */
|
||||
protected CommandContext buildContext(CommandSender sender, String label, String[] rawArgs) {
|
||||
Map<String, Object> parsed = new LinkedHashMap<>();
|
||||
int max = Math.min(subArgs.length, arguments.size());
|
||||
int max = Math.min(rawArgs.length, arguments.size());
|
||||
for (int i = 0; i < max; i++) {
|
||||
ArgumentDef def = arguments.get(i);
|
||||
try {
|
||||
Object value = def.getType().parse(subArgs[i]);
|
||||
Object value = def.getType().parse(rawArgs[i]);
|
||||
parsed.put(def.getName(), value);
|
||||
} catch (CommandException ex) {
|
||||
throw new CommandException("Argument '" + def.getName() + "': " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
return new CommandContext(sender, label, subArgs, parsed);
|
||||
return new CommandContext(sender, label, rawArgs, parsed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,172 +4,48 @@ import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Commande top-level branchée sur Bukkit. À utiliser comme racine de l'arbre :
|
||||
* <pre>{@code
|
||||
* PluginCommand cmd = plugin.getCommand("core");
|
||||
* cmd.setExecutor(new CoreCommand(...));
|
||||
* cmd.setTabCompleter((CoreCommand) cmd.getExecutor());
|
||||
* }</pre>
|
||||
*
|
||||
* <p>{@code BaseCommand} se contente de relayer {@code onCommand} et
|
||||
* {@code onTabComplete} vers {@link AbstractCommand#dispatch} et
|
||||
* {@link AbstractCommand#tabComplete}. Toute la logique (routage récursif,
|
||||
* permissions, player-only, parsing d'arguments) vit dans
|
||||
* {@link AbstractCommand}.
|
||||
*/
|
||||
public abstract class BaseCommand extends AbstractCommand
|
||||
implements CommandExecutor, TabCompleter {
|
||||
|
||||
private final Map<String, SubCommand> subCommandsByName = new LinkedHashMap<>();
|
||||
private final Map<String, SubCommand> subCommandsByAlias = new HashMap<>();
|
||||
|
||||
protected BaseCommand(String name, String... aliases) {
|
||||
super(name, aliases);
|
||||
}
|
||||
|
||||
protected final void addSubCommand(SubCommand sub) {
|
||||
Objects.requireNonNull(sub, "sub");
|
||||
if (subCommandsByName.containsKey(sub.getName())) {
|
||||
throw new IllegalStateException("Sub-command already registered: " + sub.getName());
|
||||
}
|
||||
subCommandsByName.put(sub.getName(), sub);
|
||||
for (String alias : sub.getAliases()) {
|
||||
subCommandsByAlias.put(alias, sub);
|
||||
}
|
||||
}
|
||||
|
||||
public final Collection<SubCommand> getSubCommands() {
|
||||
return Collections.unmodifiableCollection(subCommandsByName.values());
|
||||
}
|
||||
|
||||
public final Optional<SubCommand> findSubCommand(String label) {
|
||||
if (label == null) return Optional.empty();
|
||||
String lc = label.toLowerCase();
|
||||
SubCommand sub = subCommandsByName.get(lc);
|
||||
if (sub != null) return Optional.of(sub);
|
||||
return Optional.ofNullable(subCommandsByAlias.get(lc));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when no sub-command matches the first argument. Override for custom
|
||||
* fallback behavior. Default: lists available sub-commands.
|
||||
*/
|
||||
protected CommandResult execute(CommandContext context) {
|
||||
if (subCommandsByName.isEmpty()) {
|
||||
return CommandResult.invalidUsage("No action specified.");
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(ChatColor.YELLOW + "Available sub-commands:");
|
||||
for (SubCommand sub : subCommandsByName.values()) {
|
||||
sb.append('\n').append(ChatColor.GRAY).append(" - ")
|
||||
.append(ChatColor.WHITE).append(sub.getName());
|
||||
if (!sub.getDescription().isEmpty()) {
|
||||
sb.append(ChatColor.GRAY).append(" — ").append(sub.getDescription());
|
||||
}
|
||||
}
|
||||
context.reply(sb.toString());
|
||||
return CommandResult.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean onCommand(CommandSender sender, org.bukkit.command.Command command,
|
||||
String label, String[] args) {
|
||||
if (!checkAccess(sender, this)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.length == 0 || findSubCommand(args[0]).isEmpty()) {
|
||||
CommandContext ctx = new CommandContext(sender, label, args, Collections.emptyMap());
|
||||
try {
|
||||
handleResult(sender, execute(ctx));
|
||||
} catch (CommandException ex) {
|
||||
sender.sendMessage(ChatColor.RED + ex.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
SubCommand sub = findSubCommand(args[0]).orElseThrow();
|
||||
if (!checkAccess(sender, sub)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String[] subArgs = Arrays.copyOfRange(args, 1, args.length);
|
||||
if (subArgs.length < sub.getRequiredArgumentCount()) {
|
||||
sender.sendMessage(ChatColor.RED + "Usage: " + buildUsageFor(label, sub));
|
||||
return true;
|
||||
}
|
||||
|
||||
CommandContext ctx;
|
||||
try {
|
||||
ctx = sub.buildContext(sender, label, subArgs);
|
||||
} catch (CommandException ex) {
|
||||
sender.sendMessage(ChatColor.RED + ex.getMessage());
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
handleResult(sender, sub.execute(ctx));
|
||||
} catch (CommandException ex) {
|
||||
sender.sendMessage(ChatColor.RED + ex.getMessage());
|
||||
}
|
||||
CommandResult result = dispatch(sender, label, args);
|
||||
handleResult(sender, result);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final List<String> onTabComplete(CommandSender sender, org.bukkit.command.Command command,
|
||||
String alias, String[] args) {
|
||||
if (args.length == 0) return Collections.emptyList();
|
||||
|
||||
if (args.length == 1) {
|
||||
String partial = args[0].toLowerCase();
|
||||
List<String> names = new ArrayList<>();
|
||||
for (SubCommand sub : subCommandsByName.values()) {
|
||||
if (sub.getPermission() != null && !sender.hasPermission(sub.getPermission())) {
|
||||
continue;
|
||||
}
|
||||
names.add(sub.getName());
|
||||
names.addAll(sub.getAliases());
|
||||
}
|
||||
return names.stream()
|
||||
.filter(n -> n.startsWith(partial))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Optional<SubCommand> subOpt = findSubCommand(args[0]);
|
||||
if (subOpt.isEmpty()) return Collections.emptyList();
|
||||
SubCommand sub = subOpt.get();
|
||||
if (sub.getPermission() != null && !sender.hasPermission(sub.getPermission())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
int argIndex = args.length - 2;
|
||||
String partial = args[args.length - 1];
|
||||
return sub.tabComplete(sender, argIndex, partial);
|
||||
}
|
||||
|
||||
protected boolean checkAccess(CommandSender sender, Command target) {
|
||||
if (target.getPermission() != null && !sender.hasPermission(target.getPermission())) {
|
||||
sender.sendMessage(ChatColor.RED + "You don't have permission.");
|
||||
return false;
|
||||
}
|
||||
if (target.isPlayerOnly() && !(sender instanceof Player)) {
|
||||
sender.sendMessage(ChatColor.RED + "Only players can use this command.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected String buildUsageFor(String label, SubCommand sub) {
|
||||
StringBuilder sb = new StringBuilder("/").append(label).append(' ').append(sub.getName());
|
||||
for (ArgumentDef def : sub.getArgumentDefs()) {
|
||||
sb.append(' ');
|
||||
sb.append(def.isRequired() ? '<' : '[');
|
||||
sb.append(def.getName());
|
||||
sb.append(def.isRequired() ? '>' : ']');
|
||||
}
|
||||
return sb.toString();
|
||||
return tabComplete(sender, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche le {@link CommandResult} à l'utilisateur. Override pour
|
||||
* personnaliser le formatage (couleurs, locales, etc.).
|
||||
*/
|
||||
protected void handleResult(CommandSender sender, CommandResult result) {
|
||||
switch (result.getType()) {
|
||||
case SUCCESS -> {
|
||||
@@ -181,8 +57,8 @@ public abstract class BaseCommand extends AbstractCommand
|
||||
(result.getMessage() != null ? result.getMessage() : "Command failed."));
|
||||
case INVALID_USAGE -> sender.sendMessage(ChatColor.RED +
|
||||
(result.getMessage() != null ? result.getMessage() : "Invalid usage."));
|
||||
case NO_PERMISSION -> sender.sendMessage(ChatColor.RED + "You don't have permission.");
|
||||
case PLAYER_ONLY -> sender.sendMessage(ChatColor.RED + "Only players can use this command.");
|
||||
case NO_PERMISSION -> sender.sendMessage(ChatColor.RED + "Vous n'avez pas la permission.");
|
||||
case PLAYER_ONLY -> sender.sendMessage(ChatColor.RED + "Seul un joueur peut utiliser cette commande.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Contrat partagé par toutes les commandes du framework CR-Core
|
||||
* ({@link BaseCommand} top-level et {@link SubCommand} imbriquées). Implémenté
|
||||
* concrètement par {@link AbstractCommand}.
|
||||
*/
|
||||
public interface Command {
|
||||
|
||||
String getName();
|
||||
@@ -16,10 +21,20 @@ public interface Command {
|
||||
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* Logique d'exécution de la commande (cas feuille, ou fallback si aucune
|
||||
* sous-commande ne matche).
|
||||
*/
|
||||
CommandResult execute(CommandContext context);
|
||||
|
||||
List<String> tabComplete(CommandSender sender, int argIndex, String partial);
|
||||
/**
|
||||
* Suggestions de tab-completion en fonction des arguments déjà tapés.
|
||||
* {@code args} contient TOUS les arguments depuis ce niveau de commande
|
||||
* (sans le nom de la commande elle-même).
|
||||
*/
|
||||
List<String> tabComplete(CommandSender sender, String[] args);
|
||||
|
||||
/** {@code true} si {@code label} match le nom ou un alias (case-insensitive). */
|
||||
default boolean matches(String label) {
|
||||
if (label == null) return false;
|
||||
String lc = label.toLowerCase();
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
package fr.luc.crcore.command;
|
||||
|
||||
/**
|
||||
* Sous-commande imbriquée. Peut être :
|
||||
* <ul>
|
||||
* <li><b>feuille</b> — override {@link #execute(CommandContext)} avec la logique métier</li>
|
||||
* <li><b>groupe</b> — appelle {@code addSubCommand(...)} dans son constructeur pour
|
||||
* déléguer à des sous-sous-commandes (ex. {@code /core team create})</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Toute la machinerie de routage / parsing / tab-complete est héritée de
|
||||
* {@link AbstractCommand}.
|
||||
*/
|
||||
public abstract class SubCommand extends AbstractCommand {
|
||||
|
||||
protected SubCommand(String name, String... aliases) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package fr.luc.crcore.command.builtin;
|
||||
|
||||
import fr.luc.crcore.command.BaseCommand;
|
||||
import fr.luc.crcore.command.builtin.team.TeamGroupSubCommand;
|
||||
import fr.luc.crcore.player.PlayerProfileService;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Commande racine {@code /core}. Container des groupes par défaut.
|
||||
*
|
||||
* <p>Branchée par {@code CRCore.enable()} sur la {@code PluginCommand "core"}
|
||||
* du plugin de jeu (qui doit l'avoir déclarée dans son {@code plugin.yml}).
|
||||
*
|
||||
* <p>Sans arguments, affiche l'aide des groupes disponibles. Avec {@code team
|
||||
* <action>}, route vers {@link TeamGroupSubCommand}.
|
||||
*
|
||||
* <h2>Override</h2>
|
||||
* Pour remplacer un groupe entier :
|
||||
* <pre>{@code
|
||||
* core.getCoreCommand().replaceSubCommand("team", new MyTeamGroup(svc));
|
||||
* }</pre>
|
||||
* Pour remplacer une feuille :
|
||||
* <pre>{@code
|
||||
* core.getCoreCommand().findSubCommand("team")
|
||||
* .ifPresent(t -> t.replaceSubCommand("create", new MyCreate(svc)));
|
||||
* }</pre>
|
||||
*/
|
||||
public class CoreCommand extends BaseCommand {
|
||||
|
||||
protected final TeamService teamService;
|
||||
protected final PlayerProfileService playerProfileService;
|
||||
|
||||
public CoreCommand(TeamService teamService, PlayerProfileService playerProfileService) {
|
||||
super("core", "cr", "crcore");
|
||||
this.teamService = Objects.requireNonNull(teamService, "teamService");
|
||||
this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService");
|
||||
description("Commandes du noyau CR-Core");
|
||||
registerDefaults();
|
||||
}
|
||||
|
||||
/** Enregistre les groupes par défaut. Override pour ajouter / retirer des groupes. */
|
||||
protected void registerDefaults() {
|
||||
addSubCommand(new TeamGroupSubCommand(teamService));
|
||||
// Futur : addSubCommand(new PlayerGroupSubCommand(playerProfileService));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.ArgumentTypes;
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team add <player>}
|
||||
*
|
||||
* <p>Le chef ajoute un joueur à son équipe. Marche que la team soit PUBLIC ou
|
||||
* PRIVATE — c'est une action chef, pas un auto-join.
|
||||
*/
|
||||
public class TeamAddSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamAddSubCommand(TeamService service) {
|
||||
super("add", "invite");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Ajouter un joueur à son équipe (chef uniquement)");
|
||||
playerOnly();
|
||||
argument("player", ArgumentTypes.ONLINE_PLAYER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Player executor = ctx.requirePlayer();
|
||||
Player target = ctx.get("player");
|
||||
|
||||
Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
|
||||
if (team == null) {
|
||||
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||
}
|
||||
if (!team.getLeaderId().equals(executor.getUniqueId())) {
|
||||
return CommandResult.failure("Seul le chef peut ajouter des membres.");
|
||||
}
|
||||
if (service.getTeamOfPlayer(target.getUniqueId()).isPresent()) {
|
||||
return CommandResult.failure(target.getName() + " est déjà dans une équipe.");
|
||||
}
|
||||
service.addMember(team.getId(), target.getUniqueId());
|
||||
return CommandResult.success(target.getName() + " ajouté à l'équipe " + team.getName() + ".");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.ArgumentType;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Types d'arguments spécifiques aux commandes team. Fournit un
|
||||
* {@link ArgumentType} qui résout un nom d'équipe en {@link Team} et propose
|
||||
* la tab-complétion des équipes existantes.
|
||||
*/
|
||||
public final class TeamArgumentTypes {
|
||||
|
||||
private TeamArgumentTypes() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout un nom d'équipe en {@link Team} (case-insensitive) en interrogeant
|
||||
* le {@link TeamService}. Suggère les noms d'équipes existantes en
|
||||
* tab-completion.
|
||||
*/
|
||||
public static ArgumentType<Team> teamByName(TeamService service) {
|
||||
Objects.requireNonNull(service, "service");
|
||||
return new ArgumentType<>() {
|
||||
@Override
|
||||
public Team parse(String input) {
|
||||
return service.getTeamByName(input).orElseThrow(() ->
|
||||
new fr.luc.crcore.command.CommandException(
|
||||
"Aucune équipe trouvée : " + input));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggestions(CommandSender sender, String partial) {
|
||||
String lower = partial.toLowerCase();
|
||||
return service.getAllTeams().stream()
|
||||
.map(Team::getName)
|
||||
.filter(n -> n.toLowerCase().startsWith(lower))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.ArgumentTypes;
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamColor;
|
||||
import fr.luc.crcore.team.TeamException;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import fr.luc.crcore.team.TeamVisibility;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team create <name> <tag> <color> [visibility]}
|
||||
*
|
||||
* <p>Crée une équipe dont l'exécutant devient le chef. Visibilité par défaut :
|
||||
* {@link TeamVisibility#PRIVATE}.
|
||||
*/
|
||||
public class TeamCreateSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamCreateSubCommand(TeamService service) {
|
||||
super("create", "c", "new");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Créer une équipe");
|
||||
permission("crcore.team.create");
|
||||
playerOnly();
|
||||
argument("name", ArgumentTypes.STRING);
|
||||
argument("tag", ArgumentTypes.STRING);
|
||||
argument("color", ArgumentTypes.enumOf(TeamColor.class));
|
||||
optionalArgument("visibility", ArgumentTypes.enumOf(TeamVisibility.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Player player = ctx.requirePlayer();
|
||||
String name = ctx.get("name");
|
||||
String tag = ctx.get("tag");
|
||||
TeamColor color = ctx.get("color");
|
||||
TeamVisibility visibility = ctx.<TeamVisibility>getOptional("visibility")
|
||||
.orElse(TeamVisibility.PRIVATE);
|
||||
|
||||
try {
|
||||
Team team = service.createTeam(name, tag, color, player.getUniqueId(), visibility);
|
||||
return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag() + "] créée.");
|
||||
} catch (TeamException ex) {
|
||||
return CommandResult.failure(ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team delete}
|
||||
*
|
||||
* <p>Dissout l'équipe de l'exécutant. Réservé au chef. Pas d'argument :
|
||||
* l'équipe ciblée est déduite du joueur.
|
||||
*
|
||||
* <p>Aliases : {@code disband}, {@code dissolve}.
|
||||
*/
|
||||
public class TeamDeleteSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamDeleteSubCommand(TeamService service) {
|
||||
super("delete", "disband", "dissolve");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Dissoudre son équipe (chef uniquement)");
|
||||
playerOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Player player = ctx.requirePlayer();
|
||||
Team team = service.getTeamOfPlayer(player.getUniqueId())
|
||||
.orElse(null);
|
||||
if (team == null) {
|
||||
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||
}
|
||||
if (!team.getLeaderId().equals(player.getUniqueId())) {
|
||||
return CommandResult.failure("Seul le chef peut dissoudre l'équipe.");
|
||||
}
|
||||
service.dissolveTeam(team.getId());
|
||||
return CommandResult.success("Équipe " + team.getName() + " dissoute.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Groupe {@code /core team ...} : container de toutes les sous-commandes
|
||||
* d'équipe par défaut.
|
||||
*
|
||||
* <p>Pour overrider une sous-commande, un plugin de jeu fait :
|
||||
* <pre>{@code
|
||||
* core.getCoreCommand().findSubCommand("team")
|
||||
* .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc)));
|
||||
* }</pre>
|
||||
*
|
||||
* <p>Ou sous-classe {@code TeamGroupSubCommand} et redéfinit son constructeur
|
||||
* pour swap ce qu'il faut.
|
||||
*/
|
||||
public class TeamGroupSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamGroupSubCommand(TeamService service) {
|
||||
super("team", "t");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Gestion des équipes");
|
||||
registerDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre toutes les sous-commandes par défaut. Override pour exclure
|
||||
* ou ajouter des sous-commandes au lieu du jeu standard.
|
||||
*/
|
||||
protected void registerDefaults() {
|
||||
addSubCommand(new TeamCreateSubCommand(service));
|
||||
addSubCommand(new TeamDeleteSubCommand(service));
|
||||
addSubCommand(new TeamAddSubCommand(service));
|
||||
addSubCommand(new TeamRemoveSubCommand(service));
|
||||
addSubCommand(new TeamJoinSubCommand(service));
|
||||
addSubCommand(new TeamLeaveSubCommand(service));
|
||||
addSubCommand(new TeamInfoSubCommand(service));
|
||||
addSubCommand(new TeamListSubCommand(service));
|
||||
addSubCommand(new TeamTransferSubCommand(service));
|
||||
addSubCommand(new TeamVisibilitySubCommand(service));
|
||||
addSubCommand(new TeamScoreSubCommand(service));
|
||||
addSubCommand(new TeamTopSubCommand(service));
|
||||
addSubCommand(new TeamSetSpawnSubCommand(service));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamMember;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* {@code /core team info [name]}
|
||||
*
|
||||
* <p>Affiche les infos d'une équipe. Si aucun nom n'est donné, affiche celle
|
||||
* de l'exécutant.
|
||||
*/
|
||||
public class TeamInfoSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamInfoSubCommand(TeamService service) {
|
||||
super("info", "i");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Afficher les infos d'une équipe");
|
||||
optionalArgument("name", TeamArgumentTypes.teamByName(service));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Team team = ctx.<Team>getOptional("name").orElseGet(() -> {
|
||||
if (ctx.isPlayer()) {
|
||||
Player p = (Player) ctx.getSender();
|
||||
return service.getTeamOfPlayer(p.getUniqueId()).orElse(null);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (team == null) {
|
||||
return CommandResult.failure("Aucune équipe spécifiée et vous n'êtes pas dans une équipe.");
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
ChatColor c = team.getColor().getChatColor();
|
||||
sb.append(c).append("=== ").append(team.getName())
|
||||
.append(" [#").append(team.getTag()).append("] ===\n");
|
||||
sb.append(ChatColor.GRAY).append("Couleur : ").append(c).append(team.getColor().getDisplayName()).append('\n');
|
||||
sb.append(ChatColor.GRAY).append("Visibilité : ").append(ChatColor.WHITE).append(team.getVisibility()).append('\n');
|
||||
sb.append(ChatColor.GRAY).append("Membres (").append(team.size()).append(") : ").append(ChatColor.WHITE);
|
||||
sb.append(team.getMembers().stream()
|
||||
.map(m -> Bukkit.getOfflinePlayer(m.getPlayerId()).getName() +
|
||||
(m.isLeader() ? "★" : ""))
|
||||
.collect(Collectors.joining(", ")));
|
||||
if (!team.getScores().isEmpty()) {
|
||||
sb.append('\n').append(ChatColor.GRAY).append("Scores : ").append(ChatColor.WHITE)
|
||||
.append(team.getScores().entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining(", ")));
|
||||
}
|
||||
if (team.hasSpawnPoint()) {
|
||||
team.getSpawnPoint().ifPresent(loc -> sb.append('\n').append(ChatColor.GRAY).append("Spawn : ")
|
||||
.append(ChatColor.WHITE).append(loc.getWorld().getName()).append(' ')
|
||||
.append((int) loc.getX()).append('/').append((int) loc.getY()).append('/').append((int) loc.getZ()));
|
||||
}
|
||||
ctx.reply(sb.toString());
|
||||
return CommandResult.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamException;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team join <name>}
|
||||
*
|
||||
* <p>Auto-join sur une équipe PUBLIC. Lève une {@link TeamException} si la
|
||||
* team est privée ou si le joueur est déjà dans une équipe (rendu en
|
||||
* message d'erreur).
|
||||
*/
|
||||
public class TeamJoinSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamJoinSubCommand(TeamService service) {
|
||||
super("join", "j");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Rejoindre une équipe publique");
|
||||
playerOnly();
|
||||
argument("name", TeamArgumentTypes.teamByName(service));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Player player = ctx.requirePlayer();
|
||||
Team team = ctx.get("name");
|
||||
try {
|
||||
service.joinTeam(team.getId(), player.getUniqueId());
|
||||
return CommandResult.success("Vous avez rejoint " + team.getName() + ".");
|
||||
} catch (TeamException ex) {
|
||||
return CommandResult.failure(ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team leave}
|
||||
*
|
||||
* <p>Le joueur quitte volontairement son équipe. Refusé pour le chef (il doit
|
||||
* d'abord transférer le leadership ou dissoudre l'équipe).
|
||||
*/
|
||||
public class TeamLeaveSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamLeaveSubCommand(TeamService service) {
|
||||
super("leave", "quit");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Quitter son équipe");
|
||||
playerOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Player player = ctx.requirePlayer();
|
||||
Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null);
|
||||
if (team == null) {
|
||||
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||
}
|
||||
if (team.getLeaderId().equals(player.getUniqueId())) {
|
||||
return CommandResult.failure(
|
||||
"Vous êtes le chef. Transférez le leadership avec /core team transfer <player>, ou dissolvez avec /core team delete.");
|
||||
}
|
||||
service.removeMember(team.getId(), player.getUniqueId());
|
||||
return CommandResult.success("Vous avez quitté l'équipe " + team.getName() + ".");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.ChatColor;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team list}
|
||||
*
|
||||
* <p>Affiche toutes les équipes existantes avec leur tag, nom, taille et
|
||||
* visibilité.
|
||||
*/
|
||||
public class TeamListSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamListSubCommand(TeamService service) {
|
||||
super("list", "ls");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Lister toutes les équipes");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Collection<Team> teams = service.getAllTeams();
|
||||
if (teams.isEmpty()) {
|
||||
return CommandResult.success("Aucune équipe pour le moment.");
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(ChatColor.YELLOW + "Équipes (" + teams.size() + ") :");
|
||||
for (Team team : teams) {
|
||||
ChatColor c = team.getColor().getChatColor();
|
||||
sb.append('\n').append(ChatColor.GRAY).append(" - ")
|
||||
.append(c).append('[').append(team.getTag()).append("] ")
|
||||
.append(team.getName())
|
||||
.append(ChatColor.GRAY).append(" (").append(team.size()).append(" membres, ")
|
||||
.append(team.getVisibility()).append(")");
|
||||
}
|
||||
ctx.reply(sb.toString());
|
||||
return CommandResult.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.ArgumentTypes;
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team remove <player>}
|
||||
*
|
||||
* <p>Le chef retire un membre. Accepte les joueurs offline (utilise leur nom
|
||||
* pour résoudre l'UUID via {@link Bukkit#getOfflinePlayer(String)}).
|
||||
*/
|
||||
public class TeamRemoveSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamRemoveSubCommand(TeamService service) {
|
||||
super("remove", "kick", "expel");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Retirer un joueur de son équipe (chef uniquement)");
|
||||
playerOnly();
|
||||
argument("player", ArgumentTypes.STRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Player executor = ctx.requirePlayer();
|
||||
String targetName = ctx.get("player");
|
||||
|
||||
Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
|
||||
if (team == null) {
|
||||
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||
}
|
||||
if (!team.getLeaderId().equals(executor.getUniqueId())) {
|
||||
return CommandResult.failure("Seul le chef peut retirer des membres.");
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
|
||||
if (target.getUniqueId().equals(executor.getUniqueId())) {
|
||||
return CommandResult.failure("Pour quitter l'équipe en tant que chef, transférez d'abord le leadership.");
|
||||
}
|
||||
if (!team.hasMember(target.getUniqueId())) {
|
||||
return CommandResult.failure(targetName + " n'est pas dans votre équipe.");
|
||||
}
|
||||
service.removeMember(team.getId(), target.getUniqueId());
|
||||
return CommandResult.success(targetName + " retiré de l'équipe.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.ArgumentTypes;
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team score <team> <name> <add|set> <value>}
|
||||
*
|
||||
* <p>Commande admin pour ajuster les scores d'une équipe à la main (debug,
|
||||
* fix, init). Le gameplay normal pilote les scores via le service, pas via
|
||||
* cette commande.
|
||||
*
|
||||
* <p>Restreinte par défaut à la permission {@code crcore.team.score.modify}.
|
||||
*/
|
||||
public class TeamScoreSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamScoreSubCommand(TeamService service) {
|
||||
super("score");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("[Admin] Modifier le score d'une équipe");
|
||||
permission("crcore.team.score.modify");
|
||||
argument("team", TeamArgumentTypes.teamByName(service));
|
||||
argument("name", ArgumentTypes.STRING);
|
||||
argument("op", ArgumentTypes.choice("add", "set"));
|
||||
argument("value", ArgumentTypes.INTEGER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Team team = ctx.get("team");
|
||||
String name = ctx.get("name");
|
||||
String op = ctx.get("op");
|
||||
int value = ctx.get("value");
|
||||
|
||||
int result = switch (op) {
|
||||
case "add" -> service.addScore(team.getId(), name, value);
|
||||
case "set" -> service.setScore(team.getId(), name, value);
|
||||
default -> throw new IllegalStateException("unreachable: " + op);
|
||||
};
|
||||
return CommandResult.success("Score " + name + " de " + team.getName() + " = " + result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team setspawn}
|
||||
*
|
||||
* <p>Définit le point de spawn de l'équipe à la position courante du chef.
|
||||
*/
|
||||
public class TeamSetSpawnSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamSetSpawnSubCommand(TeamService service) {
|
||||
super("setspawn", "spawn");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Définir le point de spawn de l'équipe (chef uniquement)");
|
||||
playerOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Player player = ctx.requirePlayer();
|
||||
Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null);
|
||||
if (team == null) {
|
||||
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||
}
|
||||
if (!team.getLeaderId().equals(player.getUniqueId())) {
|
||||
return CommandResult.failure("Seul le chef peut définir le spawn.");
|
||||
}
|
||||
service.setSpawnPoint(team.getId(), player.getLocation());
|
||||
return CommandResult.success("Spawn de l'équipe défini à votre position.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.ArgumentTypes;
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.TeamRanking;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.ChatColor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team top [score]}
|
||||
*
|
||||
* <p>Affiche le classement des équipes. Sans argument, classement global
|
||||
* (somme de tous les scores). Avec un nom de score, classement sur ce score
|
||||
* précis.
|
||||
*/
|
||||
public class TeamTopSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
protected final int limit;
|
||||
|
||||
public TeamTopSubCommand(TeamService service) {
|
||||
this(service, 10);
|
||||
}
|
||||
|
||||
public TeamTopSubCommand(TeamService service, int limit) {
|
||||
super("top", "ranking", "leaderboard");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
this.limit = limit;
|
||||
description("Classement des équipes");
|
||||
optionalArgument("score", ArgumentTypes.STRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
String scoreName = ctx.<String>getOptional("score").orElse(null);
|
||||
List<TeamRanking> ranking = scoreName == null
|
||||
? service.getTopGlobalRanking(limit)
|
||||
: service.getTopRankingByScore(scoreName, limit);
|
||||
|
||||
if (ranking.isEmpty()) {
|
||||
return CommandResult.success("Aucune équipe à classer.");
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(ChatColor.YELLOW + "Top " + ranking.size() +
|
||||
(scoreName == null ? " (global) :" : " (" + scoreName + ") :"));
|
||||
for (TeamRanking r : ranking) {
|
||||
sb.append('\n').append(ChatColor.GRAY).append(" ").append(r.rank()).append(". ")
|
||||
.append(r.team().getColor().getChatColor()).append(r.team().getName())
|
||||
.append(ChatColor.GRAY).append(" — ").append(ChatColor.WHITE).append(r.score());
|
||||
}
|
||||
ctx.reply(sb.toString());
|
||||
return CommandResult.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.ArgumentTypes;
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team transfer <player>}
|
||||
*
|
||||
* <p>Le chef transmet son rôle à un autre membre existant de son équipe.
|
||||
*/
|
||||
public class TeamTransferSubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamTransferSubCommand(TeamService service) {
|
||||
super("transfer");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Transférer le rôle de chef à un autre membre");
|
||||
playerOnly();
|
||||
argument("player", ArgumentTypes.STRING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Player executor = ctx.requirePlayer();
|
||||
String targetName = ctx.get("player");
|
||||
Team team = service.getTeamOfPlayer(executor.getUniqueId()).orElse(null);
|
||||
if (team == null) {
|
||||
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||
}
|
||||
if (!team.getLeaderId().equals(executor.getUniqueId())) {
|
||||
return CommandResult.failure("Seul le chef peut transférer le leadership.");
|
||||
}
|
||||
@SuppressWarnings("deprecation")
|
||||
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
|
||||
if (!team.hasMember(target.getUniqueId())) {
|
||||
return CommandResult.failure(targetName + " n'est pas dans votre équipe.");
|
||||
}
|
||||
service.transferLeadership(team.getId(), target.getUniqueId());
|
||||
return CommandResult.success("Leadership transféré à " + targetName + ".");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package fr.luc.crcore.command.builtin.team;
|
||||
|
||||
import fr.luc.crcore.command.ArgumentTypes;
|
||||
import fr.luc.crcore.command.CommandContext;
|
||||
import fr.luc.crcore.command.CommandResult;
|
||||
import fr.luc.crcore.command.SubCommand;
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamService;
|
||||
import fr.luc.crcore.team.TeamVisibility;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@code /core team visibility <PUBLIC|PRIVATE>}
|
||||
*
|
||||
* <p>Le chef change la visibilité de son équipe. PUBLIC = les autres joueurs
|
||||
* peuvent rejoindre avec {@code /core team join}.
|
||||
*/
|
||||
public class TeamVisibilitySubCommand extends SubCommand {
|
||||
|
||||
protected final TeamService service;
|
||||
|
||||
public TeamVisibilitySubCommand(TeamService service) {
|
||||
super("visibility", "vis");
|
||||
this.service = Objects.requireNonNull(service, "service");
|
||||
description("Changer la visibilité de son équipe");
|
||||
playerOnly();
|
||||
argument("visibility", ArgumentTypes.enumOf(TeamVisibility.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandResult execute(CommandContext ctx) {
|
||||
Player player = ctx.requirePlayer();
|
||||
TeamVisibility visibility = ctx.get("visibility");
|
||||
Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null);
|
||||
if (team == null) {
|
||||
return CommandResult.failure("Vous n'appartenez à aucune équipe.");
|
||||
}
|
||||
if (!team.getLeaderId().equals(player.getUniqueId())) {
|
||||
return CommandResult.failure("Seul le chef peut changer la visibilité.");
|
||||
}
|
||||
service.setVisibility(team.getId(), visibility);
|
||||
return CommandResult.success("Visibilité réglée sur " + visibility + ".");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,17 @@ import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Contrat CRUD générique pour tout aggregate {@link Identifiable}. Permet de
|
||||
* brancher différents backends (in-memory, SQLite, …) sans toucher au code
|
||||
* service.
|
||||
*
|
||||
* <p>Implémentations CR-Core par défaut :
|
||||
* <ul>
|
||||
* <li>{@code InMemoryTeamRepository} / {@code SqliteTeamRepository}</li>
|
||||
* <li>{@code InMemoryPlayerProfileRepository} / {@code SqlitePlayerProfileRepository}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public interface Repository<T extends Identifiable> {
|
||||
|
||||
T save(T entity);
|
||||
|
||||
@@ -2,6 +2,18 @@ package fr.luc.crcore.common;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Contrat partagé par tout ce qui porte des scores nommés. Implémenté par
|
||||
* {@link fr.luc.crcore.team.Team} et {@link fr.luc.crcore.player.PlayerProfile}.
|
||||
*
|
||||
* <p>Les scores sont identifiés par un nom libre (ex. {@code "kills"},
|
||||
* {@code "objectives"}, {@code "global"}) et stockés comme entiers. Un jeu
|
||||
* mono-score peut conventionnellement utiliser {@code "global"}.
|
||||
*
|
||||
* <p>{@link #getScore(String)} renvoie 0 pour un score jamais initialisé
|
||||
* (utile pour {@code addScore("kills", 1)} sans set préalable). Pour
|
||||
* distinguer "jamais set" et "set à 0", utiliser {@link #hasScore(String)}.
|
||||
*/
|
||||
public interface ScoreHolder {
|
||||
|
||||
int getScore(String scoreName);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package fr.luc.crcore.database;
|
||||
|
||||
/**
|
||||
* Types de colonnes supportés par {@link TableBuilder}, chacun mappé sur un
|
||||
* type natif SQLite. Volontairement réduit : SQLite est faiblement typé, ces
|
||||
* 6 types couvrent largement les besoins d'un plugin Minecraft.
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #INTEGER} — entiers 32/64 bits</li>
|
||||
* <li>{@link #REAL} — flottants (double)</li>
|
||||
* <li>{@link #TEXT} — chaînes UTF-8</li>
|
||||
* <li>{@link #BLOB} — données binaires brutes</li>
|
||||
* <li>{@link #BOOLEAN} — stocké comme INTEGER (0/1) côté SQLite</li>
|
||||
* <li>{@link #UUID} — stocké comme TEXT (forme canonique 36 caractères)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum ColumnType {
|
||||
|
||||
INTEGER("INTEGER"),
|
||||
REAL("REAL"),
|
||||
TEXT("TEXT"),
|
||||
BLOB("BLOB"),
|
||||
BOOLEAN("INTEGER"),
|
||||
UUID("TEXT");
|
||||
|
||||
private final String sqlType;
|
||||
|
||||
ColumnType(String sqlType) {
|
||||
this.sqlType = sqlType;
|
||||
}
|
||||
|
||||
/** Nom SQL natif côté SQLite. */
|
||||
public String getSqlType() {
|
||||
return sqlType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package fr.luc.crcore.database;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Façade SQLite minimaliste pour CR-Core et les plugins de jeu downstream.
|
||||
*
|
||||
* <p>Ouvre une connexion JDBC vers un fichier SQLite et expose 4 méthodes pour
|
||||
* couvrir 95 % des besoins :
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #execute(String, Object...)} — DDL ou DML qui ne renvoie rien</li>
|
||||
* <li>{@link #update(String, Object...)} — INSERT/UPDATE/DELETE, renvoie le nombre de lignes affectées</li>
|
||||
* <li>{@link #queryOne(String, RowMapper, Object...)} — SELECT renvoyant au plus une ligne</li>
|
||||
* <li>{@link #query(String, RowMapper, Object...)} — SELECT renvoyant plusieurs lignes</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Les paramètres ({@code Object...}) sont liés via PreparedStatement (donc
|
||||
* pas d'injection SQL possible). Les {@link UUID} sont automatiquement
|
||||
* convertis en {@code TEXT}.
|
||||
*
|
||||
* <p>Pour créer une table de manière fluide, voir {@link #table(String)}.
|
||||
*
|
||||
* <p>Cette classe n'est pas thread-safe. Un plugin Bukkit accède en général
|
||||
* à la DB depuis le main thread ; pour de l'async, synchroniser explicitement
|
||||
* ou ouvrir plusieurs instances.
|
||||
*/
|
||||
public class Database implements AutoCloseable {
|
||||
|
||||
private final Connection connection;
|
||||
|
||||
/**
|
||||
* Ouvre (ou crée) un fichier SQLite. Le dossier parent doit exister.
|
||||
*
|
||||
* @throws DatabaseException si le driver JDBC SQLite est absent du
|
||||
* classpath, ou si l'ouverture du fichier échoue.
|
||||
*/
|
||||
public Database(File file) {
|
||||
Objects.requireNonNull(file, "file");
|
||||
try {
|
||||
// Force le chargement du driver (utile sur certains classloaders Bukkit).
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
String url = "jdbc:sqlite:" + file.getAbsolutePath();
|
||||
this.connection = DriverManager.getConnection(url);
|
||||
// Active les foreign keys (désactivées par défaut sur SQLite).
|
||||
execute("PRAGMA foreign_keys = ON");
|
||||
} catch (ClassNotFoundException ex) {
|
||||
throw new DatabaseException(
|
||||
"SQLite JDBC driver not found on classpath (org.sqlite.JDBC).", ex);
|
||||
} catch (SQLException ex) {
|
||||
throw new DatabaseException("Failed to open SQLite database: " + file, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/** Connexion JDBC sous-jacente, pour les cas avancés (transactions custom, etc.). */
|
||||
public Connection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Démarre la création d'une table de manière fluide.
|
||||
*
|
||||
* <pre>{@code
|
||||
* db.table("foo")
|
||||
* .ifNotExists()
|
||||
* .column("id", ColumnType.UUID).primaryKey()
|
||||
* .column("name", ColumnType.TEXT).notNull()
|
||||
* .create();
|
||||
* }</pre>
|
||||
*/
|
||||
public TableBuilder table(String name) {
|
||||
return new TableBuilder(this, name);
|
||||
}
|
||||
|
||||
/** Vérifie l'existence d'une table dans la base. */
|
||||
public boolean tableExists(String name) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
return queryOne(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
||||
rs -> rs.getString(1),
|
||||
name
|
||||
).isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute un statement SQL (DDL ou autre) sans collecter de résultat. Pour
|
||||
* les INSERT/UPDATE/DELETE préférer {@link #update}.
|
||||
*/
|
||||
public void execute(String sql, Object... params) {
|
||||
try (PreparedStatement stmt = prepare(sql, params)) {
|
||||
stmt.execute();
|
||||
} catch (SQLException ex) {
|
||||
throw new DatabaseException("execute() failed: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute un INSERT/UPDATE/DELETE et renvoie le nombre de lignes affectées.
|
||||
*/
|
||||
public int update(String sql, Object... params) {
|
||||
try (PreparedStatement stmt = prepare(sql, params)) {
|
||||
return stmt.executeUpdate();
|
||||
} catch (SQLException ex) {
|
||||
throw new DatabaseException("update() failed: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute un SELECT et renvoie au plus une ligne. {@link Optional#empty()}
|
||||
* si aucune ligne. Lève {@link DatabaseException} si plusieurs lignes
|
||||
* matchent — penser à mettre {@code LIMIT 1} ou un {@code WHERE} unique.
|
||||
*/
|
||||
public <T> Optional<T> queryOne(String sql, RowMapper<T> mapper, Object... params) {
|
||||
Objects.requireNonNull(mapper, "mapper");
|
||||
try (PreparedStatement stmt = prepare(sql, params);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
if (!rs.next()) return Optional.empty();
|
||||
T value = mapper.map(rs);
|
||||
if (rs.next()) {
|
||||
throw new DatabaseException("queryOne() returned more than one row: " + sql);
|
||||
}
|
||||
return Optional.ofNullable(value);
|
||||
} catch (SQLException ex) {
|
||||
throw new DatabaseException("queryOne() failed: " + sql, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/** Exécute un SELECT et renvoie la liste de toutes les lignes mappées. */
|
||||
public <T> List<T> query(String sql, RowMapper<T> mapper, Object... params) {
|
||||
Objects.requireNonNull(mapper, "mapper");
|
||||
List<T> results = new ArrayList<>();
|
||||
try (PreparedStatement stmt = prepare(sql, params);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
results.add(mapper.map(rs));
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new DatabaseException("query() failed: " + sql, ex);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécute un bloc dans une transaction. Commit automatique si le bloc se
|
||||
* termine sans exception, rollback sinon. Pour des opérations multiples
|
||||
* qui doivent être atomiques (ex. créer une équipe + ses membres).
|
||||
*/
|
||||
public void inTransaction(Runnable block) {
|
||||
Objects.requireNonNull(block, "block");
|
||||
try {
|
||||
connection.setAutoCommit(false);
|
||||
try {
|
||||
block.run();
|
||||
connection.commit();
|
||||
} catch (RuntimeException ex) {
|
||||
connection.rollback();
|
||||
throw ex;
|
||||
} finally {
|
||||
connection.setAutoCommit(true);
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new DatabaseException("Transaction failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
if (connection != null && !connection.isClosed()) {
|
||||
connection.close();
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new DatabaseException("Failed to close database", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Internal ----
|
||||
|
||||
private PreparedStatement prepare(String sql, Object[] params) throws SQLException {
|
||||
PreparedStatement stmt = connection.prepareStatement(sql);
|
||||
if (params != null) {
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
stmt.setObject(i + 1, normalize(params[i]));
|
||||
}
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/** Convertit les valeurs Java non-natives SQL en types utilisables par JDBC. */
|
||||
private static Object normalize(Object value) {
|
||||
if (value == null) return null;
|
||||
if (value instanceof UUID uuid) return uuid.toString();
|
||||
if (value instanceof Enum<?> e) return e.name();
|
||||
if (value instanceof Boolean b) return b ? 1 : 0;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package fr.luc.crcore.database;
|
||||
|
||||
/**
|
||||
* Exception levée pour toute erreur de persistance (ouverture de connexion,
|
||||
* exécution SQL, mapping de résultat). Toujours basée sur une {@link Throwable}
|
||||
* d'origine (généralement {@link java.sql.SQLException}) accessible via
|
||||
* {@link #getCause()}.
|
||||
*/
|
||||
public class DatabaseException extends RuntimeException {
|
||||
|
||||
public DatabaseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DatabaseException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package fr.luc.crcore.database;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Convertit une ligne d'un {@link ResultSet} en un objet Java.
|
||||
*
|
||||
* <p>Utilisé par {@link Database#query} et {@link Database#queryOne}. Le mapper
|
||||
* <b>ne doit pas</b> appeler {@code rs.next()} : c'est {@link Database} qui
|
||||
* itère.
|
||||
*
|
||||
* <pre>{@code
|
||||
* RowMapper<String> nameMapper = rs -> rs.getString("name");
|
||||
* List<String> names = db.query("SELECT name FROM teams", nameMapper);
|
||||
* }</pre>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface RowMapper<T> {
|
||||
|
||||
T map(ResultSet rs) throws SQLException;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package fr.luc.crcore.database;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Builder fluide pour créer une table SQL en quelques lignes. Obtenu via
|
||||
* {@link Database#table(String)}.
|
||||
*
|
||||
* <pre>{@code
|
||||
* db.table("my_scores")
|
||||
* .ifNotExists()
|
||||
* .column("player_id", ColumnType.UUID).primaryKey()
|
||||
* .column("score", ColumnType.INTEGER).notNull().defaultValue("0")
|
||||
* .column("updated_at", ColumnType.INTEGER).notNull()
|
||||
* .create();
|
||||
* }</pre>
|
||||
*
|
||||
* <p>Ne supporte pas (volontairement) les FOREIGN KEY ni les contraintes
|
||||
* multi-colonnes pour rester simple. Pour ces cas, utiliser
|
||||
* {@link Database#execute(String, Object...)} avec un {@code CREATE TABLE}
|
||||
* brut.
|
||||
*/
|
||||
public class TableBuilder {
|
||||
|
||||
private final Database database;
|
||||
private final String name;
|
||||
private final List<ColumnDef> columns = new ArrayList<>();
|
||||
private boolean ifNotExists = false;
|
||||
|
||||
TableBuilder(Database database, String name) {
|
||||
this.database = Objects.requireNonNull(database, "database");
|
||||
this.name = Objects.requireNonNull(name, "name");
|
||||
}
|
||||
|
||||
/** Ajoute {@code IF NOT EXISTS} à la création (idempotent). */
|
||||
public TableBuilder ifNotExists() {
|
||||
this.ifNotExists = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Démarre la définition d'une colonne. Renvoie un {@link ColumnDef} sur
|
||||
* lequel on chaîne {@code .primaryKey()}, {@code .notNull()}, etc.
|
||||
*/
|
||||
public ColumnDef column(String name, ColumnType type) {
|
||||
ColumnDef def = new ColumnDef(this, name, type);
|
||||
columns.add(def);
|
||||
return def;
|
||||
}
|
||||
|
||||
/** Exécute le {@code CREATE TABLE}. Lève {@link DatabaseException} en cas d'échec. */
|
||||
public void create() {
|
||||
if (columns.isEmpty()) {
|
||||
throw new IllegalStateException("Cannot create table '" + name + "' with no columns.");
|
||||
}
|
||||
StringBuilder sql = new StringBuilder("CREATE TABLE ");
|
||||
if (ifNotExists) sql.append("IF NOT EXISTS ");
|
||||
sql.append('\"').append(name).append("\" (");
|
||||
for (int i = 0; i < columns.size(); i++) {
|
||||
if (i > 0) sql.append(", ");
|
||||
sql.append(columns.get(i).toSql());
|
||||
}
|
||||
sql.append(')');
|
||||
database.execute(sql.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Définition d'une colonne en cours de construction. Toutes les méthodes
|
||||
* renvoient {@code this} (ou le {@link TableBuilder} pour continuer la
|
||||
* définition d'une autre colonne).
|
||||
*/
|
||||
public static class ColumnDef {
|
||||
|
||||
private final TableBuilder parent;
|
||||
private final String name;
|
||||
private final ColumnType type;
|
||||
private boolean primaryKey = false;
|
||||
private boolean notNull = false;
|
||||
private boolean unique = false;
|
||||
private String defaultValue = null;
|
||||
|
||||
ColumnDef(TableBuilder parent, String name, ColumnType type) {
|
||||
this.parent = parent;
|
||||
this.name = Objects.requireNonNull(name, "name");
|
||||
this.type = Objects.requireNonNull(type, "type");
|
||||
}
|
||||
|
||||
public ColumnDef primaryKey() { this.primaryKey = true; return this; }
|
||||
public ColumnDef notNull() { this.notNull = true; return this; }
|
||||
public ColumnDef unique() { this.unique = true; return this; }
|
||||
|
||||
/** Valeur par défaut en clause {@code DEFAULT}. Le texte est inséré tel quel — penser à quoter les String. */
|
||||
public ColumnDef defaultValue(String sqlExpression) {
|
||||
this.defaultValue = sqlExpression;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Démarre une nouvelle colonne (raccourci pour {@code .build().column(...)}). */
|
||||
public ColumnDef column(String name, ColumnType type) {
|
||||
return parent.column(name, type);
|
||||
}
|
||||
|
||||
/** Termine la définition et lance le {@code CREATE TABLE}. */
|
||||
public void create() {
|
||||
parent.create();
|
||||
}
|
||||
|
||||
String toSql() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append('\"').append(name).append("\" ").append(type.getSqlType());
|
||||
if (primaryKey) sb.append(" PRIMARY KEY");
|
||||
if (notNull) sb.append(" NOT NULL");
|
||||
if (unique) sb.append(" UNIQUE");
|
||||
if (defaultValue != null) sb.append(" DEFAULT ").append(defaultValue);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fr.luc.crcore.player;
|
||||
|
||||
import fr.luc.crcore.player.event.PlayerProfileCreateEvent;
|
||||
import fr.luc.crcore.player.event.PlayerProfileDeleteEvent;
|
||||
import fr.luc.crcore.player.event.PlayerScoreChangeEvent;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Variante de {@link PlayerProfileServiceImpl} qui tire des évènements Bukkit
|
||||
* via les hooks {@code on...}. Utilisée par défaut par {@code CRCore}.
|
||||
*/
|
||||
public class BukkitEventFiringPlayerProfileServiceImpl extends PlayerProfileServiceImpl {
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
|
||||
public BukkitEventFiringPlayerProfileServiceImpl(JavaPlugin plugin, PlayerProfileRepository repository) {
|
||||
super(repository);
|
||||
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||
}
|
||||
|
||||
protected JavaPlugin getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProfileCreated(PlayerProfile profile) {
|
||||
Bukkit.getPluginManager().callEvent(new PlayerProfileCreateEvent(profile));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProfileDeleted(PlayerProfile profile) {
|
||||
Bukkit.getPluginManager().callEvent(new PlayerProfileDeleteEvent(profile));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onScoreChanged(PlayerProfile profile, String scoreName, int oldValue, int newValue) {
|
||||
Bukkit.getPluginManager().callEvent(
|
||||
new PlayerScoreChangeEvent(profile, scoreName, oldValue, newValue));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,14 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Profil persistant d'un joueur. Identifié par l'UUID Bukkit du joueur,
|
||||
* porte ses scores nommés.
|
||||
*
|
||||
* <p>Indépendant du domaine team : un joueur peut entrer / quitter / changer
|
||||
* d'équipe sans toucher à son profil. Géré par {@link PlayerProfileService}
|
||||
* qui auto-crée le profil à la première écriture de score.
|
||||
*/
|
||||
public class PlayerProfile extends AbstractEntity implements ScoreHolder {
|
||||
|
||||
private final Map<String, Integer> scores;
|
||||
|
||||
@@ -6,6 +6,13 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Façade pour les profils joueurs : lifecycle, scores, classements.
|
||||
*
|
||||
* <p>Auto-création : {@link #addScore}, {@link #setScore} créent
|
||||
* automatiquement le profil s'il n'existe pas (via {@link #getOrCreateProfile}).
|
||||
* Pas besoin d'initialiser explicitement.
|
||||
*/
|
||||
public interface PlayerProfileService {
|
||||
|
||||
PlayerProfile getOrCreateProfile(UUID playerId);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package fr.luc.crcore.player;
|
||||
|
||||
import fr.luc.crcore.database.ColumnType;
|
||||
import fr.luc.crcore.database.Database;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Implémentation {@link PlayerProfileRepository} adossée à SQLite.
|
||||
*
|
||||
* <p>Mêmes principes que {@link fr.luc.crcore.team.SqliteTeamRepository} :
|
||||
* cache mémoire en write-through, schéma créé à l'init, état rechargé depuis
|
||||
* la DB au constructeur.
|
||||
*
|
||||
* <p>Schéma (2 tables) :
|
||||
* <ul>
|
||||
* <li>{@code crcore_player_profiles} — une ligne par joueur</li>
|
||||
* <li>{@code crcore_player_scores} — une ligne par (joueur, score nommé)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class SqlitePlayerProfileRepository extends InMemoryPlayerProfileRepository {
|
||||
|
||||
private static final String TABLE_PROFILES = "crcore_player_profiles";
|
||||
private static final String TABLE_SCORES = "crcore_player_scores";
|
||||
|
||||
private final Database db;
|
||||
|
||||
public SqlitePlayerProfileRepository(Database db) {
|
||||
this.db = Objects.requireNonNull(db, "db");
|
||||
ensureSchema();
|
||||
loadAll();
|
||||
}
|
||||
|
||||
private void ensureSchema() {
|
||||
db.table(TABLE_PROFILES).ifNotExists()
|
||||
.column("id", ColumnType.UUID).primaryKey()
|
||||
.create();
|
||||
|
||||
db.table(TABLE_SCORES).ifNotExists()
|
||||
.column("profile_id", ColumnType.UUID).notNull()
|
||||
.column("score_name", ColumnType.TEXT).notNull()
|
||||
.column("value", ColumnType.INTEGER).notNull()
|
||||
.create();
|
||||
}
|
||||
|
||||
private void loadAll() {
|
||||
var ids = db.query(
|
||||
"SELECT id FROM " + TABLE_PROFILES,
|
||||
rs -> UUID.fromString(rs.getString("id"))
|
||||
);
|
||||
for (UUID id : ids) {
|
||||
PlayerProfile profile = new PlayerProfile(id);
|
||||
db.query(
|
||||
"SELECT score_name, value FROM " + TABLE_SCORES + " WHERE profile_id = ?",
|
||||
rs -> {
|
||||
profile.setScore(rs.getString("score_name"), rs.getInt("value"));
|
||||
return null;
|
||||
},
|
||||
id
|
||||
);
|
||||
super.save(profile); // injecte dans le cache hérité sans repasser par notre override
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlayerProfile save(PlayerProfile profile) {
|
||||
super.save(profile);
|
||||
persist(profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(UUID id) {
|
||||
boolean removed = super.delete(id);
|
||||
if (removed) {
|
||||
db.update("DELETE FROM " + TABLE_SCORES + " WHERE profile_id = ?", id);
|
||||
db.update("DELETE FROM " + TABLE_PROFILES + " WHERE id = ?", id);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
private void persist(PlayerProfile profile) {
|
||||
db.update(
|
||||
"INSERT OR REPLACE INTO " + TABLE_PROFILES + " (id) VALUES (?)",
|
||||
profile.getId()
|
||||
);
|
||||
db.update("DELETE FROM " + TABLE_SCORES + " WHERE profile_id = ?", profile.getId());
|
||||
profile.getScores().forEach((name, value) ->
|
||||
db.update(
|
||||
"INSERT INTO " + TABLE_SCORES + " (profile_id, score_name, value) VALUES (?, ?, ?)",
|
||||
profile.getId(), name, value
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.luc.crcore.player.event;
|
||||
|
||||
import fr.luc.crcore.player.PlayerProfile;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
/** Déclenché juste après la création d'un profil (lazy ou explicite). */
|
||||
public class PlayerProfileCreateEvent extends PlayerProfileEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
public PlayerProfileCreateEvent(PlayerProfile profile) {
|
||||
super(profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.luc.crcore.player.event;
|
||||
|
||||
import fr.luc.crcore.player.PlayerProfile;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
/** Déclenché juste après la suppression d'un profil. */
|
||||
public class PlayerProfileDeleteEvent extends PlayerProfileEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
public PlayerProfileDeleteEvent(PlayerProfile profile) {
|
||||
super(profile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.luc.crcore.player.event;
|
||||
|
||||
import fr.luc.crcore.player.PlayerProfile;
|
||||
import org.bukkit.event.Event;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base abstraite pour tous les évènements liés à un {@link PlayerProfile}.
|
||||
*
|
||||
* <p>Chaque sous-classe concrète doit fournir sa propre {@code HandlerList}
|
||||
* statique (contrainte Bukkit).
|
||||
*/
|
||||
public abstract class PlayerProfileEvent extends Event {
|
||||
|
||||
private final PlayerProfile profile;
|
||||
|
||||
protected PlayerProfileEvent(PlayerProfile profile) {
|
||||
this.profile = Objects.requireNonNull(profile, "profile");
|
||||
}
|
||||
|
||||
/** Le profil concerné. */
|
||||
public PlayerProfile getProfile() {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package fr.luc.crcore.player.event;
|
||||
|
||||
import fr.luc.crcore.player.PlayerProfile;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Déclenché après changement effectif d'un score joueur. {@link #getScoreName()}
|
||||
* donne le nom du score touché.
|
||||
*/
|
||||
public class PlayerScoreChangeEvent extends PlayerProfileEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final String scoreName;
|
||||
private final int oldValue;
|
||||
private final int newValue;
|
||||
|
||||
public PlayerScoreChangeEvent(PlayerProfile profile, String scoreName, int oldValue, int newValue) {
|
||||
super(profile);
|
||||
this.scoreName = Objects.requireNonNull(scoreName, "scoreName");
|
||||
this.oldValue = oldValue;
|
||||
this.newValue = newValue;
|
||||
}
|
||||
|
||||
public String getScoreName() { return scoreName; }
|
||||
public int getOldValue() { return oldValue; }
|
||||
public int getNewValue() { return newValue; }
|
||||
public int getDelta() { return newValue - oldValue; }
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package fr.luc.crcore.team;
|
||||
|
||||
import fr.luc.crcore.team.event.PlayerJoinTeamEvent;
|
||||
import fr.luc.crcore.team.event.TeamCreateEvent;
|
||||
import fr.luc.crcore.team.event.TeamDissolveEvent;
|
||||
import fr.luc.crcore.team.event.TeamLeadershipTransferEvent;
|
||||
import fr.luc.crcore.team.event.TeamMemberAddEvent;
|
||||
import fr.luc.crcore.team.event.TeamMemberRemoveEvent;
|
||||
import fr.luc.crcore.team.event.TeamScoreChangeEvent;
|
||||
import fr.luc.crcore.team.event.TeamSpawnPointChangeEvent;
|
||||
import fr.luc.crcore.team.event.TeamVisibilityChangeEvent;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Variante de {@link TeamServiceImpl} qui, en plus de la logique métier,
|
||||
* <b>tire des évènements Bukkit</b> via les hooks {@code on...} hérités.
|
||||
*
|
||||
* <p>C'est cette implémentation qu'utilise {@code CRCore} par défaut. Les
|
||||
* plugins de jeu peuvent toujours sous-classer pour ajouter d'autres effets
|
||||
* (logs, scoreboard sync, etc.) en overridant les mêmes hooks et en appelant
|
||||
* {@code super}.
|
||||
*/
|
||||
public class BukkitEventFiringTeamServiceImpl extends TeamServiceImpl {
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
|
||||
public BukkitEventFiringTeamServiceImpl(JavaPlugin plugin, TeamRepository repository) {
|
||||
super(repository);
|
||||
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||
}
|
||||
|
||||
protected JavaPlugin getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAfterCreate(Team team) {
|
||||
Bukkit.getPluginManager().callEvent(new TeamCreateEvent(team));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAfterDissolve(Team team) {
|
||||
Bukkit.getPluginManager().callEvent(new TeamDissolveEvent(team));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMemberAdded(Team team, TeamMember member) {
|
||||
Bukkit.getPluginManager().callEvent(new TeamMemberAddEvent(team, member));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMemberRemoved(Team team, UUID playerId) {
|
||||
Bukkit.getPluginManager().callEvent(new TeamMemberRemoveEvent(team, playerId));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPlayerJoined(Team team, TeamMember member) {
|
||||
Bukkit.getPluginManager().callEvent(new PlayerJoinTeamEvent(team, member));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLeadershipTransferred(Team team, UUID oldLeaderId, UUID newLeaderId) {
|
||||
Bukkit.getPluginManager().callEvent(
|
||||
new TeamLeadershipTransferEvent(team, oldLeaderId, newLeaderId));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onVisibilityChanged(Team team, TeamVisibility oldValue, TeamVisibility newValue) {
|
||||
Bukkit.getPluginManager().callEvent(
|
||||
new TeamVisibilityChangeEvent(team, oldValue, newValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onScoreChanged(Team team, String scoreName, int oldValue, int newValue) {
|
||||
Bukkit.getPluginManager().callEvent(
|
||||
new TeamScoreChangeEvent(team, scoreName, oldValue, newValue));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSpawnPointChanged(Team team, Location oldLocation, Location newLocation) {
|
||||
Bukkit.getPluginManager().callEvent(
|
||||
new TeamSpawnPointChangeEvent(team, oldLocation, newLocation));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package fr.luc.crcore.team;
|
||||
|
||||
import fr.luc.crcore.database.ColumnType;
|
||||
import fr.luc.crcore.database.Database;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Implémentation {@link TeamRepository} adossée à SQLite.
|
||||
*
|
||||
* <p>Stratégie : <b>write-through cache</b>. On hérite de
|
||||
* {@link InMemoryTeamRepository} pour conserver les requêtes rapides
|
||||
* (findAll, findByName, etc. en mémoire), et on overrride {@link #save} et
|
||||
* {@link #delete} pour persister synchroniquement vers SQLite. Le constructeur
|
||||
* crée les tables si nécessaire et recharge l'état complet depuis la DB.
|
||||
*
|
||||
* <p>Schéma (3 tables) :
|
||||
* <ul>
|
||||
* <li>{@code crcore_teams} — une ligne par équipe (champs scalaires)</li>
|
||||
* <li>{@code crcore_team_members} — une ligne par membre</li>
|
||||
* <li>{@code crcore_team_scores} — une ligne par (équipe, score nommé)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Sur {@link #save}, on supprime puis ré-insère membres et scores de
|
||||
* l'équipe (approche simple et robuste pour des volumes faibles d'event).
|
||||
*/
|
||||
public class SqliteTeamRepository extends InMemoryTeamRepository {
|
||||
|
||||
private static final String TABLE_TEAMS = "crcore_teams";
|
||||
private static final String TABLE_MEMBERS = "crcore_team_members";
|
||||
private static final String TABLE_SCORES = "crcore_team_scores";
|
||||
|
||||
private final Database db;
|
||||
|
||||
public SqliteTeamRepository(Database db) {
|
||||
this.db = Objects.requireNonNull(db, "db");
|
||||
ensureSchema();
|
||||
loadAll();
|
||||
}
|
||||
|
||||
private void ensureSchema() {
|
||||
db.table(TABLE_TEAMS).ifNotExists()
|
||||
.column("id", ColumnType.UUID).primaryKey()
|
||||
.column("name", ColumnType.TEXT).notNull().unique()
|
||||
.column("tag", ColumnType.TEXT).notNull().unique()
|
||||
.column("color", ColumnType.TEXT).notNull()
|
||||
.column("leader_id", ColumnType.UUID).notNull()
|
||||
.column("visibility", ColumnType.TEXT).notNull()
|
||||
.column("spawn_world", ColumnType.TEXT)
|
||||
.column("spawn_x", ColumnType.REAL)
|
||||
.column("spawn_y", ColumnType.REAL)
|
||||
.column("spawn_z", ColumnType.REAL)
|
||||
.column("spawn_yaw", ColumnType.REAL)
|
||||
.column("spawn_pitch", ColumnType.REAL)
|
||||
.create();
|
||||
|
||||
db.table(TABLE_MEMBERS).ifNotExists()
|
||||
.column("team_id", ColumnType.UUID).notNull()
|
||||
.column("player_id", ColumnType.UUID).notNull()
|
||||
.column("role", ColumnType.TEXT).notNull()
|
||||
.column("joined_at", ColumnType.INTEGER).notNull()
|
||||
.create();
|
||||
// Index logique (team_id, player_id) — pas créé explicitement, SQLite gère via lookups.
|
||||
|
||||
db.table(TABLE_SCORES).ifNotExists()
|
||||
.column("team_id", ColumnType.UUID).notNull()
|
||||
.column("score_name", ColumnType.TEXT).notNull()
|
||||
.column("value", ColumnType.INTEGER).notNull()
|
||||
.create();
|
||||
}
|
||||
|
||||
/** Recharge tous les Teams depuis la DB dans le cache mémoire hérité. */
|
||||
private void loadAll() {
|
||||
// On query toutes les équipes en flat puis on ré-hydrate.
|
||||
var teamRows = db.query(
|
||||
"SELECT id, name, tag, color, leader_id, visibility, " +
|
||||
"spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch " +
|
||||
"FROM " + TABLE_TEAMS,
|
||||
rs -> new TeamRow(
|
||||
UUID.fromString(rs.getString("id")),
|
||||
rs.getString("name"),
|
||||
rs.getString("tag"),
|
||||
TeamColor.valueOf(rs.getString("color")),
|
||||
UUID.fromString(rs.getString("leader_id")),
|
||||
TeamVisibility.valueOf(rs.getString("visibility")),
|
||||
rs.getString("spawn_world"),
|
||||
(Double) rs.getObject("spawn_x"),
|
||||
(Double) rs.getObject("spawn_y"),
|
||||
(Double) rs.getObject("spawn_z"),
|
||||
(Float) (rs.getObject("spawn_yaw") == null ? null : (float) rs.getDouble("spawn_yaw")),
|
||||
(Float) (rs.getObject("spawn_pitch") == null ? null : (float) rs.getDouble("spawn_pitch"))
|
||||
)
|
||||
);
|
||||
|
||||
for (TeamRow row : teamRows) {
|
||||
Team team = new Team(row.id, row.name, row.tag, row.color, row.leaderId, row.visibility);
|
||||
// Membres
|
||||
var members = db.query(
|
||||
"SELECT player_id, role, joined_at FROM " + TABLE_MEMBERS + " WHERE team_id = ?",
|
||||
rs -> new MemberRow(
|
||||
UUID.fromString(rs.getString("player_id")),
|
||||
TeamRole.valueOf(rs.getString("role")),
|
||||
Instant.ofEpochMilli(rs.getLong("joined_at"))
|
||||
),
|
||||
row.id
|
||||
);
|
||||
// Le leader est ajouté par le constructeur de Team avec role LEADER.
|
||||
// On ajoute les autres membres manuellement via addMember (qui les marque MEMBER).
|
||||
for (MemberRow m : members) {
|
||||
if (!m.playerId.equals(row.leaderId)) {
|
||||
team.addMember(m.playerId);
|
||||
}
|
||||
}
|
||||
// Scores
|
||||
db.query(
|
||||
"SELECT score_name, value FROM " + TABLE_SCORES + " WHERE team_id = ?",
|
||||
rs -> {
|
||||
team.setScore(rs.getString("score_name"), rs.getInt("value"));
|
||||
return null;
|
||||
},
|
||||
row.id
|
||||
);
|
||||
// Spawn point — différé : nécessite un World qui n'est pas forcément chargé
|
||||
// au moment du load. Le serveur charge les worlds avant les plugins normalement,
|
||||
// mais on est défensif.
|
||||
if (row.spawnWorld != null && row.spawnX != null) {
|
||||
var world = org.bukkit.Bukkit.getWorld(row.spawnWorld);
|
||||
if (world != null) {
|
||||
org.bukkit.Location loc = new org.bukkit.Location(
|
||||
world, row.spawnX, row.spawnY, row.spawnZ);
|
||||
if (row.spawnYaw != null) loc.setYaw(row.spawnYaw);
|
||||
if (row.spawnPitch != null) loc.setPitch(row.spawnPitch);
|
||||
team.setSpawnPoint(loc);
|
||||
}
|
||||
}
|
||||
// Inject dans le cache mémoire hérité (super.save persiste à nouveau — on évite ça).
|
||||
super.save(team);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Team save(Team team) {
|
||||
super.save(team); // met à jour le cache mémoire
|
||||
persist(team);
|
||||
return team;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(UUID id) {
|
||||
boolean removed = super.delete(id);
|
||||
if (removed) {
|
||||
db.update("DELETE FROM " + TABLE_SCORES + " WHERE team_id = ?", id);
|
||||
db.update("DELETE FROM " + TABLE_MEMBERS + " WHERE team_id = ?", id);
|
||||
db.update("DELETE FROM " + TABLE_TEAMS + " WHERE id = ?", id);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
private void persist(Team team) {
|
||||
var spawn = team.getSpawnPoint();
|
||||
String spawnWorld = spawn.map(l -> l.getWorld().getName()).orElse(null);
|
||||
Double spawnX = spawn.map(org.bukkit.Location::getX).orElse(null);
|
||||
Double spawnY = spawn.map(org.bukkit.Location::getY).orElse(null);
|
||||
Double spawnZ = spawn.map(org.bukkit.Location::getZ).orElse(null);
|
||||
Float spawnYaw = spawn.map(org.bukkit.Location::getYaw).orElse(null);
|
||||
Float spawnPitch = spawn.map(org.bukkit.Location::getPitch).orElse(null);
|
||||
|
||||
// INSERT OR REPLACE = upsert simple sur SQLite (la PK gère la collision).
|
||||
db.update(
|
||||
"INSERT OR REPLACE INTO " + TABLE_TEAMS +
|
||||
" (id, name, tag, color, leader_id, visibility, " +
|
||||
" spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch) " +
|
||||
" VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
team.getId(), team.getName(), team.getTag(), team.getColor(),
|
||||
team.getLeaderId(), team.getVisibility(),
|
||||
spawnWorld, spawnX, spawnY, spawnZ, spawnYaw, spawnPitch
|
||||
);
|
||||
|
||||
// Approche simple : on remplace en bloc les membres et les scores.
|
||||
db.update("DELETE FROM " + TABLE_MEMBERS + " WHERE team_id = ?", team.getId());
|
||||
for (TeamMember m : team.getMembers()) {
|
||||
db.update(
|
||||
"INSERT INTO " + TABLE_MEMBERS +
|
||||
" (team_id, player_id, role, joined_at) VALUES (?, ?, ?, ?)",
|
||||
team.getId(), m.getPlayerId(), m.getRole(), m.getJoinedAt().toEpochMilli()
|
||||
);
|
||||
}
|
||||
|
||||
db.update("DELETE FROM " + TABLE_SCORES + " WHERE team_id = ?", team.getId());
|
||||
team.getScores().forEach((name, value) ->
|
||||
db.update(
|
||||
"INSERT INTO " + TABLE_SCORES + " (team_id, score_name, value) VALUES (?, ?, ?)",
|
||||
team.getId(), name, value
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Tuples internes pour le load.
|
||||
private record TeamRow(
|
||||
UUID id, String name, String tag, TeamColor color,
|
||||
UUID leaderId, TeamVisibility visibility,
|
||||
String spawnWorld, Double spawnX, Double spawnY, Double spawnZ,
|
||||
Float spawnYaw, Float spawnPitch
|
||||
) {}
|
||||
|
||||
private record MemberRow(UUID playerId, TeamRole role, Instant joinedAt) {}
|
||||
}
|
||||
@@ -14,6 +14,21 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Représente une équipe de joueurs. Aggregate mutable : ajout / retrait de
|
||||
* membres, transfert de leadership, mise à jour de visibilité, des scores
|
||||
* nommés, et du point de spawn passent par les méthodes de cette classe.
|
||||
*
|
||||
* <p>L'identité d'une équipe est son UUID ({@link #getId()}). Deux Team avec
|
||||
* le même UUID sont égales, quel que soit le reste de leur état.
|
||||
*
|
||||
* <p>Implémente {@link Named} (a un nom) et {@link ScoreHolder} (porte des
|
||||
* scores nommés). Hérite de {@link AbstractEntity} pour l'identité.
|
||||
*
|
||||
* <p>Toutes les modifications passent normalement par le {@link TeamService},
|
||||
* qui orchestre persistance + hooks + évènements Bukkit. Modifier une instance
|
||||
* directement (ex. {@code team.addMember(...)}) court-circuite la persistance.
|
||||
*/
|
||||
public class Team extends AbstractEntity implements Named, ScoreHolder {
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -8,6 +8,19 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Façade pour toutes les opérations sur les équipes : lifecycle (create /
|
||||
* dissolve), membres, scores, classements, point de spawn, visibilité.
|
||||
*
|
||||
* <p>Toute logique d'écriture passe par le service (jamais directement sur
|
||||
* {@link Team}) — il garantit l'unicité nom/tag, déclenche les hooks
|
||||
* d'override et tire les évènements Bukkit (via la sous-classe par défaut
|
||||
* {@code BukkitEventFiringTeamServiceImpl}).
|
||||
*
|
||||
* <p>L'implémentation par défaut est {@link TeamServiceImpl} avec ses ~12
|
||||
* hooks {@code protected} surchargeables (factories {@code newTeam},
|
||||
* {@code newRanking}, et hooks {@code on...} autour de chaque opération).
|
||||
*/
|
||||
public interface TeamService {
|
||||
|
||||
// ---- Lifecycle ----
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package fr.luc.crcore.team.event;
|
||||
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamMember;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Déclenché spécifiquement quand un joueur rejoint une équipe <b>par sa propre
|
||||
* action</b> (auto-join sur une équipe PUBLIC via {@code TeamService.joinTeam}).
|
||||
*
|
||||
* <p>Dans ce cas, {@link TeamMemberAddEvent} est <i>aussi</i> tiré juste avant.
|
||||
* Pour réagir uniquement aux auto-joins, écouter ce {@code PlayerJoinTeamEvent}.
|
||||
*/
|
||||
public class PlayerJoinTeamEvent extends TeamEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final TeamMember member;
|
||||
|
||||
public PlayerJoinTeamEvent(Team team, TeamMember member) {
|
||||
super(team);
|
||||
this.member = Objects.requireNonNull(member, "member");
|
||||
}
|
||||
|
||||
public TeamMember getMember() {
|
||||
return member;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.luc.crcore.team.event;
|
||||
|
||||
import fr.luc.crcore.team.Team;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
/**
|
||||
* Déclenché juste après qu'une équipe a été créée et persistée. Non annulable
|
||||
* (la validation est faite côté service avant création).
|
||||
*/
|
||||
public class TeamCreateEvent extends TeamEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
public TeamCreateEvent(Team team) {
|
||||
super(team);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package fr.luc.crcore.team.event;
|
||||
|
||||
import fr.luc.crcore.team.Team;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
/** Déclenché juste après la dissolution d'une équipe. */
|
||||
public class TeamDissolveEvent extends TeamEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
public TeamDissolveEvent(Team team) {
|
||||
super(team);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package fr.luc.crcore.team.event;
|
||||
|
||||
import fr.luc.crcore.team.Team;
|
||||
import org.bukkit.event.Event;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Base abstraite pour tous les évènements Bukkit liés à une équipe. Porte
|
||||
* toujours la {@link Team} concernée.
|
||||
*
|
||||
* <p>Chaque sous-classe concrète doit fournir sa propre {@code HandlerList}
|
||||
* statique (contrainte Bukkit — pas de moyen de partager via héritage).
|
||||
*/
|
||||
public abstract class TeamEvent extends Event {
|
||||
|
||||
private final Team team;
|
||||
|
||||
protected TeamEvent(Team team) {
|
||||
this.team = Objects.requireNonNull(team, "team");
|
||||
}
|
||||
|
||||
/** L'équipe concernée par l'évènement. */
|
||||
public Team getTeam() {
|
||||
return team;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package fr.luc.crcore.team.event;
|
||||
|
||||
import fr.luc.crcore.team.Team;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Déclenché après un transfert de leadership. {@link #getOldLeaderId()} et {@link #getNewLeaderId()} renvoient les UUID des deux joueurs. */
|
||||
public class TeamLeadershipTransferEvent extends TeamEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final UUID oldLeaderId;
|
||||
private final UUID newLeaderId;
|
||||
|
||||
public TeamLeadershipTransferEvent(Team team, UUID oldLeaderId, UUID newLeaderId) {
|
||||
super(team);
|
||||
this.oldLeaderId = Objects.requireNonNull(oldLeaderId, "oldLeaderId");
|
||||
this.newLeaderId = Objects.requireNonNull(newLeaderId, "newLeaderId");
|
||||
}
|
||||
|
||||
public UUID getOldLeaderId() { return oldLeaderId; }
|
||||
public UUID getNewLeaderId() { return newLeaderId; }
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package fr.luc.crcore.team.event;
|
||||
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamMember;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Déclenché quand un membre est ajouté à une équipe — par action du chef
|
||||
* ({@code TeamService.addMember}) OU par auto-join du joueur
|
||||
* ({@code TeamService.joinTeam}).
|
||||
*
|
||||
* <p>Pour distinguer les deux cas, écouter aussi {@link PlayerJoinTeamEvent}
|
||||
* qui n'est tiré que dans le second cas.
|
||||
*/
|
||||
public class TeamMemberAddEvent extends TeamEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final TeamMember member;
|
||||
|
||||
public TeamMemberAddEvent(Team team, TeamMember member) {
|
||||
super(team);
|
||||
this.member = Objects.requireNonNull(member, "member");
|
||||
}
|
||||
|
||||
public TeamMember getMember() {
|
||||
return member;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package fr.luc.crcore.team.event;
|
||||
|
||||
import fr.luc.crcore.team.Team;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Déclenché après le retrait d'un membre d'une équipe (action chef ou départ volontaire). */
|
||||
public class TeamMemberRemoveEvent extends TeamEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final UUID playerId;
|
||||
|
||||
public TeamMemberRemoveEvent(Team team, UUID playerId) {
|
||||
super(team);
|
||||
this.playerId = Objects.requireNonNull(playerId, "playerId");
|
||||
}
|
||||
|
||||
public UUID getPlayerId() {
|
||||
return playerId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package fr.luc.crcore.team.event;
|
||||
|
||||
import fr.luc.crcore.team.Team;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Déclenché après changement effectif d'un score d'équipe (uniquement si la
|
||||
* valeur change). {@link #getScoreName()} donne le nom du score touché.
|
||||
*/
|
||||
public class TeamScoreChangeEvent extends TeamEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final String scoreName;
|
||||
private final int oldValue;
|
||||
private final int newValue;
|
||||
|
||||
public TeamScoreChangeEvent(Team team, String scoreName, int oldValue, int newValue) {
|
||||
super(team);
|
||||
this.scoreName = Objects.requireNonNull(scoreName, "scoreName");
|
||||
this.oldValue = oldValue;
|
||||
this.newValue = newValue;
|
||||
}
|
||||
|
||||
public String getScoreName() { return scoreName; }
|
||||
public int getOldValue() { return oldValue; }
|
||||
public int getNewValue() { return newValue; }
|
||||
public int getDelta() { return newValue - oldValue; }
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package fr.luc.crcore.team.event;
|
||||
|
||||
import fr.luc.crcore.team.Team;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
/**
|
||||
* Déclenché après changement du point de spawn d'une équipe. {@code oldLocation}
|
||||
* et {@code newLocation} peuvent être {@code null} (clear / setup initial).
|
||||
*/
|
||||
public class TeamSpawnPointChangeEvent extends TeamEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final Location oldLocation;
|
||||
private final Location newLocation;
|
||||
|
||||
public TeamSpawnPointChangeEvent(Team team, Location oldLocation, Location newLocation) {
|
||||
super(team);
|
||||
this.oldLocation = oldLocation;
|
||||
this.newLocation = newLocation;
|
||||
}
|
||||
|
||||
/** L'ancien spawn, ou {@code null} s'il n'y en avait pas. */
|
||||
public Location getOldLocation() { return oldLocation; }
|
||||
|
||||
/** Le nouveau spawn, ou {@code null} si on a fait un clear. */
|
||||
public Location getNewLocation() { return newLocation; }
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package fr.luc.crcore.team.event;
|
||||
|
||||
import fr.luc.crcore.team.Team;
|
||||
import fr.luc.crcore.team.TeamVisibility;
|
||||
import org.bukkit.event.HandlerList;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/** Déclenché après changement effectif de visibilité PUBLIC ↔ PRIVATE. */
|
||||
public class TeamVisibilityChangeEvent extends TeamEvent {
|
||||
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final TeamVisibility oldVisibility;
|
||||
private final TeamVisibility newVisibility;
|
||||
|
||||
public TeamVisibilityChangeEvent(Team team, TeamVisibility oldVisibility, TeamVisibility newVisibility) {
|
||||
super(team);
|
||||
this.oldVisibility = Objects.requireNonNull(oldVisibility, "oldVisibility");
|
||||
this.newVisibility = Objects.requireNonNull(newVisibility, "newVisibility");
|
||||
}
|
||||
|
||||
public TeamVisibility getOldVisibility() { return oldVisibility; }
|
||||
public TeamVisibility getNewVisibility() { return newVisibility; }
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user