Utiliser les capabilities
-
Sommaire
Introduction
Bonjour, dans ce tutoriel, je vais vous montrer comment utiliser le nouveau système de “capabilities” ou capacité qui remplace les Extended Entity Properties des versions précédentes. Les capacités ont la même fonction que les EEP, elles servent à sauvegarder des données “dans” une entité. NB: Le système de Extended Entity Properties est déprécié depuis la 1.8.
Pré-requis
Code
Packet :
Je vous invite donc à créer une nouvelle classe pour notre Packet, je vais la nommer “PacketCapabilitiesTutoriel” pour ce tutoriel. Vous devez implémenter IMessage dans votre classe et ajouter les méthodes non-implémentées. Ça devrait vous donner ça:
public class PacketCapabilitiesTutoriel implements IMessage { @Override public void fromBytes(ByteBuf buf) { } @Override public void toBytes(ByteBuf buf) { } }
Pour cet exemple, je vais faire un système de monnaie comme Gugu42 a fait dans son tutoriel sur les Extended Entity Properties. Donc, on crée une nouvelle variable public de type int en haut de notre classe. Ensuite, à l’aide du ByteBuf, on la convertie en byte et on la récupère, il ne faut pas oublier de créer un constructeur vide et un constructeur avec les valeurs nécessaires sinon ça ne fonctionnera pas.
public int money; public PacketCapabilitiesTutoriel(int money) { this.money = money; } public PacketCapabilitiesTutoriel() {} @Override public void fromBytes(ByteBuf buf) { this.money = buf.readInt(); } @Override public void toBytes(ByteBuf buf) { buf.writeInt(this.money); }
Il ne nous manque plus que les Handlers, c’est relativement simple, on crée deux classes statiques à l’intérieur de notre classe. Les handlers doivent implémenter IMessageHandler, le premier sera nommé ClientHandler et le deuxième ServerHandler.
public static class ServerHandler implements IMessageHandler <PacketCapabilitiesTutoriel, IMessage>{ @Override public IMessage onMessage(PacketCapabilitiesTutoriel message, MessageContext ctx) { //Nous reviendrons sur cette ligne plus tard. MinecraftServer.getServer().addScheduledTask(new ScheduledPacketTask(ctx.getServerHandler().playerEntity, message)); return null; } } @SideOnly(Side.CLIENT) public static class ClientHandler implements IMessageHandler <PacketCapabilitiesTutoriel, IMessage>{ @Override public IMessage onMessage(PacketCapabilitiesTutoriel message, MessageContext ctx) { //Nous reviendrons sur cette ligne plus tard. Minecraft.getMinecraft().addScheduledTask(new ScheduledPacketTask(null, message)); return null; } }
Classe Principale :
Il y a plusieurs choses à faire ici. Premièrement, vous devez enregistrer le packet:
network.registerMessage(PacketCapabilitiesTutoriel.ClientHandler.class, PacketCapabilitiesTutoriel.class, 3, Side.CLIENT); network.registerMessage(PacketCapabilitiesTutoriel.ServerHandler.class, PacketCapabilitiesTutoriel.class, 3, Side.SERVER);
Ensuite, vous devez ajouter le code ci-dessous en haut de votre classe.
@CapabilityInject(TutoCapabilities.class) public static final Capability<TutoCapabilities> TUTO_CAP = null;
Et finalement, vous devez enregistrer votre capacité dans le init:
TutoCapabilities.register();
ScheduledPacketTask:
J’ai créé cette classe pour rendre mon code plus lisible, mais vous pouvez très bien la mettre directement dans vos paramètres comme n’importe quel “runnable”. Implémentez Runnable dans votre classe et ajoutez les méthodes non-implémentées. Vous devez ensuite créer un constructeur avec les paramètres nécessaires. Pour la rendre compatible client et serveur, j’ai mis un paramètre EntityPlayer, il sera null pour le client et défini pour le serveur. Ça devrait donner ça:
public class ScheduledPacketTask implements Runnable { private EntityPlayer player; private PacketCapabilitiesTutoriel message; public ScheduledPacketTask(EntityPlayer player, PacketCapabilitiesTutoriel message) { this.player = player; this.message = message; } @Override public void run() { //Condition ternaire pour récupérer le joueur selon le côté. EntityPlayer player = this.player == null ? getPlayer() : this.player; //On revient sur cette ligne plus tard. player.getCapability(ModTutoriel.TUTO_CAP, null).setMoney(message.money); } @SideOnly(Side.CLIENT) private EntityPlayer getPlayer() { return Minecraft.getMinecraft().thePlayer; } }
Le provider, le storage et la factory :
Je vous invite maintenant à créer une classe qui implémente ICapabilityProvider et INBTSerializable. Ajoutez les méthodes non-implémentées et ça devrait vous donner ceci:
public class TutoCapabilities implements ICapabilityProvider, INBTSerializable<NBTTagCompound> { @Override public boolean hasCapability(Capability capability, EnumFacing facing) { return false; } @Override public <T> T getCapability(Capability <T>capability, EnumFacing facing) { return null; } @Override public NBTTagCompound serializeNBT() { return null; } @Override public void deserializeNBT(NBTTagCompound compound) { } }
Maintenant, nous allons modifier les méthodes hasCapability et getCapability pour les adapter à notre code et pour qu’elles retournent les bonnes valeurs. Premièrement, on doit modifier le return de hasCapability pour que ça retourne la véritable valeur et non simplement false.
return ModTutoriel.TUTO_CAP != null && capability == ModTutoriel.TUTO_CAP;
On vérifie que la capacité ne soit pas nulle et que ce soit bien la nôtre. Si oui, on retourne true, sinon on retourne false. Ensuite, on change le return de getCapability pour retourner l’instance de la classe en faisant les mêmes vérifications que plus haut.
return ModTutoriel.TUTO_CAP != null && capability == ModTutoriel.TUTO_CAP ? (T)this : null;
Ensuite, il faut modifier les méthodes deserializeNBT et serializeNBT, elles servent à sauvegarder les valeurs. Dans le deserializeNBT, nous devons créer un nouvel objet NBTTagCompound, qui va stocker nos valeurs. Ensuite, on doit ajouter notre valeur dans le compound et finalement, on le retourne.
@Override public NBTTagCompound serializeNBT() { NBTTagCompound compound = new NBTTagCompound(); compound.setInteger("Money", this.getMoney()); return compound; }
Et dans la méthode deserialize, on récupère le tag et on met à jour la valeur avec celui-ci.
@Override public void deserializeNBT(NBTTagCompound compound) { this.setMoney(compound.getInteger("Money")); }
Après, on crée la variable public de type int et on crée un getter/setter pour celle-ci.
public int money; public void setMoney(int money) { this.money = money; } public int getMoney() { return this.money; }
Maintenant, on va créer deux classes statiques dans notre classe comme avec les packets. Les deux sont inutiles, mais elles sont nécessaires pour que cela fonctionne. NB: Elles ne sont pas inutiles, mais elles ne nous servent pas. Elles servent seulement quand l’entité n’appartient pas à Minecraft.
public static class Storage implements Capability.IStorage<TutoCapabilities> { @Override public NBTBase writeNBT(Capability<TutoCapabilities> capability, TutoCapabilities instance, EnumFacing side) { return null; } @Override public void readNBT(Capability<TutoCapabilities> capability, TutoCapabilities instance, EnumFacing side, NBTBase nbt) { } } public static class Factory implements Callable<TutoCapabilities> { @Override public TutoCapabilities call() throws Exception { return null; } }
Finalement, nous devons créer deux méthodes: sync, register et un constructeur. La méthode sync sert à synchroniser entre le client et le serveur, elle doit être appelée quand on modifie une valeur. La méthode register, elle, doit être statique et est appelée depuis la classe principale, elle sert à enregistrer la capacité. Le constructeur, lui, est appelé quand on attache la capacité au joueur. Il faut aussi créer une nouvelle variable de type EntityPlayer pour pouvoir récupérer le joueur dans notre méthode sync.
private EntityPlayer player; public static void register() { CapabilityManager.INSTANCE.register(TutoCapabilities.class, new TutoCapabilities.Storage(), new TutoCapabilities.Factory()); } public TutoCapabilities(EntityPlayer player) { this.money = 0; this.player = player; } public void sync() { PacketCapabilitiesTutoriel packet = new PacketCapabilitiesTutoriel(this.getMoney()); if(!this.player.worldObj.isRemote) { EntityPlayerMP playerMP = (EntityPlayerMP)player; ModTutoriel.network.sendTo(packet, playerMP); } else { ModTutoriel.network.sendToServer(packet); } }
L’event handler :
Je ne décrirai pas comment enregistrer un event handler, je vais seulement expliquer les events à utiliser. Si vous ne savez pas comment utiliser les events, je vous invite à aller voir le tutoriel sur ceux-ci dans les pré-requis. Le premier event à utiliser est le PlayerEvent.Clone, il est appelé quand le joueur est cloné. Le joueur est cloné quand il change de dimension et quand il meurt. Dans notre cas, on veut exécuter l’event seulement à la mort du joueur. Dans l’event, vous avez accès à plusieurs variables: original, entityPlayer et wasDeath. Nous aurons besoin des trois, le premier est l’instance du joueur qui est mort, le deuxième est l’instance du nouveau joueur qui respawn et le dernier est un boolean qui est à true si l’event est appelé à la suite de la mort du joueur.
@SubscribeEvent public void onPlayerCloned(PlayerEvent.Clone event) { if(event.wasDeath) { if(event.original.hasCapability(ModTutoriel.TUTO_CAP, null)) { TutoCapabilities cap = event.original.getCapability(ModTutoriel.TUTO_CAP, null); TutoCapabilities newCap = event.entityPlayer.getCapability(ModTutoriel.TUTO_CAP, null); newCap.setMoney(cap.getMoney()); } } }
Le deuxième event à utiliser est le PlayerRespawnEvent, il est appelé quand le joueur respawn, mais après le PlayerEvent.Clone et le prochain event. Dans cet event, nous allons synchroniser, tout simplement.
@SubscribeEvent public void onPlayerRespawn(PlayerRespawnEvent event) { if(!event.player.worldObj.isRemote) { event.player.getCapability(ModTutoriel.TUTO_CAP, null).sync(); } }
Et finalement, le dernier event à utiliser est le AttachCapabilitiesEvent.Entity, il est appelé quand les autres capacités sont attachées aux entités. C’est à ce moment que l’on doit attacher notre capacité à notre joueur.
@SubscribeEvent public void onAttachCapability(AttachCapabilitiesEvent.Entity event) { if(event.getEntity() instanceof EntityPlayer) { event.addCapability(new ResourceLocation(ModTutoriel.MODID + ":TUTO_CAP"), new TutoCapabilities((EntityPlayer) event.getEntity())); } }
Bonus
Aucun bonus pour le moment, mais n’hésitez pas à le demander si vous avez une idée.
Résultat
Je ne peux pas prendre de screenshot du résultat, puisque le but est de sauvegarder des données. Le résultat est simplement que les données sont sauvegardées après la mort et après la déconnexion.
Crédits
Rédaction :
Correction :
Mentions spéciales :
- diesieben07
- Lexmanos
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 -
J’attends des retours avant de le mettre dans à corriger.
-
Tu es sûr que EEP est déprécié ? Il n’y a pas de @Deprecated sur ce dernier.
Et dans la description du commit : https://github.com/MinecraftForge/MinecraftForge/commit/17db34ae31f281221b661b72b6831880ce31116b
il n’y a rien à propos de ce dernier.En tout cas bon tutoriel.
Il faudrait mettre 1.8.9 en balise comme cette fonction n’est pas présente en 1.8.
-
Ah, oui, je vais mettre la balise 1.8.9. Pour la déprécation, regarde sur le wiki de forge http://mcforge.readthedocs.org/en/latest/datastorage/extendedentityproperties/
-
Ah bah bravo le message de SCAREX est constructif je suis d’accord avec lui #devin !
-
Il y a un petit problème de sécurité : si tu modifies ton argent côté client et que tu envoies le paquet, le serveur va modifier la variable de son côté. Le paquet ne doit jamais être envoyé au serveur ! Les données doivent rester stocker côté serveur.
-
Je trouve qu’il y a beaucoup de superflu.
La synchronisation par exemple n’est pas un élément essentiel au système de Capabilities.
Il y a certains cas on n’a pas besoin de synchronisation client/serveur.
Pourtant on commence direct par la création de packet.
Pour moi le tutoriel doit être réorganisé et la synchronisation devrait être en bonus.Je ne comprend pas non plus pourquoi avoir associé le provider avec les data dans une seule et même classe.
-
Il y a autre chose, j’ai suivi le tutoriel, bon sur quelque chose de bien plus complexe mais au final c’est la même chose, j’ai une Capability et je doit dialoguer entre le serveur et le client. Et j’ai l’intuition qu’il devrait y avoir un event en plus : EntityJoinedWolrd. Quand un joueur se connecte, il n’a pas les donnés qui viennent du serveur, du coup, le joueur côté client se retrouve avec 0 en monnaie alors que côté serveur il a l’argent qu’il avait la dernière fois qu’il s’est connecté. C’est plutôt gênant quand tu veux afficher la valeur dans une GUI parce qu’on a seulement accès au joueur côté Client dans ce cas précis.
-
Comme RedRelay l’a dit, la synchronisation ne fait pas partie du système de capabilities, l’exemple que j’ai fait est un exemple random que je n’ai pas testé, sauf en solo. Le but était de montrer comment le nouveau système fonctionne, le reste est différent pour chacun.
-
Il existe des events qui peuvent de permettre de régler le problème (http://www.minecraftforgefrance.fr/forge-events.php) déclenchés sur serveur ou client (selon l’event) qui peuvent te permettre d’envoyer un packet pour sync le tout.
-
Salut, je suis en 1.11.2 et je ne trouve pas de méthode register(); dans ma classe de capabilities. Est-ce-que cela a changé depuis ?
-
Bonjour bonjour,
Ce tuto est-il encore d’actualité (en 1.12.2) ?
Je pose cette question car je l’ai suivi à la lettre, à priori eclipse ne repère pas d’erreurs et j’ai bien la capabilitie que je peux modifier (ajouter et retirer de l’argent).
Cependant il se trouve que l’argent se remet à 0 lorsque je relance le jeu. Cela se passe uniquement quand le jeu est relancé et non pas quand quand je quitte ma map et que j’y retourne juste après (sans avoir fermé le jeu). Pour y voir plus clair j’ai essayé de me connecter sur un serveur et il semple que la “money” se remet à 0 uniquement quand le serveur à été reboot ET que je m’y reco.
Comme ce tuto date d’il y a plusieurs version majeures de minecraft j’imagine que forge à du changer depuis et peut être impacter le contenu de ce tuto.
Pour ce qui est de mes classes, ce sont exactement les même que dans ce tuto mais je peux les fournir si besoin (peut-être que j’ai loupé quelque chose même si j’en doute car j’ai relu et revérifié tout plusieurs fois pour être sur).
Merci d’avance pour votre aide.
-
Salut,
Il doit avoir un problème au niveau de ton storage. Vérifies que les fonctions serialise et deserialise sont bien appelés et fonctionnent comme prévu.
Les capabilités n’ont pas beaucoup changé depuis leur introduction.
En revanche comme ce tutoriel n’est pas très détaillés, il faudrait en effet un nouveau tutoriel. -
@‘robin4002’:
Salut,
Il doit avoir un problème au niveau de ton storage. Vérifies que les fonctions serialise et deserialise sont bien appelés et fonctionnent comme prévu.
Les capabilités n’ont pas beaucoup changé depuis leur introduction.
En revanche comme ce tutoriel n’est pas très détaillés, il faudrait en effet un nouveau tutoriel.Alors je viens de vérifier et effectivement la fonction deserialize n’est jamais appelé (alors que la fonction serialise est bien appelée à la connection et à la déconnection).
Pourtant ma fonction deserialize est identique à celle du tuto (exepté le nom des variables car je ne compte pas de l’argent mais des kills de mobs) :
@Override public void deserializeNBT(NBTTagCompound compound) { this.setMobKill(compound.getInteger("Mob Kill")); }
Concernant ma classe storage ben la aussi c’est la même que dans le tuto (quasiment vide) :
public static class Storage implements Capability.IStorage <mobkillcapability>{ @Override public NBTBase writeNBT(Capability <mobkillcapability>capability, MobKillCapability instance, EnumFacing side) { return null; } @Override public void readNBT(Capability <mobkillcapability>capability, MobKillCapability instance, EnumFacing side, NBTBase nbt) { } }
-
Il y a un problème avec le tutoriel alors, car le storage ne doit pas rester vide.
Dans le storage appelle les fonctions deserializeNBT et serializeNBT comme ceci :@Override public NBTBase writeNBT(Capability <mobkillcapability>capability, MobKillCapability instance, EnumFacing side) { return instance.serializeNBT; } @Override public void readNBT(Capability <mobkillcapability>capability, MobKillCapability instance, EnumFacing side, NBTBase nbt) { instance.deserializeNBT((NBTTagCompound)nbt); } ```</mobkillcapability></mobkillcapability>
-
@‘robin4002’:
Il y a un problème avec le tutoriel alors, car le storage ne doit pas rester vide.
Dans le storage appelle les fonctions deserializeNBT et serializeNBT comme ceci :@Override public NBTBase writeNBT(Capability <mobkillcapability>capability, MobKillCapability instance, EnumFacing side) { return instance.serializeNBT; } @Override public void readNBT(Capability <mobkillcapability>capability, MobKillCapability instance, EnumFacing side, NBTBase nbt) { instance.deserializeNBT((NBTTagCompound)nbt); } ```</mobkillcapability></mobkillcapability>
Ça m’avais paru bizarre aussi mais comme c’est indiqué que le storage sert que pour les entités autres que celles de Minecraft (ce qui est faut du coup) je me suis pas posé plus de questions que ça.
En tout cas merci bien pour le coup de main ça marche nickel maintenant. -
@‘robin4002’:
Il y a un problème avec le tutoriel alors, car le storage ne doit pas rester vide.
Dans le storage appelle les fonctions deserializeNBT et serializeNBT comme ceci :@Override public NBTBase writeNBT(Capability <mobkillcapability>capability, MobKillCapability instance, EnumFacing side) { return instance.serializeNBT; } @Override public void readNBT(Capability <mobkillcapability>capability, MobKillCapability instance, EnumFacing side, NBTBase nbt) { instance.deserializeNBT((NBTTagCompound)nbt); } ```</mobkillcapability></mobkillcapability>
Ça m’avais paru bizarre sur le coup mais comme il est indiqué que storage sert pour les entités qui ne sont pas de minecraft j’ai pas fais plus gaffe que ça.
Du coup j’ai bien appelé les méthodes serialize et deserialize dans le storage comme indiqué ci-dessus cependant ça ne résout pas le problème et après 2-3 tests je me rends compte que les fonctions writeNBT et readNBT ne s’exécutent jamais. Je n’ai pas vu dans le code d’endroit ou ces fonctions sont appelées à part dans le register :
public static void register() { CapabilityManager.INSTANCE.register(MobKillCapability.class, new MobKillCapability.Storage(), new MobKillCapability.Factory()); }
D’ailleurs la fonction Factory est elle aussi quasiment vide (elle retourne null) j’ai regardé dans la doc de forge mais pas très bien compris à quoi elle sert et je suis pas sur que ce soit lié à mon problème.
Autre point que je viens de remarquer la variable mobkill qui est un int stockée dans la capabilitie (et qui se reset à chaque reboot du jeu) se reset en fait à la valeur indiquée dans les lignes ci-dessous :
public MobKillCapability(EntityPlayer player) { this.mobkill = 0; this.player = player; }
j’ai essayé de remplacer le 0 par la valeur stockée dans les nbt (en passant par la capabilitie) mais cela fait crash le jeu. J’imagine qu’en faisant ça le code essaye de récupérer la valeur de la capabilitie avant la création de celle ci. Je suis donc plutôt à court d’idées concernant mon problème surtout si y manque des lignes dans le tuto x)
-
La fonction register est appelé quelque part ?
Elle devrait l’être dans la classe init de ta classe principale. -
@‘robin4002’:
La fonction register est appelé quelque part ?
Elle devrait l’être dans la classe init de ta classe principale.Oui elle est bien appelée dans le Init et d’ailleurs la capability se crée bien et se save bien dans le NBT mais c’est comme si à la connection la valeur stockée dans le NBT n’était pas lue et du coup remplacée par la valeur par défault.
De ce que je vois c’est surement du à la fonction deserializeNBT qui n’est jamais appelée.
Voila toute la classe de la capabilitie, peut être y verras tu plus clair avec ça :
:::
package fr.frinn.coliseum.capability; import java.util.concurrent.Callable; import fr.frinn.coliseum.common.Coliseum; import fr.frinn.coliseum.network.PacketCapability; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.EnumFacing; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.capabilities.CapabilityManager; import net.minecraftforge.common.capabilities.ICapabilityProvider; import net.minecraftforge.common.capabilities.ICapabilitySerializable; public class MobKillCapability implements ICapabilityProvider, ICapabilitySerializable <nbttagcompound>{ @Override public NBTTagCompound serializeNBT() { NBTTagCompound compound = new NBTTagCompound(); compound.setInteger("Mob Kill", this.getMobKill()); return compound; } @Override public void deserializeNBT(NBTTagCompound compound) { this.setMobKill(compound.getInteger("Mob Kill")); } @Override public boolean hasCapability(Capability capability, EnumFacing facing) { return Coliseum.MOB_KILL_CAP != null && capability == Coliseum.MOB_KILL_CAP; } @Override public <t>T getCapability(Capability <t>capability, EnumFacing facing) { return Coliseum.MOB_KILL_CAP != null && capability == Coliseum.MOB_KILL_CAP ? (T)this : null; } public int mobkill; public void setMobKill(int mobkill) { this.mobkill = mobkill; } public int getMobKill() { return this.mobkill; } public static class Storage implements Capability.IStorage <mobkillcapability>{ @Override public NBTBase writeNBT(Capability <mobkillcapability>capability, MobKillCapability instance, EnumFacing side) { System.out.println("write"); return instance.serializeNBT(); } @Override public void readNBT(Capability <mobkillcapability>capability, MobKillCapability instance, EnumFacing side, NBTBase nbt) { System.out.println("read"); instance.deserializeNBT((NBTTagCompound)nbt); } } public static class Factory implements Callable <mobkillcapability>{ @Override public MobKillCapability call() throws Exception { return null; } } private EntityPlayer player; public static void register() { CapabilityManager.INSTANCE.register(MobKillCapability.class, new MobKillCapability.Storage(), new MobKillCapability.Factory()); } public MobKillCapability(EntityPlayer player) { this.mobkill = 0; this.player = player; } public void sync() { PacketCapability packet = new PacketCapability(this.getMobKill()); EntityPlayerMP playerMP = (EntityPlayerMP) player; Coliseum.network.sendTo(packet, playerMP); } }
:::
Coliseum est le nom de ma classe principale et MOB_KILL_CAP c’est ce qui est défini dans le @CapabilityInject</mobkillcapability></mobkillcapability></mobkillcapability></mobkillcapability></t></t></nbttagcompound>
-
Comme ça je ne vois pas de problème.
Sinon envoies un zip de ton dossier src, je vais faire des tests de mon côté dans la soirée.