Communiquer entre le client et le serveur : le réseau et les paquets


  • Moddeurs confirmés Rédacteurs

    Comme beaucoup de jeux et de logiciels, Minecraft est séparé en deux parties : le client et le serveur. Dans de nombreux cas, ces deux parties ont besoin de communiquer afin d'échanger des informations. Afin d'y parvenir, celles-ci utilisent ce que l'on appelle des paquets et nous allons voir comment créer nos propres paquets afin d'échanger nos propres informations.

    Rappel

    Dans le cas de Minecraft, que le client soit connecté à un serveur ou bien qu'il joue en solo, cette architecture client / serveur est présente, il faut donc bien faire attention à cette séparation dans tous les cas.

    Cas d'utilisation :

    Cas n° 1 : Nous possédons un système d'argent par joueur et nous voulons afficher la somme que le joueur possède sur son écran. Alors, il nous faut connaître cette dernière du côté client afin d'être capable d'effectuer le rendu. Il faut donc que le serveur communique cette somme au client.

    Cas n° 2 : Nous possédons une GUI (Graphical User Interface) avec des boutons et nous voulons être notifié côté serveur (rappelons qu'une GUI se situe côté client) afin d'effectuer une action associée à ce bouton. Il faut donc que le client communique cette information au serveur.

    Important

    La liste ci-dessus n'est pas une liste exhaustive des cas d'utilisation des paquets mais bien deux exemples parmi tant d'autres possibles.

    Sommaire du tutoriel

    Fonctionnement du network

    Si vous savez déjà comment fonctionne le système de paquets de Forge, vous pouvez passer cette partie.

    Pour communiquer entre le client et le serveur, Minecraft utilise un framework appelé Netty, vous n'aurez cependant pas à utiliser ce framework directement car que ce soit Minecraft ou Forge, chacun possède une surcouche facilitant la communication entre notre client et notre serveur.

    Afin de communiquer, il nous faudra faire transiter nos paquets via le réseau, pour cela nous devrons créer ce que l'on appelle des channels. Il faut voir un channel comme étant une route entre votre client et votre serveur sur laquelle vos paquets vont se déplacer, on peut avoir plusieurs routes entre un même client et le serveur. En pratique, on utilisera au minimum un channel par mod.

    Représentation des channels

    Pour ce qui est des paquets, il faut se les représenter comme un tas de données brutes (des octets) que l'on fait transiter via le réseau. Or, en Java, on utilise généralement des objets dans notre code. Afin de faire transiter nos objets, il nous faudra alors les sérialiser sous formes d'octets afin des les envoyer au client ou au serveur puis, arrivés là-bas il faudra les désérialiser afin de les retransformer en objets.

    Représentation de la sérialisation des paquets

    Afin d'appeler la bonne fonction de sérialisation et de désérialisation, Forge nous demande d'associer ensemble :

    • Une nombre unique (appelé discriminant)
    • Une classe (que l'on appellera T ici)
    • Une fonction de sérialisation pour la classe T
    • Une fonction de désérialisation pour la classe T
    • Une fonction qui sera appelée à la réception du paquet

    Ainsi, lorsqu'on demande à Forge d'envoyer un certain objet via le réseau, il cherche dans la liste des associations que l'on lui a fourni, l'association pour la classe de l'objet donné. Ensuite, il appelle la fonction de sérialisation sur l'instance à envoyer afin de transformer l'objet en octets. Enfin, il rajoute le discriminant au début des octets.

    Le tout est envoyé via le channel sur lequel est enregistré l'association.

    À la réception du paquet, Forge récupère le discriminant et cherche dans les associations celle possédant le discriminant du paquet reçu, il appelle alors la fonction de désérialisation afin de récupérer une instance de l'objet similaire à celle envoyée, puis passe cet objet à la fonction gérant la réception du paquet.

    Retenir

    Les associations telles que présentées ci-dessus sont enregistrées spécifiquement sur un channel. Il faut donc envoyer l'objet en utilisant le channel sur lequel on a enregistré l'association qui gérera ce paquet.

    Création d'un channel

    Nous allons tout d'abord créer une classe qui regroupera tout ce qui concerne le réseau (création des channels et enregistrement des paquets).
    Pour ma part je vais l'appeler TutorialNetwork. Le nom a peu d'importance, du moment que vous vous y retrouvez par la suite.
    Avant toute chose, nous allons avoir besoin de spécifier la version de notre channel, c'est une chaîne de caractères qui n'a de sens que pour vous (il est nécessaire de l'indiquer, mais vous ne vous en préoccuperez pas en réalité).
    Créons donc un champ contenant la version, dans mon cas il contiendra la valeur 1.

    public static final String PROTOCOL_VERSION = String.valueOf(1);
    

    Ensuite nous allons ajouter notre channel . La classe du channel est SimpleChannel et nous allons utiliser le builder NetworkRegistry.ChannelBuilder pour l'instancier :

    public static final SimpleChannel CHANNEL = NetworkRegistry.ChannelBuilder
            .named(new ResourceLocation(ModTutorial.MOD_ID, "my_channel"))
            .networkProtocolVersion(() -> PROTOCOL_VERSION)
            .clientAcceptedVersions(PROTOCOL_VERSION::equals)
            .serverAcceptedVersions(PROTOCOL_VERSION::equals)
            .simpleChannel();
    

    Voyons un peu ce que nous venons de faire ici :

    • Ligne 2 : .named prend une ResourceLocation représentant le nom de votre channel. Mettez l'id de votre mod en domaine (ici c'est mon ModTutorial.MOD_ID) puis ce que vous voulez pour le chemin (ici mon my_channel).

    • Ligne 3 : .networkProtocolVersion prend un Provider<String> qui doit renvoyer la version de votre protocol. Ici nous renvoyons le champ que nous avons créé plus haut.

    • Lignes 4 et 5 : celles-ci demandent un Predicate<String> afin de vérifier que la version sur le côté client et sur le côté serveur sont compatibles. Dans notre cas nous vérifions simplement que la version correspond à la version actuelle de notre protocol.

    • Ligne 6 : .simpleChannel() finalise la création de notre channel et nous renvoie son instance.

    Création d'un paquet

    Comme expliqué précédemment, un paquet est représenté par une classe, nous allons donc en créer une. Pour ma part, je l'appellerai MySimplePacket. Pour commencer, je ne vais y mettre qu'une valeur d'entier comme attribut :

    public class MySimplePacket
    {
        private int value;
    
        public MySimplePacket(int value)
        {
            this.value = value;
        }
    }
    

    Nous allons ensuite créer les fonctions de sérialisation et de désérialisation. Leur nom importe peu, mais :

    • La fonction de sérialisation doit prendre en paramètre l'objet à sérialiser (ici une instance de MySimplePacket et un PacketBuffer dans lequel on stockera l'objet sous forme d'octets. Elle ne doit rien retourner.

    • La fonction de désérialisation doit prendre en paramètre un PacketBuffer et retourner l'instance de l'objet désérialisé (ici une instance de MySimplePacket).

    Fonction de sérialisation :

    public static void encode(MySimplePacket packet, PacketBuffer buffer)
    {
        buffer.writeInt(packet.value);
    }
    

    Dans cette fonction j'écris donc l'entier de mon paquet dans le PacketBuffer.

    Fonction de désérialisation :

    public static MySimplePacket decode(PacketBuffer buffer)
    {
        int value = buffer.readInt();
        MySimplePacket instance = new MySimplePacket(value);
        return instance;
    }
    

    Et dans cette fonction, je récupère la valeur de mon entier dans le PacketBuffer puis la passe en paramètre au constructeur de mon objet que je retourne ensuite. NB : J'aurais pu directement mettre return new MySimplePacket(buffer.readInt()).

    Indication

    Le contenu du PacketBuffer que vous remplissez dans la fonction de sérialisation est le même que celui qui vous est donné dans la fonction de désérialisation. L'écriture et la lecture de données dans le PacketBuffer seront abordées plus en détails plus tard.

    Attention

    Le PacketBuffer représentant une suite d'octets, vous devez lire les valeurs dans le même ordre que celui dans lequel vous les avez écrites.

    Si vous avez suivi correctement le tutoriel jusqu'à maintenant vous devriez savoir qu'il ne reste plus qu'à créer la fonction qui gérera la réception de votre paquet. Cette fonction prend en paramètres une instance de votre paquet (ici une instance de MySimplePacket) et un Supplier<NetworkEvent.Context> et ne renvoie rien. Ce second argument permet de récupérer le contexte dans lequel le paquet a été envoyé. Il permet, entre autres, de récupérer l'instance du joueur côté serveur ou encore de savoir si nous sommes côté client ou côté serveur.

    Ajoutons donc cette fonction à notre classe :

    public static void handle(MySimplePacket packet, Supplier<NetworkEvent.Context> ctx)
    {
        System.out.println(packet.value);
        ctx.get().setPacketHandled(true);
    }
    

    Ici, j'affiche donc la valeur de mon paquet, puis j'appelle NetworkEvent.Context#setPacketHandled afin de dire que j'ai géré le paquet. Il est nécessaire de dire que nous avons géré le paquet sinon ce dernier est envoyé dans le code de Minecraft qui gère les paquets et affiche un message d'erreur.

    Nous sommes maintenant prêts à enregistrer notre paquet tout neuf sur notre channel.

    Enregistrement d'un paquet

    De retour dans notre classe TutorialNetwork. Nous allons créer une fonction dans laquelle nous enregistrerons nos paquets. Pour ma part, je vais l'appeler registerNetworkPackets. Ensuite, nous allons utiliser SimpleChannel#messageBuilder pour enregistrer notre paquet :

    public static void registerNetworkPackets()
    {
        CHANNEL.messageBuilder(MySimplePacket.class, 0)
                .encoder(MySimplePacket::encode)
                .decoder(MySimplePacket::decode)
                .consumer(MySimplePacket::handle)
                .add();
    }
    

    Le code précédent ne devrait pas être surprenant et vous devriez facilement faire le parallèle avec les associations dont je vous ai parlé auparavant. Mais analysons-le quand même :

    • Ligne 3 : .messageBuilder prend en paramètres la classe de votre paquet ainsi que le discriminant (cf. 1ère partie du tuto).

    • Ligne 4 : .encoder prend en paramètre la fonction de sérialisation.

    • Ligne 5 : .decoder prend en paramètre la fonction de désérialisation.

    • Ligne 6 : .consumer prend en paramètre le fonction qui va gérer votre paquet à sa réception.

    • Ligne 7: .add permet de finaliser l'enregistrement.

    Important

    Le discriminant doit être différent pour chaque enregistrement de paquet.

    Information

    Lors de l'enregistrement, vous verrez sûrement que le builder possède d'autres méthodes, cependant ne vous en préoccupez pas car elles ne sont utiles que pour Forge.

    Il suffit maintenant d'appeler la fonction registerNetworkPackets dans la fonction setup de votre mod (celle prenant en paramètre FMLCommonSetupEvent).

    public ModTutorial() {
        // [...]
        FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup);
        // [...]
    }
    
    private void setup(final FMLCommonSetupEvent event)
    {
        // [...]
        TutorialNetwork.registerNetworkPackets();
        // [...]
    }
    

    Envoyer un paquet

    Pour envoyer votre paquet il suffit maintenant d'appeler SimpleChannel#send pour le faire transiter du client au serveur ou du serveur à un ou des clients. Il vous faudra aussi vous aider des champs déclarés dans la classe PacketDistributor, je vous donne quelques exemples ci-dessous.

    Attention

    Vous ne pouvez pas envoyer un paquet d'un client à un autre, il faut forcément passer par le serveur !

    • Exemple d'envoi de notre paquet sur le serveur :
      Les deux lignes de code ci-dessous parviennent au même résultat.
    // Affichera la valeur 12 dans la console du serveur :
    
    // 1ère solution
    TutorialNetwork.CHANNEL.sendToServer(new MySimplePacket(12));
    // 2nd solution
    TutorialNetwork.CHANNEL.send(PacketDistributor.SERVER.noArg(), new MySimplePacket(12));
    
    • Exemple d'envoi de notre paquet sur le client :
      Pour envoyer un paquet à un client, il faut avoir une instance de EntityPlayerMP.
    public static void sendTo(EntityPlayerMP player)
    {
        // Affiche 10 dans la console du client
        TutorialNetwork.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), new MySimplePacket(10));
    }
    
    • Exemple d'envoi d'un paquet à tous les clients du serveur :
    // Affiche 42 dans la console de tous les clients
    TutorialNetwork.CHANNEL.send(PacketDistributor.ALL.noArg(), new MySimplePacket(42));
    
    • Exemple d'envoi d'un paquet à tous les clients se trouvant dans une dimension donnée :
    // Affiche 36 dans la console de tous les joueurs se trouvant dans le nether
    TutorialNetwork.CHANNEL.send(PacketDistributor.DIMENSION.with(() -> DimensionType.NETHER), new MySimplePacket(36));
    

    Voilà pour les exemples, n'hésitez pas à regarder la classe PacketDistributor pour découvrir les autres filtres d'envoi.

    Important

    Vous pouvez envoyer un paquet depuis n'importe quelle partie de votre code.

    Approfondissement

    J'ai volontairement survolé quelques points dans les sections précédentes car ce n'était pas forcément le moment d'en parler, mais ce moment est arrivé.

    • Sérialiser/Désérialiser vos objets :

    La sérialisation et la désérialisation de vos objets est une étape importante la création de votre paquet. Il va en effet généralement vous falloir découper votre objets en des objets ou type primitifs facilement sérialisables (String, int, boolean, UUID, etc ...).
    Je ne vais pas faire l'inventaire des types facilement sérialisables via le PacketBuffer, pour cela il vous suffit de chercher toutes les méthodes qui commencent par write dans cette classe.
    Je vais cependant donner un exemple de sérialisation d'une collection d'objets de la classe suivante :

    public class MyComplexObject
    {
        private String foo;
        private int bar;
        private UUID foobar;
        private ItemStack stack;
    }
    

    Dans un premier temps je vais écrire une méthode qui va sérialiser une instance de MyComplexObject dans un PacketBuffer :

    public void writeToBuffer(PacketBuffer buffer)
    {
        buffer.writeString(this.foo);
        buffer.writeInt(this.bar);
        buffer.writeUniqueId(this.foobar);
        buffer.writeItemStack(this.stack);
    }
    

    Ensuite, je vais écrire une méthode qui va désérialiser cet objet. Pour ce faire, je vais créer un constructeur qui prend un PacketBuffer en entrée :

    public MyComplexObject(PacketBuffer buffer)
    {
        this.foo = buffer.readString(32767); // 32767 correspond à la longueur max de la chaine de caractères
        this.bar = buffer.readInt();
        this.foobar = buffer.readUniqueId();
        this.stack = buffer.readItemStack();
    }
    

    On rappelle qu'il faut lire les données dans le même ordre qu'elles ont été écrites.
    Une fois tout cela fait, je crée mon paquet :

    public class MyComplexPacket
    {
        private Collection<MyComplexObject> myCollection;
        
        public MyComplexPacket(Collection<MyComplexObject> collection)
        {
            this.myCollection = collection;
        }
    }
    

    Puis je crée les fonctions de sérialisation et de désérialisation du paquet :

    public static void encode(MyComplexPacket pck, PacketBuffer buf)
    {
        // J'écris le nombre d'objet que je vais sérialiser
        buf.writeInt(pck.myCollection.size());
        
        // Puis je sérialiser chaque objet
        pck.myCollection.forEach(o -> o.writeToBuffer(buf));
    }
    
    public static MyComplexPacket decode(PacketBuffer buffer)
    {
        // Je lis combien d'objet on été sérialisés
        int collectionSize = buffer.readInt();
        
        // Puis je lis autant d'objet qui ont été sérialisés
        Collection<MyComplexObject> complexObjects = Stream
                .generate(() -> new MyComplexObject(buffer))
                .limit(collectionSize)
                .collect(Collectors.toList());
        
        // Enfin, je retourne l'instance de mon paquet
        return new MyComplexPacket(complexObjects);
    }
    

    Et voilà, vous savez maintenant sérialiser et désérialiser une collection d'objets.

    • Quelques informations sur le NetworkEvent.Context :

    Comme vu précédemment, lorsque l'on gère l'arrivé d'un paquet, on nous met à disposition un Supplier<NetworkEvent.Context>. Ici, nous allons voir légèrement plus en profondeur ce que ce dernier nous donne comme informations.

    Dans un premier temps, vous pouvez tout simplement récupérer le contexte dans lequel le paquet a été reçu en appelant Supplier#get qui va vous retourner une instance de NetworkEvent.Context, c'est celui-ci qui nous intéresse bien sûr.

    • Context#getDirection vous permet de récupérer une énumération indiquant vers où le paquet a été envoyé. NetworkDirection#PLAY_TO_SERVER et NetworkDirection#LOGIN_TO_SERVER pour client vers serveur, NetworkDirection#PLAY_TO_CLIENT et NetworkDirection#LOGIN_TO_CLIENT pour serveur vers client. La différence entre PLAY et LOGIN correspond au moment où le paquet est envoyé, PLAY si l'entité du joueur a été créée, LOGIN si l'entité n'a pas encore été créée et que le client est encore en phase de chargement. Vous ne devriez avoir à gérer que le premier cas (PLAY).

    • Context#enqueueWork permet de définir un travail qui sera réalisé au prochain tick sur le thread du client ou du serveur (pas sur le thread de Netty).

    • Context#getSender renvoie une instance de EntityPlayerMP correspondante au client qui a envoyé le paquet au serveur. Comme vous venez de le comprendre, celle-ci n'est utilisable que sur le serveur.

    • Context#attr permet d'accéder à un certain attribut du channel entre le serveur et le client. L'utilisation de cette fonctionnalité correspond à des cas particulier que vous ne rencontrerez sûrement pas, je ne vais donc pas m'étendre plus longtemps dessus (mais sachez que ça existe).

    • SimpleChannel#reply(NetworkEvent.Context, MSG) permet de renvoyer un message à celui qui vous en a envoyé un.

    Je pense que nous avons maintenant fait le tour de ce qu'il y a à voir, j'espère que vous compris comment fonctionne le network et je vous souhaite du bon modding.

    Résultat sur Github

    Vous pouvez consulter le commit concernant ce tutoriel sur le Github de MinecraftForgeFrance.

    Creative Commons

    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

    retourRetour vers le sommaire des tutoriels



  • Salut Broken,

    merci pour ce tutoriel très complet !

    J'ai cependant une question qui pourra peut-être aider plus d'un!

    Pour un packet envoyé au client, comme tu le précises le Context#getSender ne fonctionne pas car nous sommes du côté client, il faudrait donc utiliser Minecraft#getInstance#player sauf que lors de l'enregistrement côté client aucun problème mais côté coté serveur la classe EntityPlayerSP n'est pas trouvé (ce qui est logique).

    Comment faut-il donc procéder ? Car un OnlyIn(Dist.CLIENT) au dessus du handler ne fonctionne pas ?

    Merci encore 🙂


  • Moddeurs confirmés Rédacteurs Administrateurs

    Salut,

    Tu as aussi le problème si tu mets le code utilisant EntityPlayerSP dans la fonctionctx.get().enqueueWork(() -> { }); ?



  • Salut,

    oui..

    f05b4f04-aca5-4860-9767-a4618ae53e8d-image.png


  • Moddeurs confirmés Rédacteurs Administrateurs

    ctx.get().enqueueWork(new Runnable() {
        @Override
        @OnlyIn(Dist.CLIENT)
        public void run() {
            EntityPlayerSP player = Minecraft.getInstance().player;
            ....
        }
    });
    

    Comme ça je pense que ça sera bon. Par contre c'est moins bien pour la lisibilité (pas possible d'appliquer l'annotation sur une lambda).



  • @robin4002 a dit dans Communiquer entre le client et le serveur : le réseau et les paquets :

    ctx.get().enqueueWork(new Runnable() { @Override @OnlyIn(Dist.CLIENT) public void run() { EntityPlayerSP player = SGClient.MC.player; .... } });

    Toujours pas 😕

    Voici l'erreur

    	Index: 1
    	Listeners:
    		0: NORMAL
    		1: net.minecraftforge.eventbus.EventBus$$Lambda$1843/1994991750@5b112b8d
    java.lang.BootstrapMethodError: java.lang.NoSuchMethodError: fr.fifoube.packets.PacketMoneyData.handle(Lfr/fifoube/packets/PacketMoneyData;Ljava/util/function/Supplier;)V
    	at fr.fifoube.packets.PacketsRegistery.registerNetworkPackets(PacketsRegistery.java:18)
    	at fr.fifoube.main.ModEconomyInc.setup(ModEconomyInc.java:74)
    	at net.minecraftforge.eventbus.EventBus.doCastFilter(EventBus.java:209)
    	at net.minecraftforge.eventbus.EventBus.lambda$addListener$11(EventBus.java:201)
    	at net.minecraftforge.eventbus.EventBus.post(EventBus.java:266)
    	at net.minecraftforge.fml.javafmlmod.FMLModContainer.fireEvent(FMLModContainer.java:105)
    	at java.util.function.Consumer.lambda$andThen$0(Unknown Source)
    	at java.util.function.Consumer.lambda$andThen$0(Unknown Source)
    	at net.minecraftforge.fml.ModContainer.transitionState(ModContainer.java:111)
    	at net.minecraftforge.fml.ModList.lambda$null$9(ModList.java:120)
    	at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
    	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(Unknown Source)
    	at java.util.stream.AbstractPipeline.copyInto(Unknown Source)
    	at java.util.stream.ForEachOps$ForEachTask.compute(Unknown Source)
    	at java.util.concurrent.CountedCompleter.exec(Unknown Source)
    	at java.util.concurrent.ForkJoinTask.doExec(Unknown Source)
    	at java.util.concurrent.ForkJoinTask.doInvoke(Unknown Source)
    	at java.util.concurrent.ForkJoinTask.invoke(Unknown Source)
    	at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(Unknown Source)
    	at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(Unknown Source)
    	at java.util.stream.AbstractPipeline.evaluate(Unknown Source)
    	at java.util.stream.ReferencePipeline.forEach(Unknown Source)
    	at java.util.stream.ReferencePipeline$Head.forEach(Unknown Source)
    	at net.minecraftforge.fml.ModList.lambda$dispatchParallelEvent$10(ModList.java:120)
    	at java.util.concurrent.ForkJoinTask$AdaptedRunnableAction.exec(Unknown Source)
    	at java.util.concurrent.ForkJoinTask.doExec(Unknown Source)
    	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(Unknown Source)
    	at java.util.concurrent.ForkJoinPool.runWorker(Unknown Source)
    	at java.util.concurrent.ForkJoinWorkerThread.run(Unknown Source)
    Caused by: java.lang.NoSuchMethodError: fr.fifoube.packets.PacketMoneyData.handle(Lfr/fifoube/packets/PacketMoneyData;Ljava/util/function/Supplier;)V
    	at java.lang.invoke.MethodHandleNatives.resolve(Native Method)
    	at java.lang.invoke.MemberName$Factory.resolve(Unknown Source)
    	at java.lang.invoke.MemberName$Factory.resolveOrFail(Unknown Source)
    	at java.lang.invoke.MethodHandles$Lookup.resolveOrFail(Unknown Source)
    	at java.lang.invoke.MethodHandles$Lookup.linkMethodHandleConstant(Unknown Source)
    	at java.lang.invoke.MethodHandleNatives.linkMethodHandleConstant(Unknown Source)
    	... 29 more
    
    [23:25:32.445] [modloading-worker-1/ERROR] [ne.mi.fm.ja.FMLModContainer/LOADING]: Caught exception during event FMLCommonSetupEvent dispatch for modid economyinc
    java.lang.BootstrapMethodError: java.lang.NoSuchMethodError: fr.fifoube.packets.PacketMoneyData.handle(Lfr/fifoube/packets/PacketMoneyData;Ljava/util/function/Supplier;)V
    	at fr.fifoube.packets.PacketsRegistery.registerNetworkPackets(PacketsRegistery.java:18) ~[main/:?]
    	at fr.fifoube.main.ModEconomyInc.setup(ModEconomyInc.java:74) ~[main/:?]
    	at net.minecraftforge.eventbus.EventBus.doCastFilter(EventBus.java:209) ~[eventbus-0.8.1-service.jar:?]
    	at net.minecraftforge.eventbus.EventBus.lambda$addListener$11(EventBus.java:201) ~[eventbus-0.8.1-service.jar:?]
    	at net.minecraftforge.eventbus.EventBus.post(EventBus.java:266) ~[eventbus-0.8.1-service.jar:?]
    	at net.minecraftforge.fml.javafmlmod.FMLModContainer.fireEvent(FMLModContainer.java:105) ~[?:25.0]
    	at java.util.function.Consumer.lambda$andThen$0(Unknown Source) ~[?:1.8.0_201]
    	at java.util.function.Consumer.lambda$andThen$0(Unknown Source) ~[?:1.8.0_201]
    	at net.minecraftforge.fml.ModContainer.transitionState(ModContainer.java:111) ~[?:?]
    	at net.minecraftforge.fml.ModList.lambda$null$9(ModList.java:120) ~[?:?]
    	at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source) ~[?:1.8.0_201]
    	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(Unknown Source) ~[?:1.8.0_201]
    	at java.util.stream.AbstractPipeline.copyInto(Unknown Source) ~[?:1.8.0_201]
    	at java.util.stream.ForEachOps$ForEachTask.compute(Unknown Source) ~[?:1.8.0_201]
    	at java.util.concurrent.CountedCompleter.exec(Unknown Source) ~[?:1.8.0_201]
    	at java.util.concurrent.ForkJoinTask.doExec(Unknown Source) ~[?:1.8.0_201]
    	at java.util.concurrent.ForkJoinTask.doInvoke(Unknown Source) ~[?:1.8.0_201]
    	at java.util.concurrent.ForkJoinTask.invoke(Unknown Source) ~[?:1.8.0_201]
    	at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(Unknown Source) ~[?:1.8.0_201]
    	at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(Unknown Source) ~[?:1.8.0_201]
    	at java.util.stream.AbstractPipeline.evaluate(Unknown Source) ~[?:1.8.0_201]
    	at java.util.stream.ReferencePipeline.forEach(Unknown Source) ~[?:1.8.0_201]
    	at java.util.stream.ReferencePipeline$Head.forEach(Unknown Source) ~[?:1.8.0_201]
    	at net.minecraftforge.fml.ModList.lambda$dispatchParallelEvent$10(ModList.java:120) ~[?:?]
    	at java.util.concurrent.ForkJoinTask$AdaptedRunnableAction.exec(Unknown Source) [?:1.8.0_201]
    	at java.util.concurrent.ForkJoinTask.doExec(Unknown Source) [?:1.8.0_201]
    	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(Unknown Source) [?:1.8.0_201]
    	at java.util.concurrent.ForkJoinPool.runWorker(Unknown Source) [?:1.8.0_201]
    	at java.util.concurrent.ForkJoinWorkerThread.run(Unknown Source) [?:1.8.0_201]
    Caused by: java.lang.NoSuchMethodError: fr.fifoube.packets.PacketMoneyData.handle(Lfr/fifoube/packets/PacketMoneyData;Ljava/util/function/Supplier;)V
    	at java.lang.invoke.MethodHandleNatives.resolve(Native Method) ~[?:1.8.0_201]
    	at java.lang.invoke.MemberName$Factory.resolve(Unknown Source) ~[?:1.8.0_201]
    	at java.lang.invoke.MemberName$Factory.resolveOrFail(Unknown Source) ~[?:1.8.0_201]
    	at java.lang.invoke.MethodHandles$Lookup.resolveOrFail(Unknown Source) ~[?:1.8.0_201]
    	at java.lang.invoke.MethodHandles$Lookup.linkMethodHandleConstant(Unknown Source) ~[?:1.8.0_201]
    	at java.lang.invoke.MethodHandleNatives.linkMethodHandleConstant(Unknown Source) ~[?:1.8.0_201]
    	... 29 more
    [23:25:32.492] [modloading-worker-2/DEBUG] [ne.mi.fm.ja.FMLModContainer/LOADING]: Fired event for modid forge : FMLCommonSetupEvent
    [23:25:32.494] [Server thread/FATAL] [ne.mi.fm.ModLoader/]: Failed to complete lifecycle event SETUP, 1 errors found
    [23:25:32.494] [Server thread/ERROR] [minecraft/MinecraftServer]: Encountered an unexpected exception
    net.minecraftforge.fml.LoadingFailedException: null
    	at net.minecraftforge.fml.ModLoader.dispatchAndHandleError(ModLoader.java:157) ~[?:?]
    	at net.minecraftforge.fml.ModLoader.loadMods(ModLoader.java:144) ~[?:?]
    	at net.minecraftforge.fml.server.ServerModLoader.begin(ServerModLoader.java:44) ~[?:?]
    	at net.minecraft.server.dedicated.DedicatedServer.init(DedicatedServer.java:119) ~[?:?]
    	at net.minecraft.server.MinecraftServer.run(MinecraftServer.java:595) [?:?]
    	at java.lang.Thread.run(Unknown Source) [?:1.8.0_201]
    [23:25:32.504] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Starting version check at https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json
    [23:25:32.522] [Server thread/ERROR] [minecraft/MinecraftServer]: This crash report has been saved to: D:\Documents_D\EconomyInc\MC1132\EconomyIncV16MC1132v250193\run\.\crash-reports\crash-2019-05-10_23.25.32-server.txt
    [23:25:32.546] [Server thread/INFO] [minecraft/MinecraftServer]: Stopping server
    [23:25:32.547] [Server thread/INFO] [minecraft/MinecraftServer]: Saving worlds
    [23:25:32.550] [Server Shutdown Thread/INFO] [minecraft/MinecraftServer]: Stopping server
    [23:25:32.551] [Server Shutdown Thread/INFO] [minecraft/MinecraftServer]: Saving worlds
    

    EDIT : Je n'ai rien dis, ca fonctionne! J'avais oublier d'enlever l'autre OnlyIn x)

    Merci!


  • Moddeurs confirmés Rédacteurs Administrateurs

    Ah visiblement il cherche quand même la méthode run, donc en effet ce n'est bon.
    Ceci devrait aussi fonctionner :

    ctx.get().enqueueWork(() -> {
        DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> {
            EntityPlayerSP player = Minecraft.getInstance().player;
        });
    });
    


  • @robin4002 a dit dans Communiquer entre le client et le serveur : le réseau et les paquets :

    Ah visiblement il cherche quand même la méthode run, donc en effet ce n'est bon.
    Pas d'autres choix que faire ça alors :

    DistExecutor.runWhenOn(Dist.CLIENT, () -> () -> {
        EntityPlayerSP player = Minecraft.getInstance().player;
    });
    

    J'avais oublier d'enlever l'autre OnlyIn sur le handle, ca fonctionne parfaitement. Maintenant tu dis que ce n'est pas "propre", tu vois une alternative plus propre ?


  • Moddeurs confirmés Rédacteurs Administrateurs

    C'est entièrement valide concernant la logique, donc c'est propre à ce niveau.

    C'est par contre plus lourd niveau syntaxe que l'écriture avec une fonction lambda.



  • @robin4002 Ok je vois, donc pas d'autre possibilité de faire alors ? A part ce que tu as mis au dessus ? C'était beaucoup plus propre en 1.12.2


  • Moddeurs confirmés Rédacteurs Administrateurs

    Je ne pense pas qu'il y a d'autres moyens.
    EDIT en fait si, comme ça :

    public static void handle(PacketXXXX packet, Supplier<NetworkEvent.Context> ctx) {
        ctx.get().enqueueWork(() -> handleClient(packet));
        ctx.get().setPacketHandled(true);
    }
    
    @OnlyIn(Dist.CLIENT)
    public static void handleClient(PacketXXXX packet) {
        EntityPlayerSP player = Minecraft.getInstance().player;
    }