chore: downgrade compile target to Java 11

Uses <release>11</release> in maven-compiler-plugin (recommended over
source/target to guarantee bytecode and API surface match Java 11).

Code changes to drop Java 12-16 features:
- records (TeamRanking, PlayerRanking, internal tuples in
  SqliteTeamRepository) become hand-written immutable classes; same
  accessor names (rank()/team()/score()/...) so call sites are unchanged.
- instanceof X x pattern matching becomes classic instanceof + cast in
  CommandContext.requirePlayer and Database.normalize.
- switch expressions with -> arrows become classic switch + break, or
  if/else chains, in BaseCommand.handleResult, ArgumentTypes.BOOLEAN and
  TeamScoreSubCommand.execute.

docs/setup.md, features.md and decisions.md updated to reflect Java 11.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-09 12:18:43 +02:00
parent 7ee349f206
commit 5bd6e227d3
12 changed files with 209 additions and 43 deletions
+24
View File
@@ -323,6 +323,30 @@ Format léger : une décision = un titre + contexte + choix + raison.
même fichier SQLite (par défaut `<dataFolder>/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
+2 -2
View File
@@ -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<TeamRanking>` (record Java 16) avec `rank` (1-based),
Le résultat est une `List<TeamRanking>` (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).
+1 -1
View File
@@ -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`
+9 -5
View File
@@ -13,8 +13,13 @@
<description>Reusable core library for CR Minecraft game plugins (teams, players, scores, commands, events, SQLite persistence).</description>
<properties>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
<!--
On utilise <release> plutôt que <source>+<target> pour garantir
que les références à des API ne sont pas accidentellement plus
récentes que Java 11 (ex. List.copyOf en Java 10+ : OK ;
Stream.toList en Java 16+ : refusé par javac).
-->
<maven.compiler.release>11</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<sqlite.version>3.45.3.0</sqlite.version>
</properties>
@@ -83,8 +88,7 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<release>${maven.compiler.release}</release>
</configuration>
</plugin>
@@ -121,7 +125,7 @@
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.3</version>
<configuration>
<source>${maven.compiler.source}</source>
<source>${maven.compiler.release}</source>
<doclint>none</doclint>
<quiet>true</quiet>
<encoding>UTF-8</encoding>
@@ -46,11 +46,14 @@ public final class ArgumentTypes {
public static final ArgumentType<Boolean> 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
@@ -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;
}
}
}
@@ -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")
@@ -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);
}
}
@@ -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;
}
}
@@ -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é.
*
* <p>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 + "]";
}
}
@@ -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;
}
}
}
@@ -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é.
*
* <p>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 + "]";
}
}