Minecraft ne compte plus en 1.x. La dernière version s'appelle 26.2 et elle est sortie le 16 juin 2026. Pour qui maintient un plugin écrit pour 1.21, la question n'est pas seulement technique, elle est stratégique. Migrer coûte du temps, et viser la mauvaise version coûte des installations. Ce guide dit exactement quoi changer dans le code, dans quel ordre, et surtout quelle version cibler, parce que la bonne réponse n'est pas la plus récente.
Ce que le versionnage calendaire change vraiment
Le numéro de version encode maintenant l'année et le rang de sortie dans l'année. 26.2 veut dire deuxième version de 2026, rien de plus. Le numéro ne te dit plus si le changement est mineur ou structurel, et c'est le premier réflexe à perdre : tu ne peux plus déduire l'ampleur d'une migration du saut de numéro. Il faut regarder ce qui a bougé sous le capot.
Sous le capot, trois choses ont changé et elles comptent toutes les trois. Minecraft est désobfusqué depuis 26.1, ce qui a fait disparaître le remapper de Paper. Paper 26.x embarque Adventure 5, qui a supprimé des API auparavant seulement dépréciées. Et Java 25 est devenu le plancher d'exécution des versions calendaires. Un plugin 1.21 qui touche à ces trois zones ne se contente pas de recompiler.
Le chiffre que peu de monde regarde avant de migrer
Quelle version viser vraiment
Il y a un piège de plus. Paper 26.2 n'a pas de build stable : la cible de production raisonnable sur la branche calendaire est 26.1.2, pas 26.2. Livrer un plugin qui n'existe qu'en 26.2, c'est donc demander à tes utilisateurs de tourner sur une base que Paper lui-même ne déclare pas stable.
| Part du parc | Verdict | |
|---|---|---|
| 1.21.11 | 36,4 % | Cible principale, la majorité de tes installations |
| 26.1.2 | 14,9 % | Cible calendaire de production, celle à tester |
| 26.2 | 7,4 % | Pas de build stable Paper, ne pas livrer pour elle seule |
| 1.21.4 | 7,2 % | Compatible sans effort si tu restes sur l'API publique |
Ma lecture, assumée : abandonner 1.21 aujourd'hui serait une erreur. Ce n'est pas de la nostalgie, c'est de l'arithmétique. Tu renoncerais à une famille majoritaire pour gagner une branche qui pèse deux fois moins, et dont la version la plus récente n'a même pas de build stable. La bonne cible est un plugin unique qui compile contre 1.21, se charge sur 26.x, et n'utilise rien qui ait disparu entre les deux.
"On ne migre pas vers la dernière version, on migre vers la version que font tourner les gens qui installent le plugin."
La version d'API et la version de Java
Deux réglages distincts, souvent confondus. La version d'API est la génération de l'API Bukkit contre laquelle ton code est écrit, déclarée dans plugin.yml. La version de Java est le format de bytecode produit par le compilateur. Les deux se règlent indépendamment, et se tromper sur la seconde est la cause de refus de chargement la plus banale.
<properties>
<!-- 21, pas 25 : le bytecode Java 21 se charge sur un runtime plus recent.
L'inverse est faux. -->
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21.11-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
Compile contre la version la plus ancienne que tu veux supporter, pas la plus récente. Un plugin compilé contre l'API 26.x et installé sur un serveur 1.21 plante dès qu'il appelle une méthode qui n'existe pas encore là-bas. Dans l'autre sens, un plugin compilé contre 1.21 tourne sur 26.x tant qu'il n'utilise pas d'API supprimée. C'est asymétrique, et cette asymétrie est ton amie. Les coordonnées exactes de chaque branche se vérifient sur docs.papermc.io, et les signatures sur jd.papermc.io.
Java 25 est un plancher d'exécution, pas une cible de compilation
UnsupportedClassVersionError à la clé. Reste en 21, sauf si tu as besoin d'une fonctionnalité de langage précise, ce qui est rare dans un plugin.Adventure 5 : le texte, c'est la zone qui casse
C'est le point qui fait le plus de dégâts. Paper 26.x embarque Adventure 5, et Adventure 5 a supprimé des API qui n'étaient jusque-là que dépréciées. La zone touchée est prévisible : tout ce qui concerne les messages et le texte affiché aux joueurs. Un plugin 1.21 qui envoie ses messages à l'ancienne peut tout simplement ne plus compiler.
La bonne nouvelle : la sortie est unique et elle marche dans les deux mondes. Adventure est inclus dans Paper depuis longtemps, donc du code écrit avec Component compile aussi bien sur 1.21 que sur 26.x. Tu ne fais pas deux versions, tu modernises une fois.
package fun.example.migration;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
public final class Messages {
private static final MiniMessage MINI = MiniMessage.miniMessage();
private Messages() {
}
/** Message construit dans le code, sans aucune chaine de couleur. */
public static Component welcome(String playerName) {
return Component.text("Bienvenue ", NamedTextColor.GOLD)
.append(Component.text(playerName, NamedTextColor.WHITE))
.append(Component.text(" sur le serveur.", NamedTextColor.GRAY));
}
/** Meme principe pour une valeur numerique, sans concatenation coloree. */
public static Component balance(long amount) {
return Component.text("Solde : ", NamedTextColor.GRAY)
.append(Component.text(amount, NamedTextColor.GOLD));
}
/** Ligne ecrite par l'admin dans config.yml, au format MiniMessage. */
public static Component fromConfig(String line) {
return MINI.deserialize(line);
}
/**
* Chaine heritee d'un ancien config.yml, du type "&aTexte vert".
* On la convertit au lieu de la jeter : les fichiers des utilisateurs
* existent deja et personne ne les reecrira a la main.
*/
public static Component fromLegacy(String stored) {
return LegacyComponentSerializer.legacyAmpersand().deserialize(stored);
}
}
Côté appel, plus rien à inventer : player.sendMessage(Messages.welcome(player.getName())). La documentation Adventure vit sur docs.advntr.dev, l'intégration côté Paper sur docs.papermc.io.
Ce qui ne marche pas : le rechercher-remplacer
config.yml, et tous les serveurs qui avaient personnalisé leurs messages les perdent au premier démarrage. Deux : tu laisses passer les endroits qui ne cassent pas la compilation mais changent d'apparence en jeu, comme les noms d'items, les titres d'inventaires et les lignes de scoreboard. Fais l'inventaire de ces trois surfaces à la main avant de toucher au code.La disparition du remapper
Minecraft est désobfusqué depuis 26.1, et le remapper de Paper a disparu avec l'obfuscation. Si ton plugin restait sagement sur l'API publique Bukkit et Paper, tu ne verras absolument rien. Si en revanche il descendait dans les classes internes du serveur, la migration te concerne de plein fouet : l'étape de remapping sur laquelle reposait ton build n'existe plus, et les noms sur lesquels tu réfléchissais ne sont plus les mêmes.
Mon conseil est brutal et je l'assume : profite de la migration pour sortir des internes. Une réflexion sur des classes internes est le genre de code qui te réclamera une correction à chaque version, calendaire ou pas. La plupart du temps, l'API publique fait désormais la même chose. Quand ce n'est pas le cas, isole cet accès dans une seule classe, avec un repli propre si la classe attendue est absente, plutôt que de le laisser éparpillé dans le plugin.
Ce que la désobfuscation apporte
Plus d'étape de remapping dans le build
Piles d'appels lisibles dans les rapports d'erreur
Code interne lisible quand il faut comprendre un comportement
Ce qu'elle casse
Les builds qui dépendaient du remapper ne passent plus
La réflexion sur les anciens noms obfusqués échoue
Les tutoriels NMS d'avant 26.1 sont périmés
Le bloc libraries au lieu du shading
Le réflexe historique consistait à embarquer ses dépendances dans le .jar, autrement dit à les shader. Le bloc libraries: de plugin.yml remplace cette pratique : tu déclares la dépendance, et le serveur la télécharge lui-même. Le .jar redevient petit et ne contient que ton code.
Avant de déclarer quoi que ce soit, vérifie ce qui est déjà là. Le pilote JDBC SQLite est déjà fourni avec Paper : si tu l'embarquais, tu ajoutais du poids pour rien. HikariCP, en revanche, n'est pas fourni. C'est exactement le genre de dépendance à mettre dans libraries:.
name: MonPlugin
version: 1.0.0
main: fun.example.migration.MonPlugin
api-version: '1.21'
description: Plugin migre, un seul .jar pour 1.21 et 26.x
authors: [ MonPseudo ]
# Telecharge par le serveur au premier demarrage.
# Le pilote SQLite n'est PAS ici : Paper le fournit deja.
libraries:
- com.zaxxer:HikariCP:5.1.0
commands:
solde:
description: Affiche ton solde
usage: /solde
La valeur d'api-version déclare la génération d'API contre laquelle ton code est écrit. Garde-la sur la plus ancienne génération que tu supportes réellement : c'est ce qui te maintient chargeable sur toute la famille 1.21 tout en restant accepté par les serveurs plus récents. Le format exact du fichier est documenté sur docs.papermc.io et, pour la partie historique Bukkit, sur hub.spigotmc.org.
Ce qui ne marche pas : croire que libraries est gratuit
Le piège UUID du mode hors ligne
Voici la partie que presque aucun guide de migration ne traite, et c'est pourtant celle qui détruit le plus de données. 75,1 % des serveurs tournent en mode hors ligne. En mode hors ligne, l'UUID d'un joueur est dérivé de son pseudo. Ce n'est pas un détail d'implémentation, c'est une propriété qui change la conception de ta base : si le joueur change de pseudo, ou si le serveur passe un jour en mode en ligne, la clé change et toutes les données stockées sur l'ancien UUID deviennent orphelines.
Concrètement, un plugin d'économie qui stocke un solde sur l'UUID sans rien d'autre offre ce scénario à son utilisateur : le joueur renommé perd son argent, l'administrateur ne comprend pas, et personne ne sait reconstituer la correspondance. Le correctif tient en une colonne : garde le pseudo à côté de l'UUID, indexe-le, et tu conserves un chemin de réconciliation.
CREATE TABLE IF NOT EXISTS player_data (
uuid TEXT PRIMARY KEY,
last_name TEXT NOT NULL,
balance INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);
-- Indispensable en mode hors ligne : c'est le seul chemin de retour
-- quand un joueur change de pseudo ou quand le serveur bascule en ligne.
CREATE INDEX IF NOT EXISTS idx_player_data_name ON player_data(last_name);
package fun.example.migration;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
public final class PlayerRepository {
private final Connection connection;
public PlayerRepository(Connection connection) {
this.connection = connection;
}
/**
* Ecrit toujours le pseudo courant en meme temps que la donnee.
* Sans cette ligne, un renommage rend l'enregistrement introuvable
* sur les 75,1 % de serveurs en mode hors ligne.
*/
public void save(UUID uuid, String name, long balance) throws SQLException {
String sql = "INSERT INTO player_data(uuid, last_name, balance, updated_at) "
+ "VALUES(?, ?, ?, ?) "
+ "ON CONFLICT(uuid) DO UPDATE SET "
+ "last_name = excluded.last_name, "
+ "balance = excluded.balance, "
+ "updated_at = excluded.updated_at";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, uuid.toString());
ps.setString(2, name);
ps.setLong(3, balance);
ps.setLong(4, System.currentTimeMillis());
ps.executeUpdate();
}
}
public long readBalance(UUID uuid) throws SQLException {
String sql = "SELECT balance FROM player_data WHERE uuid = ?";
try (PreparedStatement ps = connection.prepareStatement(sql)) {
ps.setString(1, uuid.toString());
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getLong(1) : 0L;
}
}
}
}
Le test que je fais systématiquement
Folia : le thread principal n'est plus garanti
Folia exécute les régions du monde sur plusieurs threads. Un plugin qui suppose un thread principal unique ne ralentit pas, il fait crasher. La migration consiste à passer par le RegionScheduler pour toucher au monde, et à ne jamais faire d'accès base de données sur un thread de jeu, sous peine de voir le TPS s'effondrer.
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import java.sql.SQLException;
import java.util.UUID;
public final class BalanceLookup {
private final Plugin plugin;
private final PlayerRepository repository;
public BalanceLookup(Plugin plugin, PlayerRepository repository) {
this.plugin = plugin;
this.repository = repository;
}
public void showBalance(UUID uuid, Location location) {
// 1. La base de donnees, hors de tout thread de jeu.
Bukkit.getAsyncScheduler().runNow(plugin, task -> {
final long balance;
try {
balance = repository.readBalance(uuid);
} catch (SQLException e) {
plugin.getLogger().warning("Lecture du solde impossible : " + e.getMessage());
return;
}
// 2. Le retour au monde, sur le thread qui possede cette region.
Bukkit.getRegionScheduler().execute(plugin, location, () -> {
Player player = Bukkit.getPlayer(uuid);
if (player != null) {
player.sendMessage(Messages.balance(balance));
}
});
});
}
}
Ce code n'est pas réservé à Folia : Paper expose ces schedulers, tu peux donc l'écrire dès maintenant sur ta cible 1.21 et rester compatible sans double base de code. Les détails de comportement de Folia sont sur docs.papermc.io, et si tu veux voir le résultat sans monter l'environnement, notre générateur de plugin Folia produit directement du code au RegionScheduler.
Mon arbitrage, en clair
Un panorama neutre n'aide personne, alors voilà ce que je recommande, avec les seuils.
Un seul .jar pour la grande majorité des cas. Compilé en Java 21, contre l'API 1.21, zéro accès aux internes, Adventure partout, schedulers compatibles Folia. Ce plugin couvre la famille 1.21 majoritaire et se charge sur 26.x. Deux builds séparés ne se justifient que si tu touches vraiment aux classes internes du serveur.
Folia, sur un serveur modeste, je ne migre pas. Le RegionScheduler complique la lecture du code pour un gain que tu ne mesureras pas quand une seule région porte tout le monde. Sur un serveur très fréquenté, ou sur une map très étalée où les joueurs sont dispersés, l'investissement devient rentable. Écris quand même le code compatible dès le départ : il ne coûte rien sur Paper et t'évite une réécriture le jour où tu changes d'échelle.
Base de données : SQLite tant que tu as un seul serveur. Le pilote est déjà fourni, une seule connexion suffit, et tu n'as strictement rien à déclarer. Le jour où plusieurs serveurs doivent partager les mêmes données, tu passes à MySQL, et c'est là seulement que HikariCP entre dans le bloc libraries:. Un pool de connexions sur un fichier SQLite local est de la complexité pure.
26.2 : je n'y livre rien. Tant qu'il n'y a pas de build Paper stable, la cible calendaire est 26.1.2. Tu peux tester sur 26.2 par curiosité, tu ne construis pas ta compatibilité dessus.
Faire la migration sans relire ligne par ligne
Décris ton plugin et la version visée, Minax écrit du code Adventure, compile un .jar propre et te laisse le tester en ligne avant de l'installer.
Migrer mon pluginLa checklist de migration, dans l'ordre
- Fixe la cible : compilation contre 1.21, chargement vérifié sur 26.1.2.
- Vérifie que
maven.compiler.releasevaut 21, pas 25. - Recense tous les endroits qui produisent du texte : messages, noms d'items, titres d'inventaires, scoreboards.
- Passe ces endroits en
Component, et ajoute une conversion des chaînes héritées du config.yml. - Cherche tout accès aux classes internes du serveur et supprime-le, ou isole-le dans une seule classe.
- Retire du .jar tout ce que Paper fournit déjà, en commençant par le pilote JDBC SQLite.
- Déclare le reste dans
libraries:au lieu de le shader. - Sors tous les accès base de données des threads de jeu.
- Remplace les hypothèses de thread principal unique par le RegionScheduler.
- Ajoute la colonne de pseudo à côté de l'UUID, et fais le test du renommage en mode hors ligne.
- Installe le .jar sur un vrai serveur, une fois en 1.21 et une fois en 26.1.2, avant de publier.
Si une étape casse, lis la première ligne d'erreur et rien d'autre : notre guide de dépannage des plugins couvre les messages que produit exactement ce genre de migration.
Conclusion
Le versionnage calendaire a rendu la question de la cible plus visible, pas plus simple. Le travail réel de migration tient en quatre chantiers : le texte via Adventure, la sortie des internes, les dépendances déclarées plutôt qu'embarquées, et l'abandon de l'hypothèse du thread unique. Le reste est du réglage. Et la décision la plus importante n'est pas technique : tant que 1.21 pèse près de deux fois le parc calendaire, la version à viser reste 1.21, avec du code qui tourne aussi sur 26.x. Pour voir ce qui a changé côté versions, notre page plugins Minecraft 26 détaille la branche calendaire.


