Les tags NBT
-
Sommaire
Introduction
Ce tutoriel va vous expliquer le fonctionnement du format de sérialisation de Minecraft, j’ai nommé les tags NBT.
Pré-requis
Il n’y a aucun pré-requis à ce tutoriel
Les différentes classes
NBTBase
La classe NBTBase est une classe abstraite, c’est la classe mère de toutes les classes pouvant se stocker dans les tags NBT. Lorsque que je parlerai donc des NBTBase je parlerai de l’ensemble des classes qui héritent de cette dernière (NBTTagCompound, NBTTagList, NBTTTagString, NBTTagInt, etc …).
Voyons rapidement ce que cette classe demande de définir comme fonctions :
- La fonction NBTBase#getId, cette fonction retourne un byte représentant l’ID de classe fille. Par exemple NBTTagInt#getId va retourner 3, NBTTagString#getId va retourner 8, tandis que NBTTagCompound#getId va retourner 10. Chaque type est donc lié à un ID, cet ID est utilisé lors que la désérialisation des données depuis le fichier binaire. Ainsi nous ne pouvons par créer vos propres classes héritants de NBTBase, mais ne vous inquiétez pas, celles présentent sont suffisantes. Ces IDs peuvent être récupérés en utilisant NBTBase#getId ou en utilisant les constantes présentes dans la classe Constants$NBT, on y trouve par exemple NBT#TAG_COMPOUND qui vaut 10. Ces IDs correspondent aussi à l’index du tableau NBTBase#NBT_TYPES où se situe le nom du type associé à l’ID.
- La fonction Object#toString.
- La fonction NBTBase#write qui permet d’écrire les données du tag dans un DataOutput.
- La fonction NBTBase#read pour lire les données depuis un DataInput.
- La fonction NBTBase#clone.
Pour résumé ce qu’il faut savoir sur cette classe :
- Elle est la classe mère de toutes les classes permettant de stocker des tags NBT.
- Toute classe héritant de NBTBase se voit attribuer un ID unique.
Les types primitifs
Les types primitifs tels que int, double, byte, long, etc … ont une classe héritant de NBTBase qui les représentent. Le nom de cette classe est souvent NBTTagTYPE avec TYPE qui est le nom du type. Par exemple :
- int : NBTTagInt
- byte : NBTTagByte
- long : NBTTagLong
- float : NBTTagFloat
- double : NBTTagDouble
- etc …
Toutes les classes ci-dessus héritent en réalité de NBTPrimitive qui hérite elle-même de NBTBase, la classe NBTPrimitive rajoute la possibilité d’effectuer un cast de chaque type vers un autre. Ainsi si vous avec un int dans un NBTTagInt mais que vous voulez un double, vous pouvez utiliser NBTTagInt#getDouble.
Nous avons aussi des classes pour les tableaux de certains types primitifs (byte et int). Ces classes sont NBTTagByteArray et NBTTagIntArray. Je ne vais pas m’étendre plus dessus, elles stockent un tableau du type associé.
Pour ce qui est des constructeurs de toutes ces classes il faut généralement mettre en paramètre le type que l’on veut stocker, exemple :
NBTTagInt tagInt = new NBTTagInt(238); NBTTagDouble tagDouble = new NBTTagDouble(567.1D); NBTTagFloat tagFloat = new NBTTagFloat(12.4F); NBTTagIntArray tagIntArray = new NBTTagIntArray(new int[]{1, 5, 3, 7, 8}); NBTTagByteArray tagByteArray = new NBTTagByteArray(new byte[]{-128, 0, 127, 4});
Note : je n’ai pas parlé des boolean car une boolean est considérée comme un byte qui a pour valeur 1 ou 0.
NBTTagString
La classe NBTTagString permet de stocker une chaine de caractères. Elle possède 2 constructeurs, 1 avec paramètre et 1 sans paramètre.
NBTTagString tagString = new NBTTagString(); //Ici le tag contient un chaine vide NBTTagString tagString2 = new NBTTagString("Valeur du tag");
Il n’y a rien à dire de plus sur cette classe à vrai dire.
NBTTagCompound
La classe NBTTagCompound est certainement la classe la plus utilisée, lorsque que le jeu nous demande de sérialiser ou désérialiser nos données, il nous donne une instance de la classe NBTTagCompound pour que l’on y écrive ou qu’on y lise nos données.
Un NBTTagCompound lie à une mot clé une instance de NBTBase. La clé utilisée est un chaine de caractères.
Nous pourrions par exemple représenter un NBTTagCompound de la façon suivante :NBTTagCompound | |– "nom": NBTTagString("Frank") |-- "age": NBTTagByte(45) |-- "argent": NBTTagInt(35000) |-- "nombre enfants": NBTTagByte(4) |-- "noms enfants": NBTTagList(...)
Vous voyez ici que j’ai utilisé un objet NBTTagList que nous verrons plus tard, d’où les points de suspension. Mais comme vous l’avez remarqué, à chaque clé est associé un objet NBTBase et les espaces sont autorisés dans le nom des clés.
Le constructeur de la classe NBTTagCompound ne prend pas le paramètre, on l’instancie puis on lie les NBTBase aux mots clés en utilisant NBTTagCompound#setTag :
NBTTagCompound nbt = new NBTTagCompound(); nbt.setTag("nom", new NBTTagString("Frank")); nbt.setTag("age", new NBTTagByte((byte) 45)); nbt.setTag("argent", new NBTTagInt(35000)); nbt.setTag("nombre enfants", new NBTTagByte((byte) 4)); nbt.setTag("noms enfants", new NBTTagList()); //Nous n'avons rien mis dedans encore
J’ai mis ici les données vues plus haut dans un NBTTagCompound, cependant vous pouvez voir que si nous avons beaucoup de nombres à stocker cela peut être long et surtout très verbal pour peu, c’est pourquoi il existe des méthodes qui vous facilement la tâche, prenons pour exemple NBTTagCompound#setInteger :
NBTTagCompound nbt = new NBTTagCompound(); nbt.setInteger("argent", 35000);
Le contenue de la fonction NBTTagCompound#setInteger est le suivant :
public void setInteger(String key, int value) { this.tagMap.put(key, new NBTTagInt(value)); }
Vous voyez que cela revient à créer une instance de NBTTagInt et de l’ajouter aux tags, mais cela est plus rapide et plus clair. En appliquant ce concept à tous les types primitifs et d’autres on obtient :
NBTTagCompound nbt = new NBTTagCompound(); nbt.setString("nom", "Frank"); nbt.setByte("age", (byte) 45); nbt.setInteger("argent", 35000); nbt.setByte("nombre enfants", (byte) 4); nbt.setTag("noms enfants", new NBTTagList());
Ce qui est beaucoup plus clair que notre code précédent. Ce genre de raccourci est aussi existant pour la sérialisation de la classe UUID, il suffit d’invoquer NBTTagCompound#setUniqueId.
Jusqu’à maintenant je n’ai parlé que de rentrer des données dans le NBTTagCompound cependant vous pouvez les lire avec grosso-modo les mêmes fonctions sauf qu’à la place de set il faut utiliser get :
String name = nbt.getString("nom"); byte age = nbt.getByte("age"); int money = nbt.getInteger("argent"); byte nbChildren = nbt.getByte("nombre enfants"); //Nous verrons comment récupérer une liste correctement plus tard.
On récupère une donnée en indiquant la clé à laquelle elle est associée, si aucune donnée n’est associée, la fonction va retourner une valeur par défaut, 0 pour les nombres et une chaine vide pour String. Ici j’ai utilisé les fonctions “raccourcies” mais vous pouvez très bien utiliser NBTTagCompound#getTag pour récupérer vos données mais cela n’est pas conseillé, je vais cependant vous donner un exemple :
String name = ((NBTTagString)nbt.getTag("nom")).getString();
Ici si aucune donnée n’est associée à “nom” vous aurez un NPE. Si une donnée est bien associée à “nom” mais que ce n’est pas un NBTTagString vous aurez alors un problème de cast. Il existe ainsi une fonction permettant de vérifier l’existence d’un tag et son type qui est NBTTagCompound#hasKey(String, int), si vous ne voulez pas vérifier le type vous pouvez utiliser NBTTagCompound#hasKey(String) et si vous ne voulez vérifier que le type vous pouvez utiliser NBTTagCompound#getTagId. Exemple d’utilisation qui résout les problèmes énoncés précédemment :
if (nbt.hasKey("nom", NBT.TAG_STRING)) { String name = ((NBTTagString) nbt.getTag("nom")).getString(); }
De cette façon vous n’aurez plus de problème, cependant il est vivement conseillé d’utiliser NBTTagCompound#getString car cette fonction effectue la vérification et renvoie une valeur par défaut.
La classe NBTTagCompound héritant de NBTBase vous pouvez mettre des NBTTagCompound dans un NBTTagCompound :
NBTTagCompound | |-- "nom": NBTTagString("Frank") |-- "adresse": NBTTagCompound |-- "Ville": NBTTagString("Paris") |-- "Rue": NBTTagString("Rue de la paix") |-- "Numéro": NBTTagShort(10)
Ce qui donne :
NBTTagCompound nbt = new NBTTagCompound(); nbt.setString("nom", "Frank"); NBTTagCompound addressTag = new NBTTagCompound(); addressTag.setString("Ville", "Paris"); addressTag.setString("Rue", "Rue de la paix"); addressTag.setShort("Numéro", (short)10); nbt.setTag("adresse", addressTag);
On peut récupérer un NBTTagCompound grâce à NBTTagCompound#getCompoundTag, ainsi pour récupérer notre adresse on ferait :
NBTTagCompound addressTag = nbt.getCompoundTag("adresse");
Si le tag n’existe pas cela renvoie une NBTTagCompound vide. La dernière chose qu’il nous reste à voir est la NBTTagList.
NBTTagList
La NBTTagList permet de stocker des NBTBase sans leur associer de clé. Pour revenir à notre fils rouge on voudrait cela :
NBTTagList |– NBTTagString("Jean") |-- NBTTagString("Eric") |-- NBTTagString("Julie") |-- NBTTagString("Catherine")
Vous ne pouvez pas mélanger les types de NBTBase. Les éléments de la liste sont identifiés par leur index. En soit on pourrait se représenter la liste de la façon suivante :
NBTTagList |-- 0: NBTTagString("Jean") |-- 1: NBTTagString("Eric") |-- 2: NBTTagString("Julie") |-- 3: NBTTagString("Catherine")
Cette liste fonctionne comme la plupart des listes :
- Ajout d’un élément en fin de liste : NBTTagList#appendTag(NBTBase).
- Récupérer un élément de la liste : NBTTagList#get(int), si l’index est trop élevé la fonction retourne un NBTTagEnd.
- Connaitre la taille de la liste : NBTTagList#tagCount.
- Supprimer un élément de la liste : NBTTagList#removeTag(int), peut causer un IndexOutOfBoundsException.
- Savoir si la liste est vide : NBTTagList#hasNoTags.
- Remplacer un élément de la liste : NBTTagList#set(int, NBTBase).
Je répète que vous ne pouvez stocker qu’un seul type de donnée dans la liste. Pour ce qui est de récupérer les données, vous avez la fonction donnée précédemment ou les fonctions “raccourcies” du type NBTTagList#getFloatAt(int), etc … qui retourneront une valeur par défaut si le type de la liste ne correspond pas ou que l’index n’est pas bon.
Remarque : NBTTagList#getTagType retourne l’ID du type de donnée contenue dans la liste.
C’est ainsi que nous allons voir comment récupérer une liste depuis un NBTTagCompound, il va falloir utiliser NBTTagCompound#getTagList(String, int). Pour cela il va aussi falloir connaitre le type de donnée qu’elle possède (vous pouvez la récupérer sans connaitre le type de donnée en utilisant NBTTagCompound#getTag). Exemple de récupération de notre liste contenant le nom des enfants :
NBTTagList list = nbt.getTagList("noms enfants", NBT.TAG_STRING);
Si la liste n’existe pas ou qu’elle ne contient pas le bon type de donnée alors la fonction retourne une liste vide.
NBTUtil
Cette classe ne permet pas de stocker des données, elle ne contient de toute façon que des fonctions statiques, elle permet de vous aider à sérialiser et désérialiser vos données. Vous y trouverez les fonctions suivantes :
- Ecrire un GameProfile dans un NBTTagCompound : NBTUtil#writeGameProfile.
- Lire un GameProfile depuis un NBTTagCompound : NBTUtil#readGameProfileFromNBT.
- Comparer deux NBTTagCompound : NBTUtil#areNBTEquals.
- Créer un NBTTagCompound contenant un UUID : NBTUtil#createUUIDTag, attention cette fonction ne stocke pas l’UUID de la même façon que NBTTagCompound#setUniqueId.
- Lire un UUID depuis un NBTTagCompound : NBTUtil#getUUIDFromTag, attention cette fonction ne lit pas l’UUID de la même façon que NBTTagCompound#getUniqueId.
- Créer un NBTTagCompound contenant un BlockPos : NBTUtil#createPosTag.
- Lire un BlockPos depuis un NBTTagCompound : NBTUtil#getPosFromTag.
- Ecrire un IBlockState dans un NBTTagCompound : NBTUtil#writeBlockState.
- Lire un IBlockState depuis un NBTTagCompound : NBTUtil#readBlockState.
Les NBT tags et les ItemStack
Vous serez sûrement amené à stocker des informations propres à vos objets, comme le nom du propriétaire, si l’item est actif ou pas, etc … Sauf que vous ne devez pas sauvegarder ces informations dans la classe de l’Item directement. Ce genre d’informations sont stockés dans un NBTTagCompound se trouvant dans la classe ItemStack. Imaginons une fonction fictive de la classe Item de notre objet, nous voulons enregistrer le nom du joueur dans les tags NBT de notre objet lorsque cette fonction est appelée :
public void fictiveFonction(EntityPlayer player, ItemStack stack, EnumHand hand) { //Nous voulons stocker le nom du joueur dans les tags NBT de l'ItemStack }
Il faut savoir que si l’ItemStack n’a pas encore de tags NBT alors le field les contenant est null, c’est pourquoi il nous faut vérifier si l’ItemStack possède déjà des tags NBT avant de stocker le nom du joueur, si il n’en a pas nous allons créer le NBTTagCompound. La fonction ItemStack#hasTagCompound nous permet de tester la présence de tag NBT sur le stack, ItemStack#getTagCompound nous retournons les tags NBT du stack et ItemStack#setTagCompound permet de définir les tags NBT. Avec ça on a tout ce qu’il nous faut:
public void fictiveFonction(EntityPlayer player, ItemStack stack, EnumHand hand) { if(!stack.hasTagCompound()) { //Ceci est pour éviter d'avoir un NPE stack.setTagCompound(new NBTTagCompound()); } stack.getTagCompound().setString("owner", player.getName()); }
Le nombre de tags NBT sur un objet peut être vu en jeu en faisant F3 + H et en mettant sa souris sur un objet.
Rédaction : BrokenSwing
Correction : Folgansky
Ce tutoriel de Minecraft Forge France est mis à disposition selon les termes de la licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International -
Salut,
premièrement superbe tuto ! J’imagine qu’il reste des parties/exemples à rédiger mais pour le moment, cela aidera énormément les débutants
Simple question : aurons nous droit à une aparte sur les DataWatchers ? -
Je pensais le laisser tel quel, le but n’est pas vraiment de montrer l’application des NBTs mais plutôt d’expliquer comment cela marche, pour les applications il y a des tutos dédiés. Pour ce qui est des DataWatchers je ne m’y suis jamais penché. Je vais voir à quoi ça ressemble.
-