diff --git a/docs/decisions.md b/docs/decisions.md index 53c9683..304c3ba 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -323,6 +323,30 @@ Format léger : une décision = un titre + contexte + choix + raison. même fichier SQLite (par défaut `/crcore.db`) ; le préfixe isole proprement. +## 2026-06-09 — Bascule Java 16 → Java 11 (révision) + +- **Révision** de la décision "Java 16" du 2026-06-08. +- **Choix** : `maven.compiler.source/target = 11`. Le code se compile et + s'exécute sur tout JDK 11+. +- **Raison** : Java 11 reste très répandu côté serveurs Bukkit/Paper 1.16.5, + et le coût de revenir en arrière est faible. On garde une cible plus + conservatrice pour maximiser la compatibilité d'exécution. +- **Conséquences sur le code** : + - Les `record` (Java 16) → classes immutables manuelles, avec mêmes noms + d'accesseurs (`rank()`, `team()`, etc.) pour ne pas casser l'API + publique. Concerné : `TeamRanking`, `PlayerRanking`, plus deux tuples + internes (`TeamRow`, `MemberRow`) dans `SqliteTeamRepository`. + - Le **pattern matching `instanceof X x`** (Java 16) → classique + `instanceof X` + cast explicite. Concerné : `CommandContext.requirePlayer`, + `Database.normalize`. + - Les **switch expressions à flèche** (`case X -> ...`, Java 14) → + `switch (...) { case X: ...; break; }` classique, ou chaînes if/else. + Concerné : `BaseCommand.handleResult`, `ArgumentTypes.BOOLEAN.parse`, + `TeamScoreSubCommand.execute`. +- **Ce qui reste utilisé de Java 11** : `var` (Java 10+), `List.of()` / + `Map.of()` (Java 9+), interfaces avec méthodes `default`, lambdas, method + references. + ## 2026-06-09 — Enregistrement dynamique de la commande (plugin.yml optionnel) - **Choix** : `CRCore.registerCommand()` tente d'abord diff --git a/docs/features.md b/docs/features.md index f485566..d9878a5 100644 --- a/docs/features.md +++ b/docs/features.md @@ -111,7 +111,7 @@ Le service expose deux types de classements : | `getTopRankingByScore(name, n)` | Top N par score. | | `getTopGlobalRanking(n)` | Top N global. | -Le résultat est une `List` (record Java 16) avec `rank` (1-based), +Le résultat est une `List` (classe immutable, accesseurs `rank()`/`team()`/`score()`) avec `rank` (1-based), `team` et `score`. Tiebreaker : ordre alphabétique sur le nom de l'équipe (insensible à la casse). @@ -228,7 +228,7 @@ les méthodes de scoring (`getScore`, `addScore`, `setScore`, `resetScore`, ### `PlayerRanking` -Record Java 16 : `record PlayerRanking(int rank, PlayerProfile profile, int score)`. +Classe immutable : `PlayerRanking(int rank, PlayerProfile profile, int score)` avec accesseurs `rank()`/`profile()`/`score()`. Mêmes règles que `TeamRanking` (rank 1-based, tri descendant, tiebreaker par UUID pour rester déterministe). diff --git a/docs/setup.md b/docs/setup.md index b2ce415..ef2a023 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -4,7 +4,7 @@ - **Type** : librairie Java (`jar`) — pas un plugin Bukkit - **artifactId Maven** : `CR-Core` -- **Build** : Maven, Java 16 +- **Build** : Maven, Java 11 - **API serveur (provided)** : Paper 1.16.5 - **SQLite (compile)** : `org.xerial:sqlite-jdbc:3.45.3.0` - **Package racine** : `fr.luc.crcore` diff --git a/pom.xml b/pom.xml index 1fe7164..81b9cb5 100644 --- a/pom.xml +++ b/pom.xml @@ -13,8 +13,13 @@ Reusable core library for CR Minecraft game plugins (teams, players, scores, commands, events, SQLite persistence). - 16 - 16 + + 11 UTF-8 3.45.3.0 @@ -83,8 +88,7 @@ maven-compiler-plugin 3.11.0 - ${maven.compiler.source} - ${maven.compiler.target} + ${maven.compiler.release} @@ -121,7 +125,7 @@ maven-javadoc-plugin 3.6.3 - ${maven.compiler.source} + ${maven.compiler.release} none true UTF-8 diff --git a/src/main/java/fr/luc/crcore/command/ArgumentTypes.java b/src/main/java/fr/luc/crcore/command/ArgumentTypes.java index 965e650..11a88be 100644 --- a/src/main/java/fr/luc/crcore/command/ArgumentTypes.java +++ b/src/main/java/fr/luc/crcore/command/ArgumentTypes.java @@ -46,11 +46,14 @@ public final class ArgumentTypes { public static final ArgumentType BOOLEAN = new ArgumentType<>() { @Override public Boolean parse(String input) { - return switch (input.toLowerCase()) { - case "true", "yes", "y", "1", "on" -> true; - case "false", "no", "n", "0", "off" -> false; - default -> throw new CommandException("Invalid boolean: " + input); - }; + String lc = input.toLowerCase(); + if (lc.equals("true") || lc.equals("yes") || lc.equals("y") || lc.equals("1") || lc.equals("on")) { + return true; + } + if (lc.equals("false") || lc.equals("no") || lc.equals("n") || lc.equals("0") || lc.equals("off")) { + return false; + } + throw new CommandException("Invalid boolean: " + input); } @Override diff --git a/src/main/java/fr/luc/crcore/command/BaseCommand.java b/src/main/java/fr/luc/crcore/command/BaseCommand.java index ff94e72..4146460 100644 --- a/src/main/java/fr/luc/crcore/command/BaseCommand.java +++ b/src/main/java/fr/luc/crcore/command/BaseCommand.java @@ -48,17 +48,25 @@ public abstract class BaseCommand extends AbstractCommand */ protected void handleResult(CommandSender sender, CommandResult result) { switch (result.getType()) { - case SUCCESS -> { + case SUCCESS: if (result.getMessage() != null) { sender.sendMessage(ChatColor.GREEN + result.getMessage()); } - } - case FAILURE -> sender.sendMessage(ChatColor.RED + - (result.getMessage() != null ? result.getMessage() : "Command failed.")); - case INVALID_USAGE -> sender.sendMessage(ChatColor.RED + - (result.getMessage() != null ? result.getMessage() : "Invalid usage.")); - case NO_PERMISSION -> sender.sendMessage(ChatColor.RED + "Vous n'avez pas la permission."); - case PLAYER_ONLY -> sender.sendMessage(ChatColor.RED + "Seul un joueur peut utiliser cette commande."); + break; + case FAILURE: + sender.sendMessage(ChatColor.RED + + (result.getMessage() != null ? result.getMessage() : "Command failed.")); + break; + case INVALID_USAGE: + sender.sendMessage(ChatColor.RED + + (result.getMessage() != null ? result.getMessage() : "Invalid usage.")); + break; + case NO_PERMISSION: + sender.sendMessage(ChatColor.RED + "Vous n'avez pas la permission."); + break; + case PLAYER_ONLY: + sender.sendMessage(ChatColor.RED + "Seul un joueur peut utiliser cette commande."); + break; } } } diff --git a/src/main/java/fr/luc/crcore/command/CommandContext.java b/src/main/java/fr/luc/crcore/command/CommandContext.java index 7ac7dfa..7fb65a7 100644 --- a/src/main/java/fr/luc/crcore/command/CommandContext.java +++ b/src/main/java/fr/luc/crcore/command/CommandContext.java @@ -45,10 +45,10 @@ public class CommandContext { } public Player requirePlayer() { - if (!(sender instanceof Player player)) { + if (!(sender instanceof Player)) { throw new CommandException("This command can only be used by a player."); } - return player; + return (Player) sender; } @SuppressWarnings("unchecked") 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 7b5d956..8336f1e 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 @@ -40,11 +40,14 @@ public class TeamScoreSubCommand extends SubCommand { String op = ctx.get("op"); int value = ctx.get("value"); - int result = switch (op) { - case "add" -> service.addScore(team.getId(), name, value); - case "set" -> service.setScore(team.getId(), name, value); - default -> throw new IllegalStateException("unreachable: " + op); - }; + int result; + if ("add".equals(op)) { + result = service.addScore(team.getId(), name, value); + } else if ("set".equals(op)) { + result = service.setScore(team.getId(), name, value); + } else { + throw new IllegalStateException("unreachable: " + op); + } return CommandResult.success("Score " + name + " de " + team.getName() + " = " + result); } } diff --git a/src/main/java/fr/luc/crcore/database/Database.java b/src/main/java/fr/luc/crcore/database/Database.java index 456f356..f057d6a 100644 --- a/src/main/java/fr/luc/crcore/database/Database.java +++ b/src/main/java/fr/luc/crcore/database/Database.java @@ -199,9 +199,9 @@ public class Database implements AutoCloseable { /** Convertit les valeurs Java non-natives SQL en types utilisables par JDBC. */ private static Object normalize(Object value) { if (value == null) return null; - if (value instanceof UUID uuid) return uuid.toString(); - if (value instanceof Enum e) return e.name(); - if (value instanceof Boolean b) return b ? 1 : 0; + if (value instanceof UUID) return value.toString(); + if (value instanceof Enum) return ((Enum) value).name(); + if (value instanceof Boolean) return ((Boolean) value) ? 1 : 0; return value; } } diff --git a/src/main/java/fr/luc/crcore/player/PlayerRanking.java b/src/main/java/fr/luc/crcore/player/PlayerRanking.java index 01e0e23..a89129f 100644 --- a/src/main/java/fr/luc/crcore/player/PlayerRanking.java +++ b/src/main/java/fr/luc/crcore/player/PlayerRanking.java @@ -2,12 +2,56 @@ package fr.luc.crcore.player; import java.util.Objects; -public record PlayerRanking(int rank, PlayerProfile profile, int score) { +/** + * Entrée d'un classement de joueurs : rang (1-based), profil, score effectif + * sur le critère trié. + * + *

