Créer un coremod pour modifier les classes de Minecraft



  • Sommaire

    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



  • 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



  • Effectivement je l'ai oublié, je le rajouterai plus tard



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



  • 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 ?


  • Administrateurs

    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



  • 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.


  • Administrateurs

    Oui c'est possible.



  • 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



  • @'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.