Mixins let you inject code into vanilla Minecraft classes without modifying or distributing their source. The MultiLoader template already sets up the Mixin annotation processor and the per-loader mixin JSON config files; this tutorial shows how to write mixins that are safe, targeted, and compatible with other mods.
Mixin Configuration Files
The template generates mixin JSON config files in each subproject. The common mixin config is at:
Add each new mixin class name (without the package prefix) to the mixins array for server-safe mixins, or to client for client-only mixins. Forgetting to add the entry here means the mixin class is compiled but never applied.
Your First Mixin
As a practical starting point, we will add a log message whenever a player takes fall damage. Create MixinLivingEntity in the mixin package of your common project:
Then add the class name to the mixins array in examplemod.mixins.json:
defaultRequire: 1 in the mixin config means any injection that fails to find its target will crash the game at startup with a clear error. This is intentional: a silent miss is far harder to diagnose than an early crash.Injection Points
The at parameter controls where in the target method your injector fires. The most common values are:
@At("HEAD"): the very first instruction; runs before any other code in the method.@At("RETURN"): fires just before each return statement; useful for post-processing.@At(value = "INVOKE", target = "..."): fires at a specific method call within the target.@At(value = "FIELD", target = "..."): fires at a field read or write.
The method string uses the format methodName(Lpackage/ClassName;I)V (standard JVM descriptor syntax). For parameterless methods just the name is sufficient; for overloaded methods include the full descriptor to disambiguate.
Modifying Return Values
Use CallbackInfoReturnable<T> and call cir.setReturnValue(value) to override the method's return value and stop further execution:
Setting cancellable = true on the @Inject annotation is required when you call setReturnValue or cancel(). Without it the mixin processor will throw an error at startup.
Accessing Private Fields
Mixins can expose private fields through @Shadow declarations. The shadow simply tells the mixin processor which field to alias; it does not add a new field:
@Shadow @Final private int noActionTime and use the aliases parameter of @Shadow if needed. In practice, Mojang mappings (used by the MultiLoader template) keep the same names, so aliases are rarely needed.Common Pitfalls
- Forgetting to add the class to the JSON config. The mixin compiles but is never loaded. Check the config file first before debugging injection issues.
- Wrong method descriptor. If the game crashes with
@Inject on non-existent method, use a bytecode viewer such as Recaf or your IDE's decompiler to find the exact descriptor. - Injecting into a constructor. Use
method = "<init>"and target@At("TAIL")to fire after the constructor body finishes; targetingHEADin constructors fires before field assignments, which can causeNullPointerExceptionif your injection reads instance fields. - Client-only mixins on the server. Any mixin that references client-only classes (such as
Minecraftor renderer classes) must be listed underclientin the mixin JSON, notmixins.
You can find the source for this tutorial here:
View Source on GitHubCustom Particles (MultiLoader 1.21+)
Register a SimpleParticleType in common, provide a particle description JSON and sprite textures, create a TextureSheetParticle subclass with fade and animation, and register the provider client-side on both loaders.
Continue →