MFF

    Minecraft Forge France
    • Récent
    • Mots-clés
    • Populaire
    • Utilisateurs
    • Groupes
    • Forge Events
      • Automatique
      • Foncé
      • Clair
    • S'inscrire
    • Se connecter

    Créer un coremod pour modifier les classes de Minecraft

    Planifier Épinglé Verrouillé Déplacé Autres
    1.7.10
    11 Messages 5 Publieurs 6.7k Vues 2 Watching
    Charger plus de messages
    • Du plus ancien au plus récent
    • Du plus récent au plus ancien
    • Les plus votés
    Répondre
    • Répondre à l'aide d'un nouveau sujet
    Se connecter pour répondre
    Ce sujet a été supprimé. Seuls les utilisateurs avec les droits d'administration peuvent le voir.
    • SCAREXS Hors-ligne
      SCAREX
      dernière édition par robin4002

      Sommaire

      • Introduction
      • Pré-requis
      • Code
        • IFMLLoadingPlugin
        • build.gradle
        • IClassTransformer
      • Crédits

      Introduction

      Dans ce tutoriel je vais vous montrer comment modifier les classes de Minecraft.

      ATTENTION ! Lisez d’abord cette partie avant d’attaquer le tutoriel

      Ce tutoriel est réservé à des développeurs avancés dû à sa complexité. Cette méthode n’est à utiliser qu’en dernier recours, si vous avez un autre moyen d’arriver à vos fins sans faire un coremod utilisez l’autre méthode car les coremods sont très instables donc vous risquez d’avoir des problèmes de compatibilité avec d’autres mods ou si la version de minecraft change.

      Pré-requis

      • ASM (Conseillé pour la compréhension mais pas obligatoire)

      Code

      IFMLLoadingPlugin :

      @IFMLLoadingPlugin.MCVersion("1.7.10") // Indique la version de minecraft afin que le jeu crash si la version n'est pas bonne (ça permet d'éviter de faire n'importe quoi si la version n'est pas supportée)
      public class TutorialModLoadingPlugin implements IFMLLoadingPlugin // On doit implémenter IFMLLoadingPlugin
      {
          @Override
          public String[] getASMTransformerClass() { // Renvoi d'une liste de String qui renvoient vers des IClassTransformer, c'est ça qu'on va utiliser pour modifier les classes de Minecraft
              return new String[] { TutorialModClassTransformer.class.getName() };
          }
      
          @Override
          public String getModContainerClass() { // Renvoi un String qui indique la classe du mod (qui doit implémenter ModContainer), ici on ne l'utilisera pas
              return null;
          }
      
          @Override
          public String getSetupClass() { // Renvoi un String qui indique la classe de setup (qui doit implémenter IFMLCallHook), ici on ne l'utilisera pas
              return null;
          }
      
          @Override
          public void injectData(Map <String, Object> data) {} // Permet de modifier plusieurs variables de lancement
      
          @Override
          public String getAccessTransformerClass() { // Renvoi un String qui indique une classe qui modifie les accès aux variables (qui doit étendre AccessTransformer)
              return null;
          }
      }
      

      build.gradle :

      Afin d’appeler notre coremod nous allons devoir le préciser dans le META-INF :

      jar {
          manifest {
              attributes 'FMLCorePlugin': '<lien vers votre plugin>', 'FMLCorePluginContainsFMLMod': '<true s il y a un mod dans votre coremod>'
          }
      }
      

      Si vous voulez charger votre plugin lorsque vous développez (ce que je pense vous ferez), il vous faut rajouter dans les arguments de la JVM (pour ceux qui sont sur eclipse c’est le cadre du bas, pas celui du haut lorsque vous éditez le run/debug configuration) : -Dfml.coreMods.load= <lien vers votre plugin>[ancre=ct]IClassTransformer :
      Maintenant nous allons devoir créer une classe qui implémente IClassTransformer :

      public class TutorialModClassTransformer implements IClassTransformer
      {
          @Override
          public byte[] transform(String name, String transformedName, byte[] basicClass) {
              return basicClass;
          }
      }
      

      La fonction transform sera appelée lorsqu’une classe sera chargée dans la JVM.

      Nous on allons donc ajouter une liste de conditions pour changer les bonnes classes.
      Pour chaque classe que vous voulez modifier, il va vous falloir son non-obfusqué et son nom obfusqué (le nom obfusqué de minecraft et non de fml). Pour trouver le nom non-obfusqué c’est très simple : c’est le lien vers la classe en question que vous avez dans votre environnement de développement (ex : net.minecraft.world.Explosion).
      Pour le nom obfusqué de minecraft c’est plus complexe :

      • Récupérer le nom de la classe (ex: net/minecraft/world/Explosion)

      • Maintenant nous allons aller chercher le fichier correspondant à votre version :

      • Si la version de minecraft est supérieure à la 1.7 et vous utilisez des mappings stables, rendez-vous dans GRADLE_HOME/caches/minecraft/de/oceanlabs/mcp/mcp_stable/<version des mappings>/srgs/notch-mcp.srg

      • Si la version de minecraft est supérieure à la 1.7 et vous utilisez des mappings en snapshot, rendez-vous dans GRADLE_HOME/caches/minecraft/de/oceanlabs/mcp/mcp_snapshot/<version des mappings>/srgs/notch-mcp.srg

      • Si la version de minecraft est inférieure ou égale à la 1.7, rendez-vous dans GRADLE_HOME/caches/minecraft/net/minecraftforge/forge/<version de forge>/srgs/notch-mcp.srg

      • Ouvrir le fichier avec un éditeur de texte (notepad sous windows ou Notepad++ par exemple)

      • Maintenant faites une recherche avec le nom de classe (comme marqué au-dessus). Normalement le premier résultat devrait avoir “CL:” au début de la ligne, si ce n’est pas le cas regardez dans le deuxième bloc de la ligne et prenez la partie avant le “/”

      Une fois ça de fait rajoutez cette condition dans la fonction transform :

      @Override
      public byte[] transform(String name, String transformedName, byte[] basicClass) {
          if (name.equals("<nom obfusqué>") || name.equals("<nom normal>")) {
              // Par exemple ici pour l'explosion : <nom obfusqué>=agw et <nom normal>=net.minecraft.world.Explosion
              TutorialMod.LOGGER.info("About to patch : " + name);
              return patchNomDeLaClasse(name, basicClass, name.equals("<nom obfusqué>"));
          }
          return basicClass;
      }
      

      Vous devriez avoir une erreur puisque la méthode n’existe pas encore, créez la :

      private byte[] patchNomDeLaClasse(String name, byte[] basicClass, boolean obf) {
          return basicClass;
      }
      

      C’est là que les choses commencent à devenir compliqué.

      ATTENTION : pour tous les imports nécessaires, ils se trouvent dans org.objectweb.asm ! Si vous importez des classes d’un autre package ça ne marchera pas.

      Dans un premier temps, créez un ClassNode, celui ci va contenir toutes les informations concernant la classe chargée ainsi qu’un ClassReader qui va lire le contenu et de la classe et un ClassWriter qui va nous permettre d’écrire la classe :

      private byte[] patchNomDeLaClasse(String name, byte[] basicClass, boolean obf) {
          ClassNode classNode = new ClassNode();
          ClassReader classReader = new ClassReader(basicClass);
          classReader.accept(classNode, ClassReader.EXPAND_FRAMES);
      
          // C'est ici qu'ira notre code qui a besoin du contenu de la classe
      
          ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
          classNode.accept(cw);
      
          // C'est ici qu'ira notre code qui doit écrire la classe sans avoir à la lire
      
          return cw.toByteArray();
      }
      

      Maintenant vous avez plusieurs choix :

      • apprendre le bytecode par coeur (bon courage 😉 )
      • Si vous êtes sur eclipse : télécharger Bytecode outline plugin
      • Sur idea je ne connais pas de plugin, si vous en connaissez dites les moi pour que je le rajoute

      Pour ceux qui choisiront d’utiliser le Bytecode outline plugin, rendez vous dans Window > Show View > other > java > Bytecode (double-cliquez dessus).
      Maintenant un onglet à droite apparaît avec le bytecode de la classe (Si ce n’est pas le cas appuyez sur les 2 flèches en haut qui permettent de lier l’éditeur avec cet onglet).

      Malheureusement je ne peux pas vous dire quoi faire dans votre cas puisque tout dépend de votre situation mais je vais vous donner 2 exemples qui couvrent une bonne partie des choses voulues :

      • Dans le premier cas nous allons ajouter une interface à une classe ainsi que lui rajouter les méthodes correspondantes. Et nous allons aussi modifier une méthode afin d’utiliser les événements pour assurer un maximum de compatibilité entre les mods
      • Dans le deuxième cas nous allons simplement afficher du texte dans la console lorsqu’une explosion apparaît (c’est un exemple, vous pouvez faire beaucoup plus).

      Premier cas :

      Rajouter une interface :

          private byte[] patchNomDeLaClasse(String name, byte[] basicClass, boolean obf) {
          ClassNode cnode = new ClassNode();
          ClassReader cr = new ClassReader(basicClass);
          cr.accept(cnode, ClassReader.EXPAND_FRAMES);
      
          cnode.interfaces.add("<lien vers votre interface avec des slashs (ex: fr scarex tutorialmod core icanfall)>");
      
          ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
          cnode.accept(cw);
          return cw.toByteArray();
      }
      

      Rajouter les fonctions de l’interface :

          private byte[] patchNomDeLaClasse(String name, byte[] basicClass, boolean obf) {
          ClassNode cnode = new ClassNode();
          ClassReader cr = new ClassReader(basicClass);
          cr.accept(cnode, ClassReader.EXPAND_FRAMES);
      
          cnode.interfaces.add("fr/scarex/tutorialmod/core/ICanFall");
      
          ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
          cnode.accept(cw);
      
          try { 
              // On est obligés d'utiliser un try-catch à cause de <votre interface>.getMethod, vous pouvez aussi mettre une simple chaîne de caractère si vous savez comment noter la description d'une méthode (J'explique comment dans [mon tuto sur les Access Transformer](https://www.minecraftforgefrance.fr/showthread.php?tid=3686)), malheureusement vous devrez faire en fonction de si la classe est obfusquée ou non sinon la description n'est pas la même alors qu'avec cette méthode vous n'avez pas ce problème
              MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "canBlockFall", Type.getMethodDescriptor(<votre interface>.class.getMethod("<nom de votre méthode>", <liste de tous les paramètres>)), null, <liste de string renvoyant vers les exceptions lancées par la fonction>);
              mv.visitCode();
              // Ici vous pouvez utiliser le BytecodeOutline plugin pour compléter : en haut cliquez sur "Show ASMified code", cela vous montrera le code en asm de votre méthode
              mv.visitEnd();
          } catch (Exception e) {
              e.printStackTrace();
          }
      
          return cw.toByteArray();
      }
      

      Rajouter l’appel vers un Event après la première occurrence d’un certain bytecode:
      Tout d’abord il va vous falloir le nom obfusqué de la méthode (celui de minecraft), pour faire ceci c’est la même méthode qu’avec la classe mais au début de la ligne vous devriez avoir “MD:”
      Ensuite il va vous falloir récupérer la description de la méthode (J’explique comment dans mon tuto sur les Access Transformer).
      Ensuite pour nous simplifier la vie nous allons créer une classe ASMHelper où l’on va mettre une fonction appelée findMethod qui va nous permettre de trouver la méthode correspondante (au passage vous pouvez rajoutr la fonction getFirstInstrWithOpcode qui va nous permettre de trouver la première occurence d’un certain bytecode) :

      public final class ASMHelper
      {
          public static MethodNode findMethod(ClassNode cnode, String name, String desc) {
              for (MethodNode m : cnode.methods) {
                  if (name.equals(m.name) && desc.equals(m.desc)) return m;
              }
              return null;
          }
      
          public static AbstractInsnNode getFirstInstrWithOpcode(MethodNode mn, int opcode) {
              Iterator <abstractinsnnode>ite = mn.instructions.iterator();
              while (ite.hasNext()) {
                  AbstractInsnNode n = ite.next();
                  if (n.getOpcode() == opcode) return n;
              }
              return null;
          }
      }
      

      Vous devriez avoir quelque chose comme ça :

          private byte[] patchNomDeLaClasse(String name, byte[] basicClass, boolean obf) {
          ClassNode cnode = new ClassNode();
          ClassReader cr = new ClassReader(basicClass);
          cr.accept(cnode, ClassReader.EXPAND_FRAMES);
      
          String methodName= obf ? "<nom obfusqué de la méthode>" : "<nom non-obfusqué de la méthode>";
          String className = obf ? "<nom obfusqué de la classe>" : "<nom non-obfusqué de la classe>";
      
          MethodNode mn = ASMHelper.findMethod(cnode, methodName, "<votre description de méthode>"); // On récupère la méthode
          AbstractInsnNode nodeif = ASMHelper.getFirstInstrWithOpcode(mn, Opcodes.<votre opcode>); // On récupère la première occurence de notre opcode
          InsnList insnList = new InsnList(); // On créer une liste d'instructions
      
          mn.instructions.insert(nodeif, insnList); // On ajoute le code
      
          ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
          cnode.accept(cw);
          return cw.toByteArray();
      }
      

      Pour utiliser les événements de forge, il va vous falloir 2 choses :

      • La classe de votre event (doit étendre cpw.mods.fml.common.eventhandler.Event)
      • Une classe qui gérera l’appel des events comme la ForgeEventFactory. VOUS DEVEZ PLACER LE POST DE L’EVENT AUTRE PART QUE DANS VOTRE PLUGIN FML ! Sinon votre event ne sera pas enregistré correctement

      Maintenant lisez le bytecode donné par le plugin, et ajoutez tout ce qui est demandé dans la liste InsnList, normalement vous devriez avoir quelque chose qui ressemble à ça :

      private byte[] patchNomDeLaClasse(String name, byte[] basicClass, boolean obf) {
          ClassNode cnode = new ClassNode();
          ClassReader cr = new ClassReader(basicClass);
          cr.accept(cnode, ClassReader.EXPAND_FRAMES);
      
          String methodName= obf ? "<nom obfusqué de la méthode>" : "<nom non-obfusqué de la méthode>";
          String className = obf ? "<nom obfusqué de la classe>" : "<nom non-obfusqué de la classe>";
      
          MethodNode mn = ASMHelper.findMethod(cnode, methodName, "<votre description de méthode>"); // On récupère la méthode
          AbstractInsnNode nodeif = ASMHelper.getFirstInstrWithOpcode(mn, Opcodes.<votre opcode>); // On récupère la première occurence de notre opcode
          InsnList insnList = new InsnList(); // On créer une liste d'instructions
          insnList.add(new VarInsnNode(Opcodes.ALOAD, 1)); // Toutes les variables sont crées de cette manière, il vous suffit juste de récupérer les ids et les Opcodes données par le plugin
          insnList.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, className, obf ? "<nom obfusqué de méthode>" : "<nom non-obfsqué de méthode>", "<description de la méthode>", false)); // Exemple d'appel de fonction dans la même classe
          insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "<lien vers la classe qui gère vos events>", "<nom de la fonction à appeler>", "<description de la fonction>", false)); // Ici on post l'event
          LabelNode l1 = new LabelNode(); // Cela sert à créer une fin de condition
          insnList.add(new JumpInsnNode(Opcodes.IFEQ, l1)); // On va au label
          insnList.add(new InsnNode(Opcodes.RETURN)); // Si la condition est vrai, c'est que l'event est annulé donc on quitte la fonction
          insnList.add(l1); // On place le label
      
          mn.instructions.insert(nodeif, insnList); // On ajoute le code
      
          ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
          cnode.accept(cw);
          return cw.toByteArray();
      }
      

      ATTENTION !!! Ce code est un exemple, dans votre cas le code peut différer

      Maintenant nous allons voir comment afficher du texte dans la console depuis une fonction de minecraft (ici je la place au début mais vous pouvez la placées où vous voulez) :

      private byte[] patchExplosion(String name, byte[] basicClass, boolean obf) {
          String targetMethodName = obf ? "a" : "doExplosionB"; // On veut modifier la méthode explosionB
      
          ClassNode classNode = new ClassNode();
          ClassReader classReader = new ClassReader(basicClass);
          classReader.accept(classNode, ClassReader.EXPAND_FRAMES);
          MethodNode mnode = ASMHelper.findMethod(classNode, targetMethodName, "(Z)V"); // On récupère le contenu de la méthode
      
          InsnList instr = new InsnList();
          instr.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")); // On appelle System.out
          instr.add(new LdcInsnNode("Explosion !")); // On charge notre texte
          instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false)); // On appelle System.out.println
          mnode.instructions.insertBefore(mnode.instructions.getFirst(), instr); // On ajoute le code au début
      
          ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
          classNode.accept(cw);
          return cw.toByteArray();
      }
      

      Un exemple est disponible sur le github de mon mod tutoriel

      Le tutoriel est assez complexe ce qui rend presque impossible de gérer tous les cas, si vous avez des idées à proposer, n’hésitez pas. Si vous ne vous en sortez pas postez un message et moi ou un membre du forum essaieront d’y répondre.

      Crédits

      Rédaction :

      • SCAREX

      Correction :


      Ce tutoriel de SCAREX publié sur 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

      Retour vers le sommaire des tutoriels

      Site web contenant mes scripts : http://SCAREXgaming.github.io

      Pas de demandes de support par MP ni par skype SVP.
      Je n'accepte sur skype que l…

      1 réponse Dernière réponse Répondre Citer 0
      • RedRelayR Hors-ligne
        RedRelay Moddeurs confirmés
        dernière édition par

        Il manque un petit truc : Pour lancer votre coremods via Eclipse (ou autre environnement de développement), vous devez utiliser -Dfml.coreMods.load=classe.qui.implemente.IFMLLoadingPlugin dans les options de lancement de la VM

        –------------------------------------------------------------------------------------
        Si tu trouves mon intervention pertinente, n'hésite pas a m…

        1 réponse Dernière réponse Répondre Citer 0
        • SCAREXS Hors-ligne
          SCAREX
          dernière édition par

          Effectivement je l’ai oublié, je le rajouterai plus tard

          Site web contenant mes scripts : http://SCAREXgaming.github.io

          Pas de demandes de support par MP ni par skype SVP.
          Je n'accepte sur skype que l…

          1 réponse Dernière réponse Répondre Citer 0
          • AmaA Hors-ligne
            Ama
            dernière édition par

            Smiley qui foire dans une balise java, mais sinon c’est cool ! 😄

            Si je t'ai filé un coup de main n'oublie pas le + / -
            Par contre évite les demandes d'aides en MP, tu sera sympa'

            La JavaDoc c'est comme le PQ, ça sert à ce démerder tous seul. -Victor Hugo- 2017

            Une superbe API pour animer vos super modèles CraftStudio dans Minecraft !

            1 réponse Dernière réponse Répondre Citer 0
            • RedRelayR Hors-ligne
              RedRelay Moddeurs confirmés
              dernière édition par

              Je suis entrain de faire un mod qui nécessite la création d’un coremod afin de créer un nouvel event et de le propager a partir d’une classe Vanilla de MC.
              Est-ce que je dois utiliser IFMLLoadingPlugin.getModContainerClass() et mettre FMLCorePluginContainsFMLMod a true dans le build.gralde ?
              Si oui, je retrourne quoi dans getModContainerClass() ? La classname de mon classe principale de mon mod ?

              –------------------------------------------------------------------------------------
              Si tu trouves mon intervention pertinente, n'hésite pas a m…

              1 réponse Dernière réponse Répondre Citer 0
              • robin4002R Hors-ligne
                robin4002 Moddeurs confirmés Rédacteurs Administrateurs
                dernière édition par

                Dans getModContainerClass il faut en effet mettre la classename de la classe principale, par contre cette classe ne doit pas être une classe avec @Mod mais une classe fille de DummyModContainer.
                Tu peux prendre exemple sur net.minecraftforge.common.ForgeModContainer

                1 réponse Dernière réponse Répondre Citer 0
                • RedRelayR Hors-ligne
                  RedRelay Moddeurs confirmés
                  dernière édition par

                  Et c’est possible de faire le mod indépendamment du coremod dans le même workspace ?
                  A vrai dire, je préfère utiliser l’annotation.
                  Le coremod ne fait que modifier CraftResult (un slot) pour y ajouter le déclenchement d’un event custom lors de l’appel a une méthode.
                  Le reste est un mod classique, qui utilise l’event custom via @SubscribeEvent.

                  –------------------------------------------------------------------------------------
                  Si tu trouves mon intervention pertinente, n'hésite pas a m…

                  1 réponse Dernière réponse Répondre Citer 0
                  • robin4002R Hors-ligne
                    robin4002 Moddeurs confirmés Rédacteurs Administrateurs
                    dernière édition par

                    Oui c’est possible.

                    1 réponse Dernière réponse Répondre Citer 1
                    • AmaA Hors-ligne
                      Ama
                      dernière édition par

                      Yop ! J’aimerais savoir si c’est possible de remplacer la valeur d’un integer dans une méthode ?

                      Je m’explique, j’aimerais remplacer dans la methode “generate” de la classe WorldGenTree l’integer “l” par 4, tout simplement.

                       public boolean generate(World p_76484_1_, Random p_76484_2_, int p_76484_3_, int p_76484_4_, int p_76484_5_)
                         {
                             int l = p_76484_2_.nextInt(3) + this.minTreeHeight;
                          […]
                      }
                      
                      

                      Mais je ne sais pas comment procéder, j’aimerais bien ce type d’exemple là, si possible

                      Si je t'ai filé un coup de main n'oublie pas le + / -
                      Par contre évite les demandes d'aides en MP, tu sera sympa'

                      La JavaDoc c'est comme le PQ, ça sert à ce démerder tous seul. -Victor Hugo- 2017

                      Une superbe API pour animer vos super modèles CraftStudio dans Minecraft !

                      1 réponse Dernière réponse Répondre Citer 0
                      • SCAREXS Hors-ligne
                        SCAREX
                        dernière édition par

                        @‘Ama’:

                        Yop ! J’aimerais savoir si c’est possible de remplacer la valeur d’un integer dans une méthode ?

                        Je m’explique, j’aimerais remplacer dans la methode “generate” de la classe WorldGenTree l’integer “l” par 4, tout simplement.

                         public boolean generate(World p_76484_1_, Random p_76484_2_, int p_76484_3_, int p_76484_4_, int p_76484_5_)
                           {
                               int l = p_76484_2_.nextInt(3) + this.minTreeHeight;
                            […]
                        }
                        
                        

                        Mais je ne sais pas comment procéder, j’aimerais bien ce type d’exemple là, si possible

                        Pour faire ça je ferais comme dans l’exemple pour insérer un event : tu localises le premier bytecode, puis tu supprimes un certain nombre après cette instructions puis tu remplaces par celles que tu veux.

                        Site web contenant mes scripts : http://SCAREXgaming.github.io

                        Pas de demandes de support par MP ni par skype SVP.
                        Je n'accepte sur skype que l…

                        1 réponse Dernière réponse Répondre Citer 0
                        • Flow ArgF Hors-ligne
                          Flow Arg Moddeurs confirmés
                          dernière édition par

                          Hey, j’aimerais une petite précision, je dois edit une méthode de Minecraft qui ne contient qu’une seule assignation. Et j’aimerais savoir comment retirer cette assignation (afin que la méthode ne fasse plus rien).
                          Merci !

                          Mon GitHub
                          Mon repo Maven
                          Mon Updater
                          Je suis un membre apprécié et joueur, j'ai déjà obtenu 10 points de réputation.

                          1 réponse Dernière réponse Répondre Citer 0
                          • 1 / 1
                          • Premier message
                            Dernier message
                          Design by Woryk
                          ContactMentions Légales

                          MINECRAFT FORGE FRANCE © 2024

                          Powered by NodeBB