-
Notifications
You must be signed in to change notification settings - Fork 25
Contributing: Mixins
When adding features to a client-side mod like recode, mixins are inevitable. Unfortunately, they are also quite complicated. Mixin is an established library and they have their own wiki, but it is written for the technically savvy. So, while this wiki page is not strictly necessary, it is designed to be a more practical introduction to mixins for people who don't care about the ultra-fine details.
With that said, some details are more important than others. So before writing our first mixin, I want to address some things.
Minecraft mods are fundamentally different from other types of coding projects, and I think this is often underappreciated. Software development as a discipline is one where robustness and safety is respected and often prioritized. As such, it is important to structure our projects in a way that we are not left to resort to "hacky" solutions to problems; those can be recipes for disaster. But we didn't make Minecraft! It is an independently developed game, made by Mojang Studios. While they allow Java Edition to be modded, it doesn't have an official modding API, and the community is left to their own devices. For example, a mod by itself can't change Minecraft's code when it needs to: it can only add to it. How else would recode make signs glow for features like Code Search? This is where mixins come in.
Changing code that we didn't write is actually very hard. If you're familiar with binary compatibility and the Java Virtual Machine, then you should know why this is, but that is beyond the scope of this wiki. What matters is, such a task is hard for two reasons:
- The code can change at any time. For example, we don't want to change Minecraft's code by replacing its code with our own, because if a new feature is ever added, that feature wouldn't be present in our overwritten code! There are many more cases where this is a problem.
- The code is compiled. Minecraft is written in normal, readable Java, but when it is downloaded by the launcher, it is downloaded as a compiled "jar file" which is optimized for performance. The jar file contains what is called bytecode. To change Minecraft's code, we have to work with this bytecode, and that's often harder than it sounds.
Mixin is a "bytecode weaving" library that seeks to mitigate these problems as much as possible, with a clever framework and set of conventions, and helpful debugging tools. It also streamlines the process greatly and makes it easier than ever before to change Minecraft's bytecode. Each class we write using Mixin is called a "mixin". There are actually two very different ways mixins allows us to change bytecode:
- Interface injection, making an external class implement our own interface.
- Callback injection, adding our own code to an external method.
I will explain each one and how recode uses them.
Whether you want to inject an interface or a callback, you still need to first create the mixin. To do so, we need to talk about the mixin package.
Since mixins only modify bytecode, they are actually not needed at runtime. Mixin enforces this by requiring all mixin files be in a specially designated "mixin package", and no runtime constructs can go in this package.
If this is your project's first mixin and your mixin package is not already defined, you can do so like this:
- In your mod's
fabric.mod.json, add the property"mixins": "<mod_id>.mixins.json", where<mod_id>is your Mod ID. - Then create this file as a sibling to
fabric.mod.json; the file should look like this:
{
"required": true,
"package": "<mixin_package>",
"compatibilityLevel": "JAVA_17",
"mixins": [],
"client": [],
"server": [],
"injectors": {
"defaultRequire": 1
}
}Similarly to before, <mixin_package> should be the mixin package, as you would write it in an import statement. Other properties such as compatibilityLevel can be adjusted as desired. This new file is where you will register your mixins as you create them. client is for client-side only mixins, server is for server-side only mixins, and mixins is for mixins on both sides.
With all of that said, we can finally create a mixin. They look like this:
@Mixin(ClientPacketListener.class)
public abstract class MClientPacketListener {
// ...
}Notice the @Mixin annotation, and notice that the class is abstract. You then also need to register this class in the mixin JSON file, which you do by adding the subpackage to one of the arrays (mixins, client, or server). Since recode is a client-side mod, all recode mixins will go in client; for example, this mixin's subpackage string is multiplayer.MClientPacketListener because it is in a multiplayer package nested within the mixin package.
Also notice the name; the name is not super important, as long as you are consistent. Some mods would name this ClientPacketListenerMixin, but recode uses the convention of prefixing all mixins with the letter M.
Lastly, notice that the mixin is written in Java, not Kotlin. Because the Mixin library expects specific bytecode, mixins must always be written in Java. Trying to do otherwise is technically possible, but futile and counterproductive.
Interface injection is by far the less common of the mixin techniques, but it is easier to understand and do well, so we will start with it. Consider recode's side chat feature: to make it work smoothly, we often need to get the SideChat instance from various classes (such as MChatListener). But the SideChat instance is itself defined in a mixin! Interface injection is how we expose it.
@Mixin(Gui.class)
public abstract class MGui implements DGuiWithSideChat {
@Unique
private final SideChat sideChat = new SideChat();
// ...
@NotNull
@Override
public SideChat getRecode$sideChat() {
return sideChat;
}
}To make an external class implement our interface, we simply add implements <interface> to the mixin (in place of the actual class). We can then implement the interface's members like normal.
There are two important conventions to note here. First, note that we prefix the interface with the letter D. The D stands for "duck", as in "duck interface", and is a technical term that we use to mean an interface added after compilation. However, the prefix is again a recode-specific convention. More importantly, notice that we prefix properties and functions with <mod_id>$; this is to keep the interfaces from colliding with other mods. For private members, this is not necessary, because we can (and should) add the @Unique annotation as shown above.
Since the rest of the mod's code, at compile time, does not yet know about this duck interface, we have to use casts to access it. Observe this method from MChatListener:
@Unique
private SideChat getSideChat() {
var gui = (DGuiWithSideChat) Minecraft.getInstance().gui;
return gui.getRecode$sideChat();
}