MultiLoader 1.21+ · Part 21

Introduction to Mixins (MultiLoader 1.21+)

ADVANCED MULTILOADER 1.21-1.21.1 22 min read · Sep 28, 2026
MultiLoader 1.21+ · 28 parts
1 Getting Started with MultiLoader 1.21+ 2 Setting Up RegistrationUtils 3 Creating Items (MultiLoader 1.21+) 4 Creating Blocks (MultiLoader 1.21+) 5 Data Generation: Block & Item Models (MultiLoader 1.21+) 6 Data Generation: Block Loot Tables (MultiLoader 1.21+) 7 Data Generation: Crafting Recipes (MultiLoader 1.21+) 8 Data Generation: Block & Item Tags (MultiLoader 1.21+) 9 Custom Food Items (MultiLoader 1.21+) 10 Custom Tools (MultiLoader 1.21+) 11 Custom Armour (MultiLoader 1.21+) 12 Block Entities (MultiLoader 1.21+) 13 Config Files (MultiLoader 1.21+) 14 Custom Sounds (MultiLoader 1.21+) 15 Events and Listeners (MultiLoader 1.21+) 16 Networking and Custom Packets (MultiLoader 1.21+) 17 Data Generation: Advancements (MultiLoader 1.21+) 18 Data Generation: Language Files (MultiLoader 1.21+) 19 Custom Entities (MultiLoader 1.21+) 20 Ore Generation (MultiLoader 1.21+) 21 Introduction to Mixins (MultiLoader 1.21+) 22 Custom Particles (MultiLoader 1.21+) 23 Menus & Screens (MultiLoader 1.21+) 24 Key Bindings (MultiLoader 1.21+) 25 Custom Potion Effects (MultiLoader 1.21+) 26 Custom Enchantments (MultiLoader 1.21+) 27 Custom Commands (MultiLoader 1.21+) 28 Custom Biomes (MultiLoader 1.21+)

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.

WARNING
Mixins inject directly into compiled bytecode. A poorly targeted mixin can crash the game on startup, silently corrupt game state, or conflict with other mods. Only use mixins when the same outcome cannot be achieved through events, capability attachments, or datagen.

Mixin Configuration Files

The template generates mixin JSON config files in each subproject. The common mixin config is at:

text
common/src/main/resources/examplemod.mixins.json
json
{
"required": true,
"minVersion": "0.8",
"package": "com.example.examplemod.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [],
"client": [],
"injectors": {
"defaultRequire": 1
}
}

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:

java
@Mixin(LivingEntity.class)
public abstract class MixinLivingEntity {
@Inject(method = "causeFallDamage", at = @At("HEAD"))
private void onFallDamage(float fallDistance, float multiplier,
DamageSource source, CallbackInfoReturnable<Boolean> cir) {
if ((Object) this instanceof Player player) {
Constants.LOG.info("{} is taking fall damage from {}m",
player.getName().getString(), Math.round(fallDistance));
}
}
}

Then add the class name to the mixins array in examplemod.mixins.json:

json
"mixins": ["MixinLivingEntity"]
TIP
The 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:

java
@Mixin(Player.class)
public abstract class MixinPlayer {
// Double experience gain for all players
@Inject(method = "getXpNeededForNextLevel", at = @At("RETURN"),
cancellable = true)
private void doubleXpRequirement(CallbackInfoReturnable<Integer> cir) {
cir.setReturnValue(cir.getReturnValue() * 2);
}
}

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:

java
@Mixin(LivingEntity.class)
public abstract class MixinLivingEntityFields {
@Shadow
private int noActionTime;
@Inject(method = "tick", at = @At("HEAD"))
private void onTick(CallbackInfo ci) {
if (noActionTime > 200) {
Constants.LOG.debug("Entity has been idle for {} ticks", noActionTime);
}
}
}
TIP
If the field name differs between the mapped (development) and obfuscated (production) environments, add @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; targeting HEAD in constructors fires before field assignments, which can causeNullPointerException if your injection reads instance fields.
  • Client-only mixins on the server. Any mixin that references client-only classes (such as Minecraft or renderer classes) must be listed under client in the mixin JSON, not mixins.

You can find the source for this tutorial here:

View Source on GitHub
NEXT IN SERIES

Custom 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 →