feat: MessagesService — externalized YAML messages in a single per-plugin file

New module fr.luc.crcore.message exposing:
- MessagesService (interface) with get(key, placeholderPairs...), raw, has,
  set, reload, loadAdditional, color-code toggle.
- YamlMessagesService (impl) that auto-orchestrates everything at construction:
  1. Loads CR-Core defaults from the bundled crcore-messages.yml resource
     (always in memory as fallback for missing keys).
  2. Creates <dataFolder>/<plugin-name-lowercase>-messages.yml at first run
     by copying the plugin's own resource of the same name if it ships one,
     else CR-Core's defaults.
  3. Loads the user file as an override layer on top of the defaults.

Single per-plugin file: the admin only edits one YAML, named after the host
plugin (e.g. mygame-messages.yml). Missing keys transparently fall back to
the bundled CR-Core defaults, so future CR-Core releases adding new keys
work without forcing the admin to edit anything. Plugins can ship their
own version of the file with extra keys + overrides to seed the template.

Placeholder substitution: {name} style via varargs key/value pairs. Color
codes (&a, &c, ...) translated automatically via
ChatColor.translateAlternateColorCodes.

Migration: all 14 default /core team subcommands (create, delete, add,
remove, join, leave, info, list, transfer, setleader, visibility, score,
top, setspawn) now use messages.get(...) instead of hardcoded French
strings. CoreCommand overrides handleResult to render
SUCCESS/FAILURE/INVALID_USAGE/NO_PERMISSION/PLAYER_ONLY through the
service.

CRCore exposes the service via core.messages() / getMessages(); built via
buildMessagesService() (overridable for custom impl).

