How to write a core mod in Minecraft Forge 1.15.2

I will briefly explain how to write a core mod in the Minecraft Forge 1.15.2 environment. What is ASM? What is a byte code? I will not explain it here. Excuse me. I will not explain Javascript either. Excuse me.

However, if you are a modder who uses core mods, you can write with a feeling.

Forge 1.13 or later will probably work in the same way, but I haven't verified it.

I have very much referred to this page and the sample code. I am deeply grateful to my predecessors. 99.99 - Coremod

What is coremod? What is ASM?

Still easy. "coremod" is one of the mechanisms provided by Forge, and you can use it to rewrite the Java code of Minecraft itself (the expression is a little misleading). The library used for that is ASM. Also, the code itself is not the human code that you normally write, but the compiled bytecode. In any case, if you write ASM, you will be able to do anything. However, competition with other mods may increase.

This pdf is highly recommended (but long) for studying ASM. ASM 4.0 A Java bytecode engineering library

Verification environment

Minecraft: 1.15.2 Minecraft Forge: 1.15.2-31.2.0

Changes from before 1.13

Previously, coremod was written in Java code, but from 1.13, it will be written in ** Javascript **. It's okay to think that ASM itself is written in the same way as before (probably).

file organization

At least two files are required.

coremods.json Place it in ** main / java / resources / META-INF / directly under the folder **. The file name should probably be "coremods.json".

The contents are as below.

coremods.js


{
  "TestMod Transformer": "testmod-transformer.js"
}

The key in the array ("TestMod Transformer") is probably anything. I think it doesn't matter what the case. For the value, write the location of the js file that you will write.

If you just write the file name as above, it means that you are specifying the file in ** main / java / resources / directly under **. I haven't tried whether the absolute path or the relative path works, but I feel that the relative path seems to work.

coremods.js


{
  "TestMod Transformer": "testmod-transformer.js",
  "TestMod Transformer2": "testmod-transformer2.js"
}

You can also specify multiple javascript files at once.

Javascript file to write ASM

For the sake of clarity, let's put it directly under main / java / resources /. If you change the location, please change the value of the contents of coremods.json above. The js that actually writes ASM looks like the following.

testmod-transformer.js


function initializeCoreMod() {
    return {
        'coremodmethod': {
            'target': {
                'type': 'METHOD',
                'class': 'your.target.class',
                'methodName': 'targetMethodName',
                'methodDesc': 'targetMethodDescriptor'
            },
            'transformer': function(method) {
                //Write ASM processing here.
                return method;
            }
        }
    }
}

In the js file, you need to write a function that returns json with the name "initializeCoreMod ()". It's OK if you copy and paste on the top. If you write various things in the above "Write ASM processing here", it is completed.

Thank you for your hard work.               Then, huh, do you usually come here? It seems to be said, so I will write a little more. If you understand the above, feel free to do the rest.

Transformer type

You can specify three types of targets: "FIELD", "METHOD", and "CLASS". It interferes with fields, methods, and classes, respectively. In other words, you can write the following three ways.

testmod-transformer.js


function initializeCoreMod() {
    return {
        'coremodmethod': {
            'target': {
                'type': 'FIELD',
                'class': 'your.target.class',
                'fieldName': 'targetFieldName'
            },
            'transformer': function(field) {
                //Write ASM processing here.
                return field;
            }
        }
    }
}

testmod-transformer.js


function initializeCoreMod() {
    return {
        'coremodmethod': {
            'target': {
                'type': 'METHOD',
                'class': 'your.target.class',
                'methodName': 'targetMethodName',
                'methodDesc': 'targetMethodDescriptor'
            },
            'transformer': function(method) {
                //Write ASM processing here.
                return method;
            }
        }
    }
}

testmod-transformer.js


function initializeCoreMod() {
    return {
        'coremodmethod': {
            'target': {
                'type': 'CLASS',
                'class': 'your.target.class'
            },
            'transformer': function(class) {
                //Write ASM processing here.
                return class;
            }
        }
    }
}

The required values in "target" are as above. However, the field can be interfered with from Java with the reflection helper, and I feel that it is often enough to interfere with the method without interfering with the class itself, so for the personal reason, I will modify FIELD and CLASS below. Will not touch. Someone please.

How to write concretely

function initializeCoreMod() {
    var ASMAPI = Java.type("net.minecraftforge.coremod.api.ASMAPI");
    var Opcodes = Java.type('org.objectweb.asm.Opcodes');
    var InsnList = Java.type("org.objectweb.asm.tree.InsnList");
    var InsnNode = Java.type("org.objectweb.asm.tree.InsnNode");

    var mappedMethodName = ASMAPI.mapMethod("target_srg_func_name");

    return {
        'coremodmethod': {
            'target': {
                'type': 'METHOD',
                'class': 'your.target.class',
                'methodName': mappedMethodName,
                'methodDesc':'targetMethodDescriptor'
            },
            'transformer': function(method) {
                print("Enter transformer.");

                var arrayLength = method.instructions.size();
                var target_instruction = null;

                //search the target instruction from the entire instructions.
                for(var i = 0; i < arrayLength; ++i) {
                    var instruction = method.instructions.get(i);
                    if(instruction.name === "targetMethodName") {
                        target_instruction = instruction;
                        break;
                    }
                }

                if(target_instruction == null) {
                    print("Failed to detect target.");
                    return method;
                }

                var toInject = new InsnList();
//              toInject.add(new InsnNode(Opcodes.POP));
                toInject.add(new InsnNode(Opcodes.RETURN));

                // Inject new instructions just after the target.
                method.instructions.insert(target_instruction, toInject);

                return method;
            }
        }
    }
}

I think the outlook has improved considerably. I will explain in order.

Java class call

    var ASMAPI = Java.type("net.minecraftforge.coremod.api.ASMAPI");
    var Opcodes = Java.type('org.objectweb.asm.Opcodes');
    var InsnList = Java.type("org.objectweb.asm.tree.InsnList");
    var InsnNode = Java.type("org.objectweb.asm.tree.InsnNode");

I don't know what kind of magic it is, but if you put the usual import destination class name in the argument of "Java.type ()", you can use it as if it were Java (explanation miscellaneous). If Opcodes comes, I feel like I have a hundred people. But I'm having trouble with code completion not working. If anyone knows how to complement, please let me know. I'm sure ASMAPI and Opcodes will use any modifications, but I think it's up to you to call other classes. The current coremod uses the so-called tree API, so please check there separately to find out what classes are available.

Specifying methods to interfere

    var mappedMethodName = ASMAPI.mapMethod("target_srg_func_name");

    return {
        'coremodmethod': {
            'target': {
                'type': 'METHOD',
                'class': 'your.target.class',
                'methodName': mappedMethodName,
                'methodDesc':'targetMethodDescriptor'
            },
            'transformer': function(method) {
                return method;
            }
        }
    }

I named it "coremodmethod", but it seems that this can be any string. Other "target" and "transformer" must be this.

A method called mapMethod () is defined in ASMAPI, and it returns the mapped method name from the obfuscated method name. For example, the "saveScreenshotRaw" method of the "net.minecraft.util.ScreenShotHelper" class is "func_228051_b_" or something like that. If you don't get the method name using this mapMethod (), it will work in the development environment but not in the real environment (or vice versa).

When actually writing, put the obfuscated method name in the argument such as "ASMAPI.mapMethod (" func_228051_b_ ")".

For the method name after obfuscation, select the mapping that suits your development environment from the usual MCP Bot and search for it.

By the way, there are some people who can't find it even if they look for it by MCP mapping. You don't need to fetch the method name with mapMethod () because they already work in the development environment and the real environment with the name that is easy to understand.

"methodDesc" is the usual descriptor.

void hogehoge(int i, float f)

If it is a method like that, the descriptor will be "(IF) V", which is the usual thing.

Both IDEA and Eclipse have plug-ins that display Java code in bytecode, so I think that using it will improve copy and paste.

Actual reorganization

'transformer': function(method) {
    print("Enter transformer.");
    
    var arrayLength = method.instructions.size();
    var target_instruction = null;
    
    //search the target instruction from the entire instructions.
    for(var i = 0; i < arrayLength; ++i) {
        var instruction = method.instructions.get(i);
        if(instruction.name === "targetMethodName") {
            target_instruction = instruction;
            break;
        }
    }

    if(target_instruction == null) {
        print("Failed to detect target.");
        return method;
    }

    var toInject = new InsnList();
//  toInject.add(new InsnNode(Opcodes.POP));
    toInject.add(new InsnNode(Opcodes.RETURN));
    
    // Inject new instructions just after the target.
    method.instructions.insert(target_instruction, toInject);
    
    return method;
}

I personally think that the parts that are actually modified are easier to write than before.

The method that crosses the argument has a field called instructions, which contains bytecode line by line in an array. The method bytecode itself is. After that, you can delete, change, or insert a return at any position in the bytecode.

In the above, we have made a modification that "returns on the spot when a certain method is called". Primitively, it turns instructions one by one with for to find the line (that is, instructions) where the target method is called.

When you find the instruction you are looking for, insert a new instruction.

InsnList () is a class for creating a series of instruction pairs, and each instruction is added () to this class. Here, as "new InsnNode (Opcodes.RETURN)", only the instruction that means "return" is added to the InsnList.

Finally, instructions has a method called insert (argument 1, argument 2), which adds the instructions inside the insnList specified in argument 2 immediately after ** immediately after the instruction specified in argument 1. I will.

Here, it is the process of adding "return" immediately after a specific method.

It's important to note that if you've touched coremod, you'll know that if you return as much as you like, the frames will be inconsistent and will probably crash in the current version as well. I think that values that are loaded in the frame but are no longer used must be popped the correct number of times.

Other

--I don't know the reason, but you can't use let or const when declaring a variable. An error will occur. Use var. --Print () outputs to the console when you debug and start Minecraft in the development environment, but it seems that it does not appear anywhere in the real environment. ――It seems that you can return multiple transformers with one javascript file, but I'm not sure how to do it. --Forge's ASMAPI class (which is usually Java) seems to have a method to find the place where a method is called first, so I think it's a good idea to take a look at the contents once. ..

At the end

I thought I'd write an example, but it helped me. I will write it again if I have some spare capacity.

I feel that there are various mistakes and misunderstandings, so please let me know if you notice.

Thank you for your relationship.

Recommended Posts

How to write a core mod in Minecraft Forge 1.15.2
How to sign a Minecraft MOD
How to write a date comparison search in Rails
[Rails] How to write in Japanese
How to write a ternary operator
[Basic] How to write a Dockerfile Self-learning ②
How to insert a video in Rails
[Introduction to Java] How to write a Java program
How to publish a library in jCenter
[SpringBoot] How to write a controller test
Rails: How to write a rake task nicely
Add original weapons to mods in Minecraft Forge 1.15.2
[JavaFX] How to write Eclipse permissions in build.gradle
[Rails] How to write when making a subquery
How to display a web page in Java
How to edit layers in Core ML Tools
How to run a djUnit task in Ant
How to add a classpath in Spring Boot
How to create a theme in Liferay 7 / DXP
How to implement a like feature in Rails
How to easily create a pull-down in Rails
How to make a follow function in Rails
How to write Java String # getBytes in Kotlin?
Notes on how to write comments in English
How to automatically generate a constructor in Eclipse
How to write Rails
How to write Mockito
How to write migrationfile
How to clear all data in a particular table
How to create a Java environment in just 3 seconds
How to write a unit test for Spring Boot 2
java: How to write a generic type list [Note]
How to create a Spring Boot project in IntelliJ
How to create a data URI (base64) in Java
How to launch another command in a Ruby program
How to display a browser preview in VS Code
[How to insert a video in haml with Rails]
How to store Rakuten API data in a table
How to mock a super method call in PowerMock
How to convert a file to a byte array in Java
[Rails 6] How to set a background image in Rails [CSS]
How to write an external reference key in FactoryBot
[Rails] How to load JavaScript in a specific view
[Ruby/Rails] How to generate a password in a regular expression
How to write good code
Bit Tetris (how to write)
How to write java comments
How to leave a comment
[Refactoring] How to write routing
Great poor (how to write)
[Note] How to write Dockerfile/docker-compose.yml
How to write Junit 5 organized
How to write Rails validation
How to write Rails seed
To write a user-oriented program (1)
[Ruby] How to write blocks
How to write Rails routing
How to insert a video
How to create a method
How to write a migration from Rails datetime type to date type
[Forge] How to register your own Entity and Entity Render in 1.13.2