diff --git a/src/main/java/fr/luc/crcore/CRCore.java b/src/main/java/fr/luc/crcore/CRCore.java index 865bd27..fe6189c 100644 --- a/src/main/java/fr/luc/crcore/CRCore.java +++ b/src/main/java/fr/luc/crcore/CRCore.java @@ -2,6 +2,8 @@ package fr.luc.crcore; import fr.luc.crcore.command.builtin.CoreCommand; import fr.luc.crcore.database.Database; +import fr.luc.crcore.message.MessagesService; +import fr.luc.crcore.message.YamlMessagesService; import fr.luc.crcore.player.BukkitEventFiringPlayerProfileServiceImpl; import fr.luc.crcore.player.InMemoryPlayerProfileRepository; import fr.luc.crcore.player.PlayerProfileRepository; @@ -77,6 +79,7 @@ public class CRCore { private TeamService teamService; private PlayerProfileRepository playerProfileRepository; private PlayerProfileService playerProfileService; + private MessagesService messages; private CoreCommand coreCommand; private boolean enabled = false; @@ -114,7 +117,9 @@ public class CRCore { this.teamService = buildTeamService(teamRepository); this.playerProfileService = buildPlayerProfileService(playerProfileRepository); - this.coreCommand = buildCoreCommand(teamService, playerProfileService); + this.messages = buildMessagesService(); + + this.coreCommand = buildCoreCommand(teamService, playerProfileService, messages); registerCommand(); registerPlaceholderHook(); @@ -181,8 +186,15 @@ public class CRCore { } /** Construit le {@link CoreCommand}. Override pour ajouter des groupes top-level. */ - protected CoreCommand buildCoreCommand(TeamService teamService, PlayerProfileService playerProfileService) { - return new CoreCommand(teamService, playerProfileService); + protected CoreCommand buildCoreCommand(TeamService teamService, + PlayerProfileService playerProfileService, + MessagesService messages) { + return new CoreCommand(teamService, playerProfileService, messages); + } + + /** Construit le {@link MessagesService}. Override pour utiliser une impl custom. */ + protected MessagesService buildMessagesService() { + return new YamlMessagesService(plugin); } /** @@ -259,6 +271,8 @@ public class CRCore { public TeamService getTeamService() { return teamService; } public PlayerProfileRepository getPlayerProfileRepository() { return playerProfileRepository; } public PlayerProfileService getPlayerProfileService() { return playerProfileService; } + public MessagesService getMessages() { return messages; } + public MessagesService messages() { return messages; } public CoreCommand getCoreCommand() { return coreCommand; } public boolean isEnabled() { return enabled; } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java b/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java index 5492913..b25424a 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/CoreCommand.java @@ -1,9 +1,12 @@ package fr.luc.crcore.command.builtin; import fr.luc.crcore.command.BaseCommand; +import fr.luc.crcore.command.CommandResult; import fr.luc.crcore.command.builtin.team.TeamGroupSubCommand; +import fr.luc.crcore.message.MessagesService; import fr.luc.crcore.player.PlayerProfileService; import fr.luc.crcore.team.TeamService; +import org.bukkit.command.CommandSender; import java.util.Objects; @@ -11,38 +14,71 @@ import java.util.Objects; * Commande racine {@code /core}. Container des groupes par défaut. * *

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

Sans arguments, affiche l'aide des groupes disponibles. Avec {@code team - * }, route vers {@link TeamGroupSubCommand}. + * du plugin de jeu (ou enregistrée dynamiquement via le {@code CommandMap} + * si elle n'est pas dans le {@code plugin.yml}). * *

Override

* Pour remplacer un groupe entier : *
{@code
- * core.getCoreCommand().replaceSubCommand("team", new MyTeamGroup(svc));
+ * core.getCoreCommand().replaceSubCommand("team", new MyTeamGroup(svc, msgs));
  * }
* Pour remplacer une feuille : *
{@code
  * core.getCoreCommand().findSubCommand("team")
- *     .ifPresent(t -> t.replaceSubCommand("create", new MyCreate(svc)));
+ *     .ifPresent(t -> t.replaceSubCommand("create", new MyCreate(svc, msgs)));
  * }
*/ public class CoreCommand extends BaseCommand { protected final TeamService teamService; protected final PlayerProfileService playerProfileService; + protected final MessagesService messages; - public CoreCommand(TeamService teamService, PlayerProfileService playerProfileService) { + public CoreCommand(TeamService teamService, + PlayerProfileService playerProfileService, + MessagesService messages) { super("core"); this.teamService = Objects.requireNonNull(teamService, "teamService"); this.playerProfileService = Objects.requireNonNull(playerProfileService, "playerProfileService"); + this.messages = Objects.requireNonNull(messages, "messages"); 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)); + addSubCommand(new TeamGroupSubCommand(teamService, messages)); + } + + /** + * Override de {@link BaseCommand#handleResult} pour utiliser + * {@link MessagesService} sur les cas génériques (no-permission, + * player-only, etc.) au lieu des strings hardcodés du framework. + */ + @Override + protected void handleResult(CommandSender sender, CommandResult result) { + switch (result.getType()) { + case SUCCESS: + if (result.getMessage() != null) { + sender.sendMessage(result.getMessage()); + } + break; + case FAILURE: + sender.sendMessage(result.getMessage() != null + ? result.getMessage() + : messages.get("common.failure")); + break; + case INVALID_USAGE: + sender.sendMessage(result.getMessage() != null + ? result.getMessage() + : messages.get("common.invalid-usage")); + break; + case NO_PERMISSION: + sender.sendMessage(messages.get("common.no-permission")); + break; + case PLAYER_ONLY: + sender.sendMessage(messages.get("common.player-only")); + break; + } } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamAddSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamAddSubCommand.java index c7cad31..9e669a0 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamAddSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamAddSubCommand.java @@ -4,25 +4,23 @@ 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.message.MessagesService; 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 } - * - *

Admin uniquement. Ajoute un joueur connecté à l'équipe spécifiée, - * quelle que soit sa visibilité. - */ +/** {@code /core team add } — admin uniquement. */ public class TeamAddSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamAddSubCommand(TeamService service) { + public TeamAddSubCommand(TeamService service, MessagesService messages) { super("add"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Ajouter un joueur à une équipe (admin)"); permission("crcore.team.add"); argument("team", TeamArgumentTypes.teamByName(service)); @@ -34,9 +32,12 @@ public class TeamAddSubCommand extends SubCommand { Team team = ctx.get("team"); Player target = ctx.get("player"); if (service.getTeamOfPlayer(target.getUniqueId()).isPresent()) { - return CommandResult.failure(target.getName() + " est déjà dans une équipe."); + return CommandResult.failure(messages.get("team.add.already-in-team", + "player", target.getName())); } service.addMember(team.getId(), target.getUniqueId()); - return CommandResult.success(target.getName() + " ajouté à l'équipe " + team.getName() + "."); + return CommandResult.success(messages.get("team.add.success", + "player", target.getName(), + "name", team.getName())); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java index 3424e07..132dd97 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java @@ -4,6 +4,7 @@ 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.message.MessagesService; import fr.luc.crcore.team.Team; import fr.luc.crcore.team.TeamColor; import fr.luc.crcore.team.TeamException; @@ -18,26 +19,18 @@ import java.util.UUID; /** * {@code /core team create [visibility] [leader]} * - *

Admin uniquement. - * - *

Les deux derniers arguments sont optionnels mais positionnels : - * pour passer un {@code leader}, il faut aussi taper la {@code visibility} - * d'abord (PUBLIC ou PRIVATE). Variantes valides : - * - *

+ *

Admin uniquement. Crée une équipe ; visibilité par défaut PRIVATE, + * chef optionnel (sinon team leaderless). */ public class TeamCreateSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamCreateSubCommand(TeamService service) { + public TeamCreateSubCommand(TeamService service, MessagesService messages) { super("create"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Créer une équipe (admin)"); permission("crcore.team.create"); argument("name", ArgumentTypes.STRING); @@ -60,10 +53,13 @@ public class TeamCreateSubCommand extends SubCommand { try { Team team = service.createTeam(name, tag, color, leaderId, visibility); String chefPart = leaderOpt.isPresent() - ? "chef : " + leaderOpt.get().getName() - : "sans chef"; - return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag() - + "] créée (" + visibility + ", " + chefPart + ")."); + ? messages.get("team.create.with-leader", "leader", leaderOpt.get().getName()) + : messages.get("team.create.no-leader"); + return CommandResult.success(messages.get("team.create.success", + "name", team.getName(), + "tag", team.getTag(), + "visibility", team.getVisibility().name(), + "chef", chefPart)); } catch (TeamException ex) { return CommandResult.failure(ex.getMessage()); } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamDeleteSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamDeleteSubCommand.java index 249ef32..4350d88 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamDeleteSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamDeleteSubCommand.java @@ -3,24 +3,22 @@ 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.message.MessagesService; import fr.luc.crcore.team.Team; import fr.luc.crcore.team.TeamService; import java.util.Objects; -/** - * {@code /core team delete } - * - *

Admin uniquement. Dissout l'équipe spécifiée. Aucun check de chef - * — l'action est gated par la permission {@code crcore.team.delete}. - */ +/** {@code /core team delete } — admin uniquement, dissolution. */ public class TeamDeleteSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamDeleteSubCommand(TeamService service) { + public TeamDeleteSubCommand(TeamService service, MessagesService messages) { super("delete"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Dissoudre une équipe (admin)"); permission("crcore.team.delete"); argument("team", TeamArgumentTypes.teamByName(service)); @@ -30,6 +28,6 @@ public class TeamDeleteSubCommand extends SubCommand { public CommandResult execute(CommandContext ctx) { Team team = ctx.get("team"); service.dissolveTeam(team.getId()); - return CommandResult.success("Équipe " + team.getName() + " dissoute."); + return CommandResult.success(messages.get("team.delete.success", "name", team.getName())); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java index 2fcf268..568d05b 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamGroupSubCommand.java @@ -1,6 +1,7 @@ package fr.luc.crcore.command.builtin.team; import fr.luc.crcore.command.SubCommand; +import fr.luc.crcore.message.MessagesService; import fr.luc.crcore.team.TeamService; import java.util.Objects; @@ -12,19 +13,18 @@ import java.util.Objects; *

Pour overrider une sous-commande, un plugin de jeu fait : *

{@code
  * core.getCoreCommand().findSubCommand("team")
- *     .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc)));
+ *     .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc, msgs)));
  * }
- * - *

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; + protected final MessagesService messages; - public TeamGroupSubCommand(TeamService service) { + public TeamGroupSubCommand(TeamService service, MessagesService messages) { super("team"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Gestion des équipes"); registerDefaults(); } @@ -34,19 +34,19 @@ public class TeamGroupSubCommand extends SubCommand { * 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 TeamSetLeaderSubCommand(service)); - addSubCommand(new TeamVisibilitySubCommand(service)); - addSubCommand(new TeamScoreSubCommand(service)); - addSubCommand(new TeamTopSubCommand(service)); - addSubCommand(new TeamSetSpawnSubCommand(service)); + addSubCommand(new TeamCreateSubCommand(service, messages)); + addSubCommand(new TeamDeleteSubCommand(service, messages)); + addSubCommand(new TeamAddSubCommand(service, messages)); + addSubCommand(new TeamRemoveSubCommand(service, messages)); + addSubCommand(new TeamJoinSubCommand(service, messages)); + addSubCommand(new TeamLeaveSubCommand(service, messages)); + addSubCommand(new TeamInfoSubCommand(service, messages)); + addSubCommand(new TeamListSubCommand(service, messages)); + addSubCommand(new TeamTransferSubCommand(service, messages)); + addSubCommand(new TeamSetLeaderSubCommand(service, messages)); + addSubCommand(new TeamVisibilitySubCommand(service, messages)); + addSubCommand(new TeamScoreSubCommand(service, messages)); + addSubCommand(new TeamTopSubCommand(service, messages)); + addSubCommand(new TeamSetSpawnSubCommand(service, messages)); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamInfoSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamInfoSubCommand.java index 773a482..80b2629 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamInfoSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamInfoSubCommand.java @@ -3,29 +3,25 @@ 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.message.MessagesService; 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]} - * - *

Affiche les infos d'une équipe. Si aucun nom n'est donné, affiche celle - * de l'exécutant. - */ +/** {@code /core team info [team]} — joueur, infos d'une équipe (ou la sienne). */ public class TeamInfoSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamInfoSubCommand(TeamService service) { + public TeamInfoSubCommand(TeamService service, MessagesService messages) { super("info"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Afficher les infos d'une équipe"); permission("crcore.team.info"); optionalArgument("name", TeamArgumentTypes.teamByName(service)); @@ -42,31 +38,36 @@ public class TeamInfoSubCommand extends SubCommand { }); if (team == null) { - return CommandResult.failure("Aucune équipe spécifiée et vous n'êtes pas dans une équipe."); + return CommandResult.failure(messages.get("team.info.not-in-team")); } + String colorCode = team.getColor().getChatColor().toString(); 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(", "))); + sb.append(messages.get("team.info.header", + "color", colorCode, "name", team.getName(), "tag", team.getTag())); + sb.append('\n').append(messages.get("team.info.color", + "color", colorCode, "color_name", team.getColor().getDisplayName())); + sb.append('\n').append(messages.get("team.info.visibility", + "visibility", team.getVisibility().name())); + String memberList = team.getMembers().stream() + .map(m -> { + String name = Bukkit.getOfflinePlayer(m.getPlayerId()).getName(); + return (name != null ? name : m.getPlayerId().toString()) + (m.isLeader() ? "★" : ""); + }) + .collect(Collectors.joining(", ")); + sb.append('\n').append(messages.get("team.info.members", + "count", String.valueOf(team.size()), "list", memberList)); 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())); + String scoreList = team.getScores().entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(", ")); + sb.append('\n').append(messages.get("team.info.scores", "list", scoreList)); } + team.getSpawnPoint().ifPresent(loc -> sb.append('\n').append(messages.get("team.info.spawn", + "world", loc.getWorld().getName(), + "x", String.valueOf((int) loc.getX()), + "y", String.valueOf((int) loc.getY()), + "z", String.valueOf((int) loc.getZ())))); ctx.reply(sb.toString()); return CommandResult.success(); } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamJoinSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamJoinSubCommand.java index f0a0493..27a1cef 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamJoinSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamJoinSubCommand.java @@ -3,6 +3,7 @@ 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.message.MessagesService; import fr.luc.crcore.team.Team; import fr.luc.crcore.team.TeamException; import fr.luc.crcore.team.TeamService; @@ -10,20 +11,16 @@ import org.bukkit.entity.Player; import java.util.Objects; -/** - * {@code /core team join } - * - *

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). - */ +/** {@code /core team join } — joueur, auto-join sur team PUBLIC. */ public class TeamJoinSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamJoinSubCommand(TeamService service) { + public TeamJoinSubCommand(TeamService service, MessagesService messages) { super("join"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Rejoindre une équipe publique"); permission("crcore.team.join"); playerOnly(); @@ -36,7 +33,7 @@ public class TeamJoinSubCommand extends SubCommand { Team team = ctx.get("name"); try { service.joinTeam(team.getId(), player.getUniqueId()); - return CommandResult.success("Vous avez rejoint " + team.getName() + "."); + return CommandResult.success(messages.get("team.join.success", "name", team.getName())); } catch (TeamException ex) { return CommandResult.failure(ex.getMessage()); } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamLeaveSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamLeaveSubCommand.java index 605c232..eb7f2a7 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamLeaveSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamLeaveSubCommand.java @@ -3,25 +3,23 @@ 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.message.MessagesService; 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} - * - *

Le joueur quitte volontairement son équipe. Refusé pour le chef (il doit - * d'abord transférer le leadership ou dissoudre l'équipe). - */ +/** {@code /core team leave} — joueur, refuse pour le chef. */ public class TeamLeaveSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamLeaveSubCommand(TeamService service) { + public TeamLeaveSubCommand(TeamService service, MessagesService messages) { super("leave"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Quitter son équipe"); permission("crcore.team.leave"); playerOnly(); @@ -32,13 +30,12 @@ public class TeamLeaveSubCommand extends SubCommand { Player player = ctx.requirePlayer(); Team team = service.getTeamOfPlayer(player.getUniqueId()).orElse(null); if (team == null) { - return CommandResult.failure("Vous n'appartenez à aucune équipe."); + return CommandResult.failure(messages.get("team.not-in-team")); } if (team.isLeader(player.getUniqueId())) { - return CommandResult.failure( - "Vous êtes le chef. Demandez à un admin de réassigner le leadership (/core team setleader) ou de dissoudre l'équipe."); + return CommandResult.failure(messages.get("team.leave.is-leader")); } service.removeMember(team.getId(), player.getUniqueId()); - return CommandResult.success("Vous avez quitté l'équipe " + team.getName() + "."); + return CommandResult.success(messages.get("team.leave.success", "name", team.getName())); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamListSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamListSubCommand.java index 611a708..8200636 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamListSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamListSubCommand.java @@ -3,26 +3,23 @@ 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.message.MessagesService; 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} - * - *

Affiche toutes les équipes existantes avec leur tag, nom, taille et - * visibilité. - */ +/** {@code /core team list} — joueur, liste toutes les équipes. */ public class TeamListSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamListSubCommand(TeamService service) { + public TeamListSubCommand(TeamService service, MessagesService messages) { super("list"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Lister toutes les équipes"); permission("crcore.team.list"); } @@ -31,16 +28,17 @@ public class TeamListSubCommand extends SubCommand { public CommandResult execute(CommandContext ctx) { Collection teams = service.getAllTeams(); if (teams.isEmpty()) { - return CommandResult.success("Aucune équipe pour le moment."); + return CommandResult.success(messages.get("team.list.empty")); } - StringBuilder sb = new StringBuilder(ChatColor.YELLOW + "Équipes (" + teams.size() + ") :"); + StringBuilder sb = new StringBuilder(messages.get("team.list.header", + "count", String.valueOf(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(")"); + sb.append('\n').append(messages.get("team.list.entry", + "color", team.getColor().getChatColor().toString(), + "tag", team.getTag(), + "name", team.getName(), + "size", String.valueOf(team.size()), + "visibility", team.getVisibility().name())); } ctx.reply(sb.toString()); return CommandResult.success(); diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamRemoveSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamRemoveSubCommand.java index c95608e..41af6d9 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamRemoveSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamRemoveSubCommand.java @@ -4,6 +4,7 @@ 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.message.MessagesService; import fr.luc.crcore.team.Team; import fr.luc.crcore.team.TeamService; import org.bukkit.Bukkit; @@ -11,20 +12,16 @@ import org.bukkit.OfflinePlayer; import java.util.Objects; -/** - * {@code /core team remove } - * - *

Admin uniquement. Retire un joueur (online ou offline) de - * l'équipe spécifiée. Refuse si le joueur ciblé est le chef — l'admin doit - * d'abord transférer ou réassigner via {@code setleader}. - */ +/** {@code /core team remove } — admin uniquement. */ public class TeamRemoveSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamRemoveSubCommand(TeamService service) { + public TeamRemoveSubCommand(TeamService service, MessagesService messages) { super("remove"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Retirer un joueur d'une équipe (admin)"); permission("crcore.team.remove"); argument("team", TeamArgumentTypes.teamByName(service)); @@ -39,13 +36,14 @@ public class TeamRemoveSubCommand extends SubCommand { @SuppressWarnings("deprecation") OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); if (!team.hasMember(target.getUniqueId())) { - return CommandResult.failure(targetName + " n'est pas dans l'équipe " + team.getName() + "."); + return CommandResult.failure(messages.get("team.remove.not-member", + "player", targetName, "name", team.getName())); } if (team.isLeader(target.getUniqueId())) { - return CommandResult.failure( - "Impossible de retirer le chef. Réassignez-le d'abord via /core team setleader."); + return CommandResult.failure(messages.get("team.remove.is-leader")); } service.removeMember(team.getId(), target.getUniqueId()); - return CommandResult.success(targetName + " retiré de l'équipe " + team.getName() + "."); + return CommandResult.success(messages.get("team.remove.success", + "player", targetName, "name", team.getName())); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamScoreSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamScoreSubCommand.java index 8336f1e..565cf7b 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamScoreSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamScoreSubCommand.java @@ -4,29 +4,24 @@ 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.message.MessagesService; import fr.luc.crcore.team.Team; import fr.luc.crcore.team.TeamService; import java.util.Objects; -/** - * {@code /core team score } - * - *

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

Restreinte par défaut à la permission {@code crcore.team.score.modify}. - */ +/** {@code /core team score } — admin, debug/fix. */ public class TeamScoreSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamScoreSubCommand(TeamService service) { + public TeamScoreSubCommand(TeamService service, MessagesService messages) { super("score"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("[Admin] Modifier le score d'une équipe"); - permission("crcore.team.score.modify"); + permission("crcore.team.score"); argument("team", TeamArgumentTypes.teamByName(service)); argument("name", ArgumentTypes.STRING); argument("op", ArgumentTypes.choice("add", "set")); @@ -48,6 +43,7 @@ public class TeamScoreSubCommand extends SubCommand { } else { throw new IllegalStateException("unreachable: " + op); } - return CommandResult.success("Score " + name + " de " + team.getName() + " = " + result); + return CommandResult.success(messages.get("team.score.success", + "score", name, "name", team.getName(), "value", String.valueOf(result))); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetLeaderSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetLeaderSubCommand.java index c6d53ba..6bc4ca8 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetLeaderSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetLeaderSubCommand.java @@ -4,35 +4,23 @@ 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.message.MessagesService; import fr.luc.crcore.team.Team; import fr.luc.crcore.team.TeamService; import org.bukkit.entity.Player; import java.util.Objects; -/** - * {@code /core team setleader } - * - *

Admin uniquement. Assigne un joueur comme chef d'une équipe : - *

- * - *

L'évènement {@code TeamLeadershipTransferEvent} est tiré dans tous les - * cas (avec {@code oldLeaderId} vide si la team était leaderless). - */ +/** {@code /core team setleader } — admin, plus permissif que transfer. */ public class TeamSetLeaderSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamSetLeaderSubCommand(TeamService service) { + public TeamSetLeaderSubCommand(TeamService service, MessagesService messages) { super("setleader"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Assigner / changer le chef d'une équipe (admin)"); permission("crcore.team.setleader"); argument("team", TeamArgumentTypes.teamByName(service)); @@ -46,8 +34,10 @@ public class TeamSetLeaderSubCommand extends SubCommand { boolean changed = service.setLeader(team.getId(), target.getUniqueId()); if (!changed) { - return CommandResult.success(target.getName() + " est déjà chef de " + team.getName() + "."); + return CommandResult.success(messages.get("team.setleader.already-leader", + "player", target.getName(), "name", team.getName())); } - return CommandResult.success(target.getName() + " est désormais chef de " + team.getName() + "."); + return CommandResult.success(messages.get("team.setleader.success", + "player", target.getName(), "name", team.getName())); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetSpawnSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetSpawnSubCommand.java index 39d5611..ba41fac 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetSpawnSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamSetSpawnSubCommand.java @@ -3,26 +3,23 @@ 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.message.MessagesService; 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 } - * - *

Admin uniquement, player-only. Définit le point de spawn de - * l'équipe spécifiée à la position courante de l'admin (où il/elle se trouve). - * Doit être lancé en jeu — la console n'a pas de location. - */ +/** {@code /core team setspawn } — admin, player-only (position de l'admin). */ public class TeamSetSpawnSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamSetSpawnSubCommand(TeamService service) { + public TeamSetSpawnSubCommand(TeamService service, MessagesService messages) { super("setspawn"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Définir le point de spawn d'une équipe (admin, en jeu)"); permission("crcore.team.setspawn"); playerOnly(); @@ -34,6 +31,6 @@ public class TeamSetSpawnSubCommand extends SubCommand { Player admin = ctx.requirePlayer(); Team team = ctx.get("team"); service.setSpawnPoint(team.getId(), admin.getLocation()); - return CommandResult.success("Spawn de " + team.getName() + " défini à votre position."); + return CommandResult.success(messages.get("team.setspawn.success", "name", team.getName())); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamTopSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamTopSubCommand.java index 67b2808..c453a14 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamTopSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamTopSubCommand.java @@ -4,32 +4,28 @@ 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.message.MessagesService; 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]} - * - *

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. - */ +/** {@code /core team top [score]} — joueur, classement (par score précis ou global). */ public class TeamTopSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; protected final int limit; - public TeamTopSubCommand(TeamService service) { - this(service, 10); + public TeamTopSubCommand(TeamService service, MessagesService messages) { + this(service, messages, 10); } - public TeamTopSubCommand(TeamService service, int limit) { + public TeamTopSubCommand(TeamService service, MessagesService messages, int limit) { super("top"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); this.limit = limit; description("Classement des équipes"); permission("crcore.team.top"); @@ -44,14 +40,18 @@ public class TeamTopSubCommand extends SubCommand { : service.getTopRankingByScore(scoreName, limit); if (ranking.isEmpty()) { - return CommandResult.success("Aucune équipe à classer."); + return CommandResult.success(messages.get("team.top.empty")); } - StringBuilder sb = new StringBuilder(ChatColor.YELLOW + "Top " + ranking.size() + - (scoreName == null ? " (global) :" : " (" + scoreName + ") :")); + StringBuilder sb = new StringBuilder(scoreName == null + ? messages.get("team.top.header-global", "count", String.valueOf(ranking.size())) + : messages.get("team.top.header-score", + "count", String.valueOf(ranking.size()), "score", 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()); + sb.append('\n').append(messages.get("team.top.entry", + "rank", String.valueOf(r.rank()), + "color", r.team().getColor().getChatColor().toString(), + "name", r.team().getName(), + "value", String.valueOf(r.score()))); } ctx.reply(sb.toString()); return CommandResult.success(); diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamTransferSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamTransferSubCommand.java index cb6c43e..37e2222 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamTransferSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamTransferSubCommand.java @@ -4,6 +4,7 @@ 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.message.MessagesService; import fr.luc.crcore.team.Team; import fr.luc.crcore.team.TeamService; import org.bukkit.Bukkit; @@ -11,24 +12,16 @@ import org.bukkit.OfflinePlayer; import java.util.Objects; -/** - * {@code /core team transfer } - * - *

Admin uniquement. Transfère le rôle de chef à un membre existant - * de l'équipe. Strict : le joueur cible doit déjà être membre, et - * l'équipe doit avoir un chef actuel. - * - *

Pour un cas plus permissif (assigner un chef sur une équipe leaderless, - * ou auto-ajouter un non-membre comme chef), utiliser - * {@code /core team setleader}. - */ +/** {@code /core team transfer } — admin, transfert strict chef→membre existant. */ public class TeamTransferSubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamTransferSubCommand(TeamService service) { + public TeamTransferSubCommand(TeamService service, MessagesService messages) { super("transfer"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Transférer le rôle de chef à un membre (admin, strict)"); permission("crcore.team.transfer"); argument("team", TeamArgumentTypes.teamByName(service)); @@ -43,15 +36,15 @@ public class TeamTransferSubCommand extends SubCommand { @SuppressWarnings("deprecation") OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); if (!team.hasMember(target.getUniqueId())) { - return CommandResult.failure( - targetName + " n'est pas membre de " + team.getName() + - " — utilisez /core team setleader pour un cas plus général."); + return CommandResult.failure(messages.get("team.transfer.not-member", + "player", targetName, "name", team.getName())); } try { service.transferLeadership(team.getId(), target.getUniqueId()); } catch (IllegalStateException ex) { return CommandResult.failure(ex.getMessage()); } - return CommandResult.success(targetName + " est désormais chef de " + team.getName() + "."); + return CommandResult.success(messages.get("team.transfer.success", + "player", targetName, "name", team.getName())); } } diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamVisibilitySubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamVisibilitySubCommand.java index 96eb1a1..229e8d9 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamVisibilitySubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamVisibilitySubCommand.java @@ -4,25 +4,23 @@ 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.message.MessagesService; import fr.luc.crcore.team.Team; import fr.luc.crcore.team.TeamService; import fr.luc.crcore.team.TeamVisibility; import java.util.Objects; -/** - * {@code /core team visibility } - * - *

Admin uniquement. Change la visibilité d'une équipe. PUBLIC permet - * aux joueurs de la rejoindre avec {@code /core team join}. - */ +/** {@code /core team visibility } — admin. */ public class TeamVisibilitySubCommand extends SubCommand { protected final TeamService service; + protected final MessagesService messages; - public TeamVisibilitySubCommand(TeamService service) { + public TeamVisibilitySubCommand(TeamService service, MessagesService messages) { super("visibility"); this.service = Objects.requireNonNull(service, "service"); + this.messages = Objects.requireNonNull(messages, "messages"); description("Changer la visibilité d'une équipe (admin)"); permission("crcore.team.visibility"); argument("team", TeamArgumentTypes.teamByName(service)); @@ -34,6 +32,7 @@ public class TeamVisibilitySubCommand extends SubCommand { Team team = ctx.get("team"); TeamVisibility visibility = ctx.get("visibility"); service.setVisibility(team.getId(), visibility); - return CommandResult.success("Visibilité de " + team.getName() + " réglée sur " + visibility + "."); + return CommandResult.success(messages.get("team.visibility.success", + "name", team.getName(), "visibility", visibility.name())); } } diff --git a/src/main/java/fr/luc/crcore/message/MessagesService.java b/src/main/java/fr/luc/crcore/message/MessagesService.java new file mode 100644 index 0000000..900e2a4 --- /dev/null +++ b/src/main/java/fr/luc/crcore/message/MessagesService.java @@ -0,0 +1,91 @@ +package fr.luc.crcore.message; + +import java.io.File; + +/** + * Service de messages localisables / configurables pour CR-Core et les + * plugins de jeu downstream. + * + *

Modèle de chargement

+ * + * Une instance par {@code CRCore}. Au boot, deux couches sont chargées dans + * cet ordre (la deuxième écrase la première sur les mêmes clés) : + * + *
    + *
  1. Defaults CR-Core — embarqués dans le jar + * ({@code resources/crcore-messages.yml}), toujours en mémoire en + * fallback. Une clé manquante dans le fichier user retombe ici + * automatiquement.
  2. + *
  3. Fichier utilisateur — un seul fichier + * {@code /-messages.yml}. + * Auto-créé au premier démarrage à partir d'une copie des defaults + * (CR-Core OU du plugin de jeu s'il bundle son propre fichier sous + * le même nom dans ses ressources). C'est le fichier que + * l'admin du serveur édite.
  4. + *
+ * + *

Substitution de placeholders

+ * + * Format {@code {name}}, substitution via varargs paire-par-paire : + *
{@code
+ * core.messages().get("team.create.success",
+ *     "name", team.getName(),
+ *     "tag", team.getTag(),
+ *     "visibility", team.getVisibility());
+ * }
+ * + *

Codes couleur

+ * + * Les {@code &a}, {@code &c}, … sont traduits automatiquement en {@code §a}, + * {@code §c}, … (toggle via {@link #setApplyColorCodes(boolean)}). + * + *

Clés manquantes

+ * + * Si une clé n'existe ni dans le fichier user ni dans les defaults, + * {@link #get} renvoie {@code "[missing: key]"} pour faciliter le debug. + */ +public interface MessagesService { + + /** + * Récupère un message formaté. + * + * @param key clé du message (ex. {@code "team.create.success"}) + * @param placeholderPairs paires {@code (nom, valeur, nom, valeur, …)}. + * Le nombre d'éléments DOIT être pair. + */ + String get(String key, Object... placeholderPairs); + + /** Template brut sans substitution ni traduction couleur. */ + String raw(String key); + + /** {@code true} si la clé existe (dans le fichier user ou dans les defaults). */ + boolean has(String key); + + /** + * Définit / écrase un message ponctuellement en mémoire. Utile pour des + * messages dynamiques ou injectés par un plugin de jeu. NON persisté + * dans le fichier user. + */ + void set(String key, String template); + + /** + * Recharge le fichier utilisateur depuis le disque. Les defaults CR-Core + * restent en mémoire (pas re-chargés depuis le jar — ils ne bougent pas). + */ + void reload(); + + /** + * Charge un fichier YAML supplémentaire en plus du fichier user principal. + * Cas d'usage : un plugin de jeu qui veut séparer ses messages en plusieurs + * fichiers (ex. un par module). Le fichier est résolu dans le dataFolder + * du plugin et copié depuis ses ressources s'il n'existe pas encore. + */ + void loadAdditional(String resourceName); + + void setApplyColorCodes(boolean enabled); + + boolean isApplyColorCodes(); + + /** Chemin du fichier user principal (informationnel). */ + File getUserFile(); +} diff --git a/src/main/java/fr/luc/crcore/message/YamlMessagesService.java b/src/main/java/fr/luc/crcore/message/YamlMessagesService.java new file mode 100644 index 0000000..6b0d6c3 --- /dev/null +++ b/src/main/java/fr/luc/crcore/message/YamlMessagesService.java @@ -0,0 +1,235 @@ +package fr.luc.crcore.message; + +import org.bukkit.ChatColor; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Implémentation par défaut de {@link MessagesService}, basée sur les + * {@link YamlConfiguration} de Bukkit. + * + *

Constructeur auto-orchestré : charge les defaults CR-Core en mémoire, + * crée le fichier utilisateur à partir d'un template (bundlé par le plugin + * de jeu sous le même nom, ou défaut CR-Core), et le charge en couche + * d'override. + * + *

Voir {@link MessagesService} pour le détail du modèle. + */ +public class YamlMessagesService implements MessagesService { + + /** Nom de la ressource embarquée dans le jar CR-Core (et shadée dans le plugin de jeu). */ + private static final String CRCORE_DEFAULTS_RESOURCE = "crcore-messages.yml"; + + private static final String MISSING_PREFIX = "[missing: "; + private static final String MISSING_SUFFIX = "]"; + + private final JavaPlugin plugin; + private final Logger logger; + /** Defaults immuables chargés depuis la ressource CR-Core (lookup en dernier recours). */ + private final Map defaults = new HashMap<>(); + /** Messages effectifs : defaults + fichier user, le user gagne. Reconstruit à chaque reload. */ + private final Map messages = new HashMap<>(); + /** Nom du fichier user (auto-dérivé du nom du plugin). */ + private final String userFileName; + private final File userFile; + private boolean applyColorCodes = true; + + public YamlMessagesService(JavaPlugin plugin) { + this.plugin = Objects.requireNonNull(plugin, "plugin"); + this.logger = plugin.getLogger(); + this.userFileName = plugin.getName().toLowerCase() + "-messages.yml"; + this.userFile = new File(plugin.getDataFolder(), userFileName); + initialize(); + } + + /** + * Orchestration au démarrage : + *

    + *
  1. Charge les defaults CR-Core depuis la ressource embarquée.
  2. + *
  3. Crée le fichier user s'il manque (template = ressource du plugin + * de jeu sous le même nom si présente, sinon defaults CR-Core).
  4. + *
  5. Charge le fichier user par-dessus les defaults.
  6. + *
+ */ + private void initialize() { + loadDefaultsFromResource(); + ensureUserFile(); + rebuildEffectiveMessages(); + } + + private void loadDefaultsFromResource() { + try (InputStream is = plugin.getResource(CRCORE_DEFAULTS_RESOURCE)) { + if (is == null) { + logger.warning("Ressource " + CRCORE_DEFAULTS_RESOURCE + + " introuvable dans le jar — defaults CR-Core indisponibles."); + return; + } + YamlConfiguration cfg = YamlConfiguration.loadConfiguration( + new InputStreamReader(is, StandardCharsets.UTF_8)); + flatten(cfg, "", defaults); + } catch (IOException ex) { + logger.log(Level.WARNING, "Échec lecture defaults CR-Core", ex); + } + } + + private void ensureUserFile() { + if (userFile.exists()) return; + if (!plugin.getDataFolder().exists() && !plugin.getDataFolder().mkdirs()) { + logger.warning("Impossible de créer " + plugin.getDataFolder()); + return; + } + // Priorité 1 : ressource du plugin de jeu sous le même nom (le plugin + // bundle son propre template avec ses overrides + ses messages perso). + try (InputStream pluginResource = plugin.getResource(userFileName)) { + if (pluginResource != null) { + copyStreamToFile(pluginResource, userFile); + logger.info("Fichier messages créé depuis le template du plugin : " + userFileName); + return; + } + } catch (IOException ex) { + logger.log(Level.WARNING, "Échec copie du template plugin", ex); + } + // Priorité 2 : copie les defaults CR-Core comme starter. + try (InputStream coreResource = plugin.getResource(CRCORE_DEFAULTS_RESOURCE)) { + if (coreResource != null) { + copyStreamToFile(coreResource, userFile); + logger.info("Fichier messages créé depuis les defaults CR-Core : " + userFileName); + } + } catch (IOException ex) { + logger.log(Level.WARNING, "Échec copie des defaults CR-Core", ex); + } + } + + private static void copyStreamToFile(InputStream in, File target) throws IOException { + try (FileOutputStream out = new FileOutputStream(target)) { + in.transferTo(out); + } + } + + /** Recompose la map effective : defaults d'abord, fichier user par-dessus. */ + private void rebuildEffectiveMessages() { + messages.clear(); + messages.putAll(defaults); + if (userFile.exists()) { + try { + YamlConfiguration cfg = YamlConfiguration.loadConfiguration(userFile); + flatten(cfg, "", messages); + } catch (Exception ex) { + logger.log(Level.WARNING, "Échec chargement " + userFile, ex); + } + } + } + + // ---- MessagesService API ---- + + @Override + public String get(String key, Object... placeholderPairs) { + Objects.requireNonNull(key, "key"); + String template = raw(key); + if (placeholderPairs != null && placeholderPairs.length > 0) { + if (placeholderPairs.length % 2 != 0) { + throw new IllegalArgumentException( + "placeholderPairs must have even length, got " + placeholderPairs.length); + } + for (int i = 0; i < placeholderPairs.length; i += 2) { + String placeholderKey = String.valueOf(placeholderPairs[i]); + String placeholderValue = String.valueOf(placeholderPairs[i + 1]); + template = template.replace("{" + placeholderKey + "}", placeholderValue); + } + } + return applyColorCodes + ? ChatColor.translateAlternateColorCodes('&', template) + : template; + } + + @Override + public String raw(String key) { + Objects.requireNonNull(key, "key"); + String template = messages.get(key); + return template != null ? template : MISSING_PREFIX + key + MISSING_SUFFIX; + } + + @Override + public boolean has(String key) { + return messages.containsKey(key); + } + + @Override + public void set(String key, String template) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(template, "template"); + messages.put(key, template); + } + + @Override + public void reload() { + rebuildEffectiveMessages(); + } + + @Override + public void loadAdditional(String resourceName) { + Objects.requireNonNull(resourceName, "resourceName"); + File extraFile = new File(plugin.getDataFolder(), resourceName); + if (!extraFile.exists()) { + try (InputStream is = plugin.getResource(resourceName)) { + if (is == null) { + logger.warning("Ressource additionnelle " + resourceName + " introuvable."); + return; + } + if (!plugin.getDataFolder().exists()) Files.createDirectories(plugin.getDataFolder().toPath()); + copyStreamToFile(is, extraFile); + } catch (IOException ex) { + logger.log(Level.WARNING, "Échec copie de " + resourceName, ex); + return; + } + } + try { + YamlConfiguration cfg = YamlConfiguration.loadConfiguration(extraFile); + flatten(cfg, "", messages); + } catch (Exception ex) { + logger.log(Level.WARNING, "Échec chargement " + extraFile, ex); + } + } + + @Override + public void setApplyColorCodes(boolean enabled) { + this.applyColorCodes = enabled; + } + + @Override + public boolean isApplyColorCodes() { + return applyColorCodes; + } + + @Override + public File getUserFile() { + return userFile; + } + + /** Parcourt récursivement une section et pousse chaque feuille en clé plate. */ + private static void flatten(ConfigurationSection section, String prefix, Map out) { + for (String key : section.getKeys(false)) { + Object value = section.get(key); + String fullKey = prefix.isEmpty() ? key : prefix + "." + key; + if (value instanceof ConfigurationSection) { + flatten((ConfigurationSection) value, fullKey, out); + } else if (value != null) { + out.put(fullKey, value.toString()); + } + } + } +} diff --git a/src/main/resources/crcore-messages.yml b/src/main/resources/crcore-messages.yml new file mode 100644 index 0000000..d611ab2 --- /dev/null +++ b/src/main/resources/crcore-messages.yml @@ -0,0 +1,111 @@ +# ============================================================================= +# CR-Core — messages par défaut +# ----------------------------------------------------------------------------- +# Ce fichier est embarqué dans le jar CR-Core et copié dans : +# /-messages.yml +# au premier démarrage. C'est CE fichier (la copie en dataFolder) que tu édites. +# +# Codes couleur : utilise '&' (ex. &a vert, &c rouge, &7 gris, &f blanc). +# Placeholders nommés : {name}, {tag}, {visibility}, etc. +# Les noms disponibles sont listés en commentaire devant +# chaque message. +# +# Après modif, / reload-messages (à brancher côté +# plugin de jeu si tu veux du hot reload sans restart). +# ============================================================================= + +common: + no-permission: "&cVous n'avez pas la permission." + player-only: "&cSeul un joueur peut utiliser cette commande." + failure: "&cÉchec de la commande." + invalid-usage: "&cUsage incorrect." + +team: + # Placeholders : {name} + not-found: "&cAucune équipe trouvée : {name}" + not-in-team: "&cVous n'appartenez à aucune équipe." + + create: + # Placeholders : {name}, {tag}, {visibility}, {chef} + success: "&aÉquipe &f{name} &7[#{tag}]&a créée &7({visibility}, {chef})&a." + # Placeholders : {leader} + with-leader: "chef : {leader}" + no-leader: "sans chef" + + delete: + # Placeholders : {name} + success: "&aÉquipe &f{name}&a dissoute." + + add: + # Placeholders : {player} + already-in-team: "&c{player} est déjà dans une équipe." + # Placeholders : {player}, {name} + success: "&a{player} ajouté à l'équipe &f{name}&a." + + remove: + # Placeholders : {player}, {name} + not-member: "&c{player} n'est pas dans l'équipe {name}." + is-leader: "&cImpossible de retirer le chef. Réassignez-le d'abord via /core team setleader." + success: "&a{player} retiré de l'équipe &f{name}&a." + + join: + # Placeholders : {name} + success: "&aVous avez rejoint &f{name}&a." + + leave: + is-leader: "&cVous êtes le chef. Demandez à un admin de réassigner le leadership ou de dissoudre l'équipe." + # Placeholders : {name} + success: "&aVous avez quitté l'équipe &f{name}&a." + + transfer: + # Placeholders : {player}, {name} + not-member: "&c{player} n'est pas membre de {name} — utilisez /core team setleader pour un cas plus général." + success: "&a{player} est désormais chef de &f{name}&a." + + setleader: + # Placeholders : {player}, {name} + success: "&a{player} est désormais chef de &f{name}&a." + already-leader: "&7{player} est déjà chef de {name}." + + visibility: + # Placeholders : {name}, {visibility} + success: "&aVisibilité de &f{name}&a réglée sur &f{visibility}&a." + + setspawn: + # Placeholders : {name} + success: "&aSpawn de &f{name}&a défini à votre position." + + score: + # Placeholders : {score}, {name}, {value} + success: "&aScore &f{score}&a de &f{name}&a = &f{value}" + + info: + not-in-team: "&cAucune équipe spécifiée et vous n'êtes pas dans une équipe." + # Placeholders : {color}, {name}, {tag} + header: "{color}=== {name} [#{tag}] ===" + # Placeholders : {color}, {color_name} + color: "&7Couleur : {color}{color_name}" + # Placeholders : {visibility} + visibility: "&7Visibilité : &f{visibility}" + # Placeholders : {count}, {list} + members: "&7Membres ({count}) : &f{list}" + # Placeholders : {list} + scores: "&7Scores : &f{list}" + # Placeholders : {world}, {x}, {y}, {z} + spawn: "&7Spawn : &f{world} {x}/{y}/{z}" + + list: + empty: "&7Aucune équipe pour le moment." + # Placeholders : {count} + header: "&eÉquipes ({count}) :" + # Placeholders : {color}, {tag}, {name}, {size}, {visibility} + entry: "&7 - {color}[{tag}] {name} &7({size} membres, {visibility})" + + top: + empty: "&7Aucune équipe à classer." + # Placeholders : {count} + header-global: "&eTop {count} (global) :" + # Placeholders : {count}, {score} + header-score: "&eTop {count} ({score}) :" + # Placeholders : {rank}, {color}, {name}, {value} + entry: "&7 {rank}. {color}{name} &7— &f{value}"