Un item unique, avec son nom, son histoire dans le lore, ses dégâts à lui et sa recette, c'est la demande numéro un sur un serveur qui veut se démarquer. Le code tient en cinquante lignes. Ce qui casse, ce n'est presque jamais la logique : c'est la version. Et en 2026, l'endroit exact où ça casse est justement le premier que tu vas écrire, le nom d'affichage.
Ce guide couvre le nom et le lore, les enchantements, les attributs, la reconnaissance de l'item plus tard dans ton code, et les recettes. Il précise aussi, ligne par ligne, ce qui fonctionne avec le serveur seul et ce qui exige un pack de ressources chez chaque joueur.
Commence par regarder où tournent vraiment les serveurs
Minecraft est passé au versionnage calendaire. La dernière version est la 26.2, sortie le 16 juin 2026. Mais le relevé public de bStats, sur 62 948 serveurs, raconte une autre histoire que les pages de téléchargement :
- 1.21.11 : 36,4 % du parc, de loin la première.
- 26.1.2 : 14,9 %.
- 26.2 : 7,4 %, moins que 1.21.11 à elle seule, cinq fois moins.
- 1.21.4 : 7,2 %.
La famille 1.21 reste majoritaire, et Paper 26.2 n'a même pas de build stable : la cible de production raisonnable côté calendaire, c'est 26.1.2. Cette répartition n'est pas une anecdote, elle décide de ton code. Un extrait copié d'un tutoriel 1.21 et un extrait copié d'un tutoriel 26.x ne peuvent pas être tous les deux justes, et les chiffres te disent lequel des deux mondes tes joueurs habitent.
La règle de compilation qui règle l'essentiel du problème
Autre changement de fond : Minecraft est désobfusqué depuis la 26.1, et le remapper de Paper a disparu. Bonne nouvelle en général, mais surtout : pour un item personnalisé, tu n'as désormais plus aucune raison de descendre au NMS. Tout ce que décrit cet article se fait avec l'API publique documentée sur docs.papermc.io et jd.papermc.io.
Nom d'affichage et lore : exactement là où Adventure 5 casse
Paper 26.x embarque Adventure 5, et Adventure 5 a supprimé des API qui étaient auparavant seulement dépréciées. Le lot concerne surtout les messages et le texte affiché aux joueurs. Or le nom d'affichage et le lore d'un item sont précisément du texte affiché aux joueurs. C'est pour ça que ce sujet, en apparence trivial, est le premier à tomber : du code écrit pour 1.21 peut ne plus compiler, sur la ligne 3.
// L'ancienne voie : chaines de caracteres et codes couleur legacy.
// C'est ce que renvoient la plupart des tutoriels encore en ligne.
meta.setDisplayName(ChatColor.GOLD + "Lame du Vide");
meta.setLore(Arrays.asList(ChatColor.GRAY + "Forgee dans une bulle d'air"));
// La voie qui compile sur les API 1.21 ET sur Paper 26.x : des Component.
meta.displayName(Component.text("Lame du Vide", NamedTextColor.GOLD)
.decoration(TextDecoration.ITALIC, false));
meta.lore(List.of(
Component.text("Forgee dans une bulle d'air", NamedTextColor.GRAY)
.decoration(TextDecoration.ITALIC, false)));Le détail que tout le monde oublie, c'est decoration(TextDecoration.ITALIC, false). Le client met en italique tout nom personnalisé et tout lore, par défaut. Sans cette ligne, ton item légendaire s'affiche penché comme un item renommé à l'enclume, et ça se voit immédiatement. C'est aussi le seul avantage vraiment concret de la voie Component sur les vieilles chaînes : la chaîne legacy ne permet pas de désactiver l'italique.
Adventure est inclus dans Paper, tu n'as donc rien à embarquer ni à déclarer. Et comme MiniMessage vient avec, tu peux écrire un dégradé en une ligne au lieu de bricoler des codes couleur caractère par caractère.
package fun.minax.artefacts;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.EquipmentSlotGroup;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.Plugin;
import java.util.List;
public final class LameDuVide {
/** Version du schema de l'item. A incrementer a chaque rebalancement. */
public static final int SCHEMA = 2;
public static final String ITEM_ID = "lame_du_vide";
private static final MiniMessage MM = MiniMessage.miniMessage();
private final NamespacedKey idKey;
private final NamespacedKey schemaKey;
private final NamespacedKey degatsKey;
public LameDuVide(Plugin plugin) {
this.idKey = new NamespacedKey(plugin, "item_id");
this.schemaKey = new NamespacedKey(plugin, "item_schema");
this.degatsKey = new NamespacedKey(plugin, "lame_du_vide_degats");
}
public ItemStack creer() {
ItemStack item = new ItemStack(Material.NETHERITE_SWORD);
ItemMeta meta = item.getItemMeta();
meta.displayName(MM.deserialize("<gradient:#8f5bff:#25d0ff>Lame du Vide</gradient>")
.decoration(TextDecoration.ITALIC, false));
meta.lore(List.of(
Component.text("Forgee dans une bulle d'air.", NamedTextColor.GRAY)
.decoration(TextDecoration.ITALIC, false),
Component.empty(),
Component.text("Degats +12", NamedTextColor.DARK_AQUA)
.decoration(TextDecoration.ITALIC, false)));
meta.addEnchant(Enchantment.SHARPNESS, 5, true);
meta.setEnchantmentGlintOverride(Boolean.TRUE);
meta.addAttributeModifier(
Attribute.ATTACK_DAMAGE,
new AttributeModifier(degatsKey, 12.0,
AttributeModifier.Operation.ADD_NUMBER,
EquipmentSlotGroup.MAINHAND));
meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES);
meta.getPersistentDataContainer().set(idKey, PersistentDataType.STRING, ITEM_ID);
meta.getPersistentDataContainer().set(schemaKey, PersistentDataType.INTEGER, SCHEMA);
item.setItemMeta(meta);
return item;
}
}Ne compile pas contre 26.x pour distribuer
Enchantements et brillance
Un enchantement au delà du niveau vanilla se pose avec addEnchant(enchantement, niveau, true), le troisième argument autorisant le dépassement des limites du jeu. Rien de compliqué, sauf le nom de la constante.
Les enchantements sont devenus des entrées de registre, et les constantes de l'API ont suivi les identifiants du jeu. Un extrait ancien qui écrit Enchantment.DAMAGE_ALL ne compile plus sur une API récente, où l'entrée s'appelle Enchantment.SHARPNESS. Vérifie toujours la constante dans le javadoc de ta version cible sur jd.papermc.io ou hub.spigotmc.org. Et si tu veux du code qui survit au prochain renommage, résous l'enchantement par sa clé plutôt que par un champ statique :
// Robuste face aux renommages : la cle vanilla, elle, ne change pas.
Enchantment tranchant = Registry.ENCHANTMENT.get(NamespacedKey.minecraft("sharpness"));
if (tranchant != null) {
meta.addEnchant(tranchant, 5, true);
}Pour la brillance seule, sans enchantement réel, setEnchantmentGlintOverride(true) fait le travail. C'est bien plus propre que le vieux truc de l'enchantement bidon masqué par un ItemFlag, et surtout ça n'ajoute aucun effet parasite. À l'inverse, si tu veux garder l'enchantement mais cacher sa ligne dans l'infobulle pour écrire toi-même le texte, ItemFlag.HIDE_ENCHANTS reste la bonne réponse. Aucun pack de ressources dans les deux cas.
Attributs : les vrais chiffres de l'item
Un attribut, c'est une valeur que le jeu connaît déjà : dégâts d'attaque, vitesse de déplacement, armure, portée. Tu les modifies avec un AttributeModifier attaché à un groupe d'emplacements, ce qui évite le grand classique de l'épée qui donne ses bonus depuis le sac à dos.
Deux pièges ici. Le premier est le même que pour les enchantements : les constantes d'attributs ont perdu leur préfixe historique quand elles ont rejoint un registre, et un extrait qui écrit Attribute.GENERIC_ATTACK_DAMAGE peut ne pas compiler là où l'API attend Attribute.ATTACK_DAMAGE. Le second est le constructeur : l'ancienne forme à base d'UUID et de nom libre est dépréciée au profit d'une NamespacedKey plus un EquipmentSlotGroup. Donne une clé distincte à chaque modificateur, sinon tu ne pourras plus le retirer proprement quand tu voudras rééquilibrer.
Ce qu'un attribut ne fera jamais
Reconnaître l'item plus tard : le seul point qui compte vraiment
Presque tous les plugins d'items amateurs partagent le même défaut : ils identifient l'item par son nom d'affichage. C'est fragile pour deux raisons. Un joueur peut renommer n'importe quelle épée à l'enclume, et surtout, le jour où tu corriges une faute dans le nom, tous les exemplaires déjà en circulation cessent d'être reconnus. Tu viens de casser l'inventaire de tes joueurs avec une correction de copie.
La bonne réponse est le PersistentDataContainer de l'ItemMeta : des clés typées, préfixées par le nom de ton plugin, stockées dans l'item lui-même. Elles survivent aux coffres, aux shulkers, aux drops au sol, au redémarrage du serveur, et elles sont invisibles pour le joueur.
package fun.minax.artefacts;
import org.bukkit.NamespacedKey;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.Plugin;
public final class LameDuVideListener implements Listener {
private final NamespacedKey idKey;
private final NamespacedKey schemaKey;
public LameDuVideListener(Plugin plugin) {
this.idKey = new NamespacedKey(plugin, "item_id");
this.schemaKey = new NamespacedKey(plugin, "item_schema");
}
@EventHandler
public void onInteract(PlayerInteractEvent event) {
ItemStack item = event.getItem();
if (item == null || !item.hasItemMeta()) {
return;
}
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
if (!LameDuVide.ITEM_ID.equals(pdc.get(idKey, PersistentDataType.STRING))) {
return;
}
Integer schema = pdc.get(schemaKey, PersistentDataType.INTEGER);
if (schema == null || schema < LameDuVide.SCHEMA) {
// Exemplaire d'une ancienne generation : on le remet a niveau sur place
// au lieu de laisser deux equilibrages differents cohabiter sur le serveur.
migrer(item);
return;
}
declencherEffet(event);
}
private void migrer(ItemStack ancien) {
// Reappliquer lore, attributs et schema courant sur l'exemplaire existant.
}
private void declencherEffet(PlayerInteractEvent event) {
// L'effet propre a l'item.
}
}La clé item_schema est le détail que presque personne n'écrit, et c'est celui qui te sauvera. Six mois après la sortie de ton item, tu voudras baisser ses dégâts. Les exemplaires déjà fabriqués, eux, dorment dans des coffres avec l'ancien modificateur écrit dedans. Sans numéro de version stocké sur l'item, tu n'as aucun moyen de les distinguer des neufs, et ton rééquilibrage ne s'applique qu'aux nouveaux joueurs. Avec, tu les remets à niveau au premier clic.
Le piège de la base de données : 75,1 % des serveurs sont en mode hors ligne
proprietaire_item(uuid_joueur, item_id). C'est le piège numéro un de la conception d'une base pour un plugin. 75,1 % des serveurs tournent en mode hors ligne, et dans ce mode l'UUID d'un joueur est dérivé de son pseudo. Le jour où le joueur change de pseudo, ou le jour où le serveur passe en mode en ligne, la clé change et toutes les lignes deviennent orphelines. L'item, lui, n'a pas de pseudo : garde son identité sur lui, dans le PDC, et indexe sur cette identité si tu as vraiment besoin d'une base.Mon arbitrage sur ce point est net. Tant que ta question est « cet item est-il le mien, et dans quelle version », le PDC suffit et tu n'as besoin d'aucune base de données. Une base devient meilleure au moment précis où la question s'inverse : « qui détient les trois exemplaires uniques que j'ai distribués ». Là, il faut un index. Le pilote JDBC SQLite est déjà fourni avec Paper, tu n'as donc rien à embarquer pour ça. HikariCP, lui, n'est pas fourni : si tu veux un pool de connexions, déclare-le dans le bloc libraries: de ton plugin.yml et laisse le serveur le télécharger, plutôt que de le noyer dans ton .jar. Et dans tous les cas, jamais d'accès base sur le thread principal, sous peine de faire chuter le TPS.
Si tu vises aussi Folia, souviens-toi que les régions du monde tournent sur plusieurs threads : un plugin qui suppose un thread principal unique provoque des crashs. Tout ce que tu planifies autour de l'item passe par le RegionScheduler, pas par un scheduler global.
Recettes personnalisées
Une recette façonnée s'enregistre au démarrage, avec une clé propre à ton plugin. Le résultat que tu passes conserve son ItemMeta, donc l'item fabriqué par un joueur porte bien tes données persistantes, ton lore et tes attributs.
NamespacedKey recetteKey = new NamespacedKey(this, "lame_du_vide");
ShapedRecipe recette = new ShapedRecipe(recetteKey, new LameDuVide(this).creer());
recette.shape(" F ", " L ", " B ");
recette.setIngredient('F', Material.NETHERITE_INGOT);
recette.setIngredient('L', Material.NETHERITE_INGOT);
recette.setIngredient('B', Material.BREEZE_ROD);
// Idempotent : sans ce retrait, un second enregistrement de la meme cle echoue.
Bukkit.removeRecipe(recetteKey);
Bukkit.addRecipe(recette);Trois choses que la documentation ne met pas assez en avant. D'abord, le retrait avant l'ajout : sans lui, tout second enregistrement de la même clé pose problème, et c'est exactement ce qui arrive quand quelqu'un lance un /reload au lieu de redémarrer. Ensuite, la découverte : la recette fonctionne pour qui connaît la forme, mais elle n'apparaît dans le livre de recettes qu'après un discoverRecipe sur le joueur. Enfin, la limite la plus gênante.
Une ShapedRecipe raisonne sur le matériau, pas sur tes données persistantes. Tu ne peux donc pas exiger, avec elle seule, que l'ingrédient soit ton propre item personnalisé. Le motif fiable consiste à déclarer la recette sur le matériau de base, puis à valider le marqueur au moment de la fabrication :
@EventHandler
public void onPrepare(PrepareItemCraftEvent event) {
Recipe recette = event.getRecipe();
if (!(recette instanceof Keyed keyed) || !recetteKey.equals(keyed.getKey())) {
return;
}
boolean fragmentPresent = false;
for (ItemStack slot : event.getInventory().getMatrix()) {
if (slot == null || !slot.hasItemMeta()) {
continue;
}
String id = slot.getItemMeta().getPersistentDataContainer()
.get(idKey, PersistentDataType.STRING);
if ("fragment_du_vide".equals(id)) {
fragmentPresent = true;
}
}
if (!fragmentPresent) {
event.getInventory().setResult(null);
}
}Teste l'enclume, la meule et la table de forge
Ce qui exige un pack de ressources, et ce qui n'en exige pas
C'est la question qui revient le plus souvent, et la réponse est plus favorable que ce que la plupart des gens croient. Presque tout est côté serveur.
| Serveur seul | Pack de ressources requis | |
|---|---|---|
| Nom, lore, couleurs, dégradés | Oui | Non |
| Brillance, enchantements cachés | Oui | Non |
| Attributs, durabilité, taille de pile | Oui | Non |
| Données persistantes, recette | Oui | Non |
| Particules, sons vanilla | Oui | Non |
| Texture et modèle 3D propres | Impossible | Obligatoire |
| Son inédit | Impossible | Obligatoire |
Pour une texture, le mécanisme est en deux moitiés dont tu ne contrôles qu'une. Côté serveur, tu poses une valeur de données de modèle personnalisé sur l'item. Côté client, un pack de ressources doit associer cette valeur à un modèle. Le point dur : tu peux poser la valeur, elle ne fera rien du tout sans le pack. Un joueur qui refuse le téléchargement voit une épée en netherite ordinaire, avec ton nom et ton lore dessus. Le format des composants d'items est décrit sur minecraft.wiki, et l'envoi du pack se règle dans server.properties ou par joueur depuis ton plugin.
Le conseil que je donne aux petits serveurs
Ce qui va casser chez toi
- Le lore en italique. Tu as oublié l'appel qui désactive la décoration. Ça se voit en trois secondes, ça se corrige en une ligne.
- L'extrait copié d'un vieux tutoriel. Constantes d'attributs préfixées, noms d'enchantements d'avant le passage aux registres, appels de texte supprimés par Adventure 5. Trois familles d'erreurs de compilation, un seul remède : le javadoc de ta version cible.
- L'identification par le nom. Fonctionne une semaine, puis un joueur renomme une épée à l'enclume ou tu corriges une faute.
- La table indexée sur l'UUID du joueur. En mode hors ligne, soit 75,1 % des serveurs, cet UUID dérive du pseudo. Un changement de pseudo et tes lignes sont orphelines.
- La recette enregistrée deux fois. Un
/reloadet la clé existe déjà. Retire avant d'ajouter, et redémarre au lieu de recharger. - La texture qui ne marche que chez toi. Ton pack est installé en local, tu oublies qu'il n'est pas servi aux autres.
- L'accès base sur le thread principal, ou un scheduler global sur Folia. Le premier fait chuter le TPS, le second fait crasher le serveur.
Mon arbitrage
En dessous de 50 joueurs simultanés : un seul plugin, tout dans le PersistentDataContainer, zéro base de données, zéro pack de ressources obligatoire. Compilé contre l'API Paper 1.21.11 en ciblant Java 21, avec les Component d'Adventure pour tout le texte. Ce .jar tourne aussi bien sur les 36,4 % du parc en 1.21.11 que sur les 14,9 % en 26.1.2, et tu n'auras rien à recompiler avant très longtemps.
Au dessus, ou dès que des items uniques ont une valeur : ajoute un index SQLite, dont le pilote est déjà dans Paper, avec une clé qui vient de l'item et jamais du joueur, et des accès sortis du thread principal. À cette échelle tu peux aussi rendre le pack de ressources obligatoire : les joueurs d'un serveur établi acceptent le téléchargement, ceux d'un serveur inconnu non.
Ne cible 26.2 en exclusivité que si : le serveur est le tien, tu acceptes de recompiler à chaque build, et tu n'as pas l'intention de distribuer le .jar. Paper 26.2 n'a pas de build stable, et 26.2 représente 7,4 % du parc. Ce n'est pas une cible de distribution, c'est un terrain de jeu.
Ton item personnalisé, compilé et testé
Décris l'item : son nom, son lore, ses effets, sa recette. Minax écrit le Java, le compile pour ta version cible et te rend un .jar prêt à déposer dans /plugins/.
Créer mon itemQuestions fréquentes
Faut-il un pack de ressources pour creer un item personnalise ?
Non, sauf pour la texture et le modele 3D. Le nom colore, le lore, les enchantements, la brillance, les attributs, les donnees persistantes et la recette sont entierement cote serveur. Seul un visuel different du materiau de base exige un pack de ressources installe chez chaque joueur.
Pourquoi setDisplayName ne compile plus sur Paper 26 ?
Paper 26.x embarque Adventure 5, qui a supprime des API auparavant seulement depreciees, en particulier tout ce qui touche aux messages et au texte affiche aux joueurs. Le nom d'affichage et le lore passent desormais par des Component, avec displayName(Component) et lore(List<Component>), qui existent aussi sur les API 1.21.
Comment reconnaitre mon item personnalise plus tard dans le code ?
Avec le PersistentDataContainer de l'ItemMeta, sous une NamespacedKey propre a ton plugin. Jamais par le nom d'affichage : un joueur peut renommer un item a l'enclume, et le moindre changement de formulation de ta part invalide tous les exemplaires deja en circulation.
Quelle version de Java faut-il viser pour un plugin en 2026 ?
Java 25 est le plancher des versions calendaires de Minecraft, mais compiler en Java 21 reste le bon choix : c'est ce que fait tourner la majorite du parc, et un .jar Java 21 se charge sur un runtime plus recent. L'inverse est faux.
Peut-on exiger un item personnalise comme ingredient d'une recette ?
Pas avec une ShapedRecipe seule : elle raisonne sur le materiau, pas sur tes donnees persistantes. Le motif fiable est de declarer la recette sur le materiau de base, puis de verifier le marqueur dans PrepareItemCraftEvent et de vider le resultat quand il manque.
Pour aller plus loin : si ton plugin refuse de démarrer, passe par le guide de dépannage, et si tu hésites encore entre un script et un vrai .jar, lis Skript ou plugin compilé.