Classe immutable. Volontairement écrite "à la main" plutôt qu'en + * {@code record} (Java 16+) pour rester compatible Java 11. + */ +public final class PlayerRanking { - public PlayerRanking { + private final int rank; + private final PlayerProfile profile; + private final int score; + + public PlayerRanking(int rank, PlayerProfile profile, int score) { Objects.requireNonNull(profile, "profile"); if (rank < 1) { throw new IllegalArgumentException("rank must be >= 1, got " + rank); } + this.rank = rank; + this.profile = profile; + this.score = score; + } + + public int rank() { + return rank; + } + + public PlayerProfile profile() { + return profile; + } + + public int score() { + return score; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof PlayerRanking)) return false; + PlayerRanking that = (PlayerRanking) other; + return rank == that.rank && score == that.score && profile.equals(that.profile); + } + + @Override + public int hashCode() { + return Objects.hash(rank, profile, score); + } + + @Override + public String toString() { + return "PlayerRanking[rank=" + rank + ", playerId=" + profile.getId() + ", score=" + score + "]"; } } diff --git a/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java b/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java index 0f7093f..0359003 100644 --- a/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java +++ b/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java @@ -197,13 +197,49 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { ); } - // Tuples internes pour le load. - private record TeamRow( - UUID id, String name, String tag, TeamColor color, - UUID leaderId, TeamVisibility visibility, - String spawnWorld, Double spawnX, Double spawnY, Double spawnZ, - Float spawnYaw, Float spawnPitch - ) {} + // Tuples internes pour le load. Classes immutables manuelles (Java 11 compat). + private static final class TeamRow { + final UUID id; + final String name; + final String tag; + final TeamColor color; + final UUID leaderId; + final TeamVisibility visibility; + final String spawnWorld; + final Double spawnX; + final Double spawnY; + final Double spawnZ; + final Float spawnYaw; + final Float spawnPitch; - private record MemberRow(UUID playerId, TeamRole role, Instant joinedAt) {} + TeamRow(UUID id, String name, String tag, TeamColor color, + UUID leaderId, TeamVisibility visibility, + String spawnWorld, Double spawnX, Double spawnY, Double spawnZ, + Float spawnYaw, Float spawnPitch) { + this.id = id; + this.name = name; + this.tag = tag; + this.color = color; + this.leaderId = leaderId; + this.visibility = visibility; + this.spawnWorld = spawnWorld; + this.spawnX = spawnX; + this.spawnY = spawnY; + this.spawnZ = spawnZ; + this.spawnYaw = spawnYaw; + this.spawnPitch = spawnPitch; + } + } + + private static final class MemberRow { + final UUID playerId; + final TeamRole role; + final Instant joinedAt; + + MemberRow(UUID playerId, TeamRole role, Instant joinedAt) { + this.playerId = playerId; + this.role = role; + this.joinedAt = joinedAt; + } + } } diff --git a/src/main/java/fr/luc/crcore/team/TeamRanking.java b/src/main/java/fr/luc/crcore/team/TeamRanking.java index 85a2884..5a250bf 100644 --- a/src/main/java/fr/luc/crcore/team/TeamRanking.java +++ b/src/main/java/fr/luc/crcore/team/TeamRanking.java @@ -2,12 +2,56 @@ package fr.luc.crcore.team; import java.util.Objects; -public record TeamRanking(int rank, Team team, int score) { +/** + * Entrée d'un classement d'équipes : rang (1-based), équipe, score effectif + * sur le critère trié. + * + *

Classe immutable. Volontairement écrite "à la main" plutôt qu'en + * {@code record} (Java 16+) pour rester compatible Java 11. + */ +public final class TeamRanking { - public TeamRanking { + private final int rank; + private final Team team; + private final int score; + + public TeamRanking(int rank, Team team, int score) { Objects.requireNonNull(team, "team"); if (rank < 1) { throw new IllegalArgumentException("rank must be >= 1, got " + rank); } + this.rank = rank; + this.team = team; + this.score = score; + } + + public int rank() { + return rank; + } + + public Team team() { + return team; + } + + public int score() { + return score; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof TeamRanking)) return false; + TeamRanking that = (TeamRanking) other; + return rank == that.rank && score == that.score && team.equals(that.team); + } + + @Override + public int hashCode() { + return Objects.hash(rank, team, score); + } + + @Override + public String toString() { + return "TeamRanking[rank=" + rank + ", team=" + team.getName() + ", score=" + score + "]"; } }