Bundled defaults: src/main/resources/crcore-messages.yml with the full set
of French defaults, documented inline (placeholders listed per message).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-09 15:58:02 +02:00
parent bcba8363c9
commit f92f22f6c8
20 changed files with 671 additions and 223 deletions
+17 -3
View File
@@ -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; }
}
@@ -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.
*
* <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}.
* du plugin de jeu (ou enregistrée dynamiquement via le {@code CommandMap}
* si elle n'est pas dans le {@code plugin.yml}).
*
* <h2>Override</h2>
* Pour remplacer un groupe entier :
* <pre>{@code
* core.getCoreCommand().replaceSubCommand("team", new MyTeamGroup(svc));
* core.getCoreCommand().replaceSubCommand("team", new MyTeamGroup(svc, msgs));
* }</pre>
* Pour remplacer une feuille :
* <pre>{@code
* core.getCoreCommand().findSubCommand("team")
* .ifPresent(t -> t.replaceSubCommand("create", new MyCreate(svc)));
* .ifPresent(t -> t.replaceSubCommand("create", new MyCreate(svc, msgs)));
* }</pre>
*/
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;
}
}
}
@@ -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 <team> <player>}
*
* <p><b>Admin uniquement</b>. Ajoute un joueur connecté à l'équipe spécifiée,
* quelle que soit sa visibilité.
*/
/** {@code /core team add <team> <player>} — 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()));
}
}
@@ -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 <name> <tag> <color> [visibility] [leader]}
*
* <p><b>Admin uniquement</b>.
*
* <p>Les deux derniers arguments sont optionnels mais <b>positionnels</b> :
* pour passer un {@code leader}, il faut aussi taper la {@code visibility}
* d'abord (PUBLIC ou PRIVATE). Variantes valides :
*
* <ul>
* <li>{@code /core team create Wolves WOLF RED} → PRIVATE, leaderless</li>
* <li>{@code /core team create Wolves WOLF RED PUBLIC} → PUBLIC, leaderless</li>
* <li>{@code /core team create Wolves WOLF RED PRIVATE Alice} → Alice chef, PRIVATE</li>
* <li>{@code /core team create Wolves WOLF RED PUBLIC Alice} → Alice chef, PUBLIC</li>
* </ul>
* <p><b>Admin uniquement</b>. 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());
}
@@ -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 <team>}
*
* <p><b>Admin uniquement</b>. 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 <team>} — 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()));
}
}
@@ -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;
* <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)));
* .ifPresent(team -> team.replaceSubCommand("create", new MyCustomCreate(svc, msgs)));
* }</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;
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));
}
}
@@ -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]}
*
* <p>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();
}
@@ -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 <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).
*/
/** {@code /core team join <team>} — 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());
}
@@ -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}
*
* <p>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()));
}
}
@@ -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}
*
* <p>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<Team> 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();
@@ -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 <team> <player>}
*
* <p><b>Admin uniquement</b>. 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 <team> <player>} — 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()));
}
}
@@ -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 <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}.
*/
/** {@code /core team score <team> <name> <add|set> <value>} — 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)));
}
}
@@ -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 <team> <player>}
*
* <p><b>Admin uniquement</b>. Assigne un joueur comme chef d'une équipe :
* <ul>
* <li>Si l'équipe est leaderless → le joueur devient chef (auto-ajouté
* comme membre s'il ne l'est pas).</li>
* <li>Si l'équipe a déjà un chef → l'ancien chef est démis en simple
* membre, le nouveau prend le rôle.</li>
* <li>Si {@code <player>} n'est pas encore membre → il est auto-ajouté
* à l'équipe en tant que chef.</li>
* </ul>
*
* <p>L'évènement {@code TeamLeadershipTransferEvent} est tiré dans tous les
* cas (avec {@code oldLeaderId} vide si la team était leaderless).
*/
/** {@code /core team setleader <team> <player>} — 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()));
}
}
@@ -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 <team>}
*
* <p><b>Admin uniquement, player-only</b>. 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 <team>} — 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()));
}
}
@@ -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]}
*
* <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.
*/
/** {@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();
@@ -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 <team> <player>}
*
* <p><b>Admin uniquement</b>. Transfère le rôle de chef à un membre existant
* de l'équipe. <i>Strict</i> : le joueur cible doit déjà être membre, et
* l'équipe doit avoir un chef actuel.
*
* <p>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 <team> <player>} — 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()));
}
}
@@ -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 <team> <PUBLIC|PRIVATE>}
*
* <p><b>Admin uniquement</b>. Change la visibilité d'une équipe. PUBLIC permet
* aux joueurs de la rejoindre avec {@code /core team join}.
*/
/** {@code /core team visibility <team> <PUBLIC|PRIVATE>} — 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()));
}
}
@@ -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.
*
* <h2>Modèle de chargement</h2>
*
* 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) :
*
* <ol>
* <li><b>Defaults CR-Core</b> — 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.</li>
* <li><b>Fichier utilisateur</b> — un seul fichier
* {@code <dataFolder>/<plugin-name-lowercase>-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 <i>le</i> fichier que
* l'admin du serveur édite.</li>
* </ol>
*
* <h2>Substitution de placeholders</h2>
*
* Format {@code {name}}, substitution via varargs paire-par-paire :
* <pre>{@code
* core.messages().get("team.create.success",
* "name", team.getName(),
* "tag", team.getTag(),
* "visibility", team.getVisibility());
* }</pre>
*
* <h2>Codes couleur</h2>
*
* Les {@code &a}, {@code &c}, … sont traduits automatiquement en {@code §a},
* {@code §c}, … (toggle via {@link #setApplyColorCodes(boolean)}).
*
* <h2>Clés manquantes</h2>
*
* 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();
}
@@ -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.
*
* <p>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.
*
* <p>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<String, String> defaults = new HashMap<>();
/** Messages effectifs : defaults + fichier user, le user gagne. Reconstruit à chaque reload. */
private final Map<String, String> 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 :
* <ol>
* <li>Charge les defaults CR-Core depuis la ressource embarquée.</li>
* <li>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).</li>
* <li>Charge le fichier user par-dessus les defaults.</li>
* </ol>
*/
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<String, String> 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());
}
}
}
}
+111
View File
@@ -0,0 +1,111 @@
# =============================================================================
# CR-Core — messages par défaut
# -----------------------------------------------------------------------------
# Ce fichier est embarqué dans le jar CR-Core et copié dans :
# <plugin>/<nom-plugin>-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, /<commande-de-ton-plugin> 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}"