Highly Suspect Agency

how 2 angry lex.md

quat, Feb 06, 2020
forge 1.12 mc-modding

This article was formerly published on my Github account here, although I never finished it. I’ve reprinted it on my new website as it originally appeared. Maybe I’ll get around to finishing it someday, but unfortunately 1.12 modding is a little archaic these days and I’m mainly keeping it up for historical purposes only.


Psst, hey kid, wanna write a coremod?

What

Coremods allow you to define class transformers.

Whenever the game tries to load a class - any class, with few exceptions - all class transformers take turns stepping in and getting an opportunity to change the raw class bytes to anything it wants. This allows you to make changes to any class the game loads.

What can they do?

You get one chance to change a class, just before it is loaded.

By “change”, I mean you can really do almost anything you want to a class. You can add and remove methods, change access modifiers, change the class/interface hierarchy, change the code of methods, and so on.

What can’t they do?

You can’t change classes after they’re loaded. You only get the one chance, right before the class loads, and that’s it. After the class loads, its implementation is finalized. (This is mostly a JVM limitation.)

Your class transformer has to return a valid class file. If the class file is not valid, the game crashes. If you don’t return a class file, the game crashes.

A warning

Class transformers are a double edged sword - one misstep can cause lots and lots of hard-to-debug errors. It takes practice and patience, and even then, issues can still be caused in production by forgetting to obfuscate a method name or other coremods changing the same classes in ways you didn’t expect.

How

Manifest attributes

When Forge initially loads your mod, it checks yourmod.jar/META-INF/MANIFEST.MF for some information pertaining to coremods.

You must set the attribute FMLCorePlugin to the name of a class that implements IFMLLoadingPlugin. This is your entrypoint. (More on this in a bit.)

You also probably want to set the attribute FMLCorePluginContainsFMLMod to true. If you don’t, Forge will skip your jar when looking for @Mod annotations.

Turns out looking in MANIFEST.MF is a pretty standard thing for Java programs to do, so Gradle has a convenient way of placing lines in this file so you don’t need to do it yourself. Paste this incantation into your build.gradle:

jar {
	manifest {
		attributes "FMLCorePlugin": "your.mod.package.core.LoadingPlugin"
		attributes "FMLCorePluginContainsFMLMod": true
	}
}

Finally, if you want your coremod to be loaded in development (which, of course you do), add the following JVM argument:

-Dfml.coreMods.load=your.mod.package.core.LoadingPlugin

I think genIntellijRuns sets this in your run configurations, but if it doesn’t, paste that right into “VM options”.

IFMLLoadingPlugin

Back to that class the FMLCorePlugin attribute points to. It must implement IFMLLoadingPlugin and additionally it should have a few annotations:

Oh, and as for the methods in IFMLLoadingPlugin?

A note on classloading

Normally classloading is transparent - when you need a class, it gets loaded on demand. Coremods and class transformers break the illusion, however.

As such, you need to be extremely careful about what classes your coremod and class transformer access.

IClassTransformer

This interface has only one method, byte[] transform(String name, String transformedName, byte[] basicClass). Let’s go through it.

First, one small thing to note is that some class transformers return null when they error. Since transformers are all put together in a chain, someone else’s transformer erroring can cause yours to NullPointerException if you try to work with the basicClass without checking. This can put your mod in the stacktrace for someone else’s error! Always null-check basicClass and if it’s null just pass the null down the line, not much else you can do.

Typically you only need to transform one or two classes with your transformer. Remember that transformers are called for literally every single class that gets loaded, so time is of the essence and memory allocations will get magnified.

Or, in other words, the first line of your class transformer should look like this:

if(basicClass == null || !transformedName.equals("my.target.class.name")) return basicClass;

(or if you want to transform a bunch of classes, assuming targets is a very efficient collection like a tree or hashset):

if(basicClass == null || !targets.contains(transformedName)) return basicClass;

Anyway, what about when basicClass is not null? Well… that’s it. It’s just a byte array.

Unfortunately this is where Forge stops helping. However, note that you get the ObjectWeb ASM library as one of the dependencies. This library takes you the rest of the way to your goal (and is why people colloquially refer to coremodding and class transforming as “asming”.)

ASM

TODO: Write more.

ClassReader reader = new ClassReader(basicClass);
ClassNode node = new ClassNode(Opcodes.ASM5);
reader.accept(node, 0);

//do work

ClassWriter writer = new ClassWriter(flags) //talk about flags
node.accept(writer);
return writer.toByteArray();

Debugging


← Fix stuff being broke in IntelliJ
Obligatory Haha New Website Post →
Currently not powered by Jortage