This is an old revision of the document!
Table of Contents
This page is currently a draft! It is not intended to be read by users of the wiki yet. Feedback will be appreciated but trust this page's information at your own risk!
Contact GauntRecluse (paleintrovert) on Discord to give feedback and suggestions on this draft
Tutorial: Making your first Mixin (DRAFT)
Preamble
This is meant to be a complementary page to the Introduction to Mixins page. It intends to hold a newcomer's hand through each step of creating an elementary Mixin, teaching what each element does and what it is for along the way. See Registering Mixins for setting up Mixins on a Fabric project if you intend to follow along with this.
This tutorial will be made using a 1.21.1 Fabric Project as reference. It is recommended to develop using IntelliJ IDEa Community Edition to be able to leverage the Minecraft Development plugin by demonwav. Yarn mappings will be used. If you wish to follow along with Mojang mappings, you may use a tool such as mappings.dev or Shedaniel.dev's mappings tool to translate between the Yarn mappings showcased and the ones in your development environment as needed.
The repository containing this tutorial's Mixin can be found at: this repo, the specific Mixin class being TutorialFirstMixin
Creating the Mixin Class
Mixins are a way to describe changes to be applied to code as more code. For a more in-depth explanation, see the introduction page.
For this tutorial, we'll add a Logger call at the head of the loadWorld
method in the Vanilla MinecraftServer
class. This will be based on the ExampleMixin
Mixin class present in the Fabric mod template at the time of writing, with some tweaks.
For this task, we'll use the Mixin annotation @Inject
. We'll explain how it works as we go. If you're wondering why we're using annotations, they serve as ways to hold information that can be used by Mixin to apply our changes as we intend them to, this makes it both organized and practical.
After following the necessary steps described in Registering Mixins to set up our mixins.json
config, we'll create our class in the designated mixins package. We'll call it TutorialFirstMixin
for this tutorial's sake:
@Mixin(MinecraftServer.class) public abstract class TutorialFirstMixin { }
You'll notice we made the class abstract
. This is a best practice for Mixin class, as it avoids an accidental instantiation of the class, and removes the burden of needing to implement methods if you implement interfaces or extend supers.
Accordingly, we will also add this to our mixins.json
config file, so that Mixin can find it. The reason Mixin uses a config rather than just finding the mods by searching your mod's jar
at runtime is mainly for performance purposes1). It is faster and less prone to error if the files are manually defined, compared to the relatively expensive operation of parsing a jar
. It also gives you, the developer, extra control.
{ ... "mixins": [ "TutorialFirstMixin" ], ... }
And that's all there is to actually creating the Mixin Class. It doesn't do anything yet, though. So let's get to that.
Creating the injector
The first step, assuming nothing has caused a crash or gone to hell yet, is to create a “stub” method. This will be the method we annotate with @Inject
, and putting it there first allows us to not have to bother with the Annotations are not allowed here
error from having a hanging annotation.
@Mixin(MinecraftServer.class) public abstract class TutorialFirstMixin { private void addLoggerAtHead() { } }
We've named it addLoggerAtHead
because of what it'll be used for, but your Mixin injectors' methods should always try to best describe their intended effects on the target.
We now add our @Inject
annotation:
@Inject(method = "loadWorld") private void addLoggerAtHead() { }
This now specifies to Mixin that, when that method gets merged into MinecraftServer
, we want to inject a call to it in the method loadWorld
. Since there is only one method of that name in MinecraftServer
, the name is enough, this would not be the case if the method had overloads. If you're using the MCDev2), you should almost always leverage the plugin's autocomplete for a Mixin annotation's fields to ensure accuracy.
Now, to specify where in the targeted method we want our injection to happen, we add something that may be a bit counterintuitive to some, an @At
annotation as one of the arguments in the @Inject
annotation:
@Inject(method = "loadWorld", at = @At(value = "HEAD") ) private void addLoggerAtHead() { }
It is absolutely not needed to put the different fields on different lines in your code, but it may become useful when the individual arguments are very long.
Now, let's look closer at the @At
annotation. You can think of it as a series of instructions, to oversimplify it:
@Inject
says that the decorated method should be invoked at a specified point.method = “loadWorld”
narrows down the point of injection to within that methodat = @At
adds the annotation used to narrow it down further in the methodvalue = “HEAD”
specifies the injection point as being at the very start of the method
Now, if you're following along with MCDev you'll likely notice that our handler method, addLoggerAtHead
, does not have the correct signature for the target. In most cases, using MCDev's context action that quickly fixes it should work. However, to break down what must be in an @Inject
handler method's parameters, there must be the target method's parameters – here there are none – and a CallbackInfo
parameter, often called ci
. If we were injecting into a method that returns a value, it would instead be CallbackInfoReturnable<T> cir
where T
is the return type of the target method.
If we fix the signature and add a logger call, the full class becomes:
@Mixin(MinecraftServer.class) public abstract class TutorialFirstMixin { @Inject(method = "loadWorld", at = @At(value = "HEAD") ) private void addLoggerAtHead(CallbackInfo ci) { GauntTutorialMod.LOGGER.info("MinecraftServer$loadWorld has started!") } }
And that's it for making this Mixin work! But let's go over an extra thing we can do with Mixin classes in general:
Extending the target class's parents
When the class you're targeting extends and/or implements parents, mimicking that on your Mixin class is an overall benefit, as it allows you to get all of the parent methods directly. For our example Mixin it'll look like:
public abstract class TutorialFirstMixin extends ReentrantThreadExecutor<ServerTask> implements class_8599, class_9820, CommandOutput, AutoCloseable { public TutorialFirstMixin(String string) { super(string) } @Inject(method = "loadWorld", at = @At(value = "HEAD") ) private void addLoggerAtHead(CallbackInfo ci) { GauntTutorialMod.LOGGER.info("MinecraftServer$loadWorld has started!"); } }
The constructor is irrelevant to actual Mixin work, it's only there for the sake of extension. A perceptive reader might notice that we didn't implement any methods. That is another benefit of making our class abstract. Abstract classes, because they are never instantiated, do not need to implement all methods from their parents. Do note if you implement an interface not implemented by the target class, that will be merged into the target class at runtime aswell. This has some utility for duck typing, also known as using a “duck interface”3)
Debugging with Mixins
There are many ways to debug mod code in general, and most apply to code you mixed into aswell, but Mixin also provides a very useful utility for debugging; namely, the @Debug
annotation, with the export = true
parameter.
Adding it to our tutorial class it should look like:
@Debug(export = true) @Mixin(MinecraftServer.class) public abstract class TutorialFirstMixin { @Inject(method = "loadWorld", at = @At(value = "HEAD") ) private void addLoggerAtHead(CallbackInfo ci) { GauntTutorialMod.LOGGER.info("MinecraftServer$loadWorld has started!") } }
The export setting makes it so that after merging all applicable Mixins to the target class, it will produce a java file representing the target class after changes are applied. It should be exported to the directory run/mixin.out
. You can also configure your development runs to export all Mixins by adding the following argument to your VM options -Dmixin.debug.export=true
4)
The
@Debug
settings stay as they are in dev when the mod is built into a jar
, make sure to not accidentally export Mixin classes in the jar
you publish!
Closing Thoughts and where to go next
So, you've completed your first functioning Mixin, and hopefully you picked up a few notions along the way. What now?
For more thorough and formal intros to the topic, you should read through the Mixin Introduction page and its linked Wikis.5)
Other than that, the most important step from here is trial, error, and asking for help/information
!!duck
on the Fabric Discord for a direct example. See also the Mixin Wiki segment on it!!mixinexport
in the Fabric Discord for a more direct presentation of this information