Skip to main content
LiquidBounce uses SpongePowered Mixins to inject code into Minecraft at runtime. This allows modifying game behavior without editing base classes directly.

What Are Mixins?

Mixins are a bytecode transformation system that allows you to:
  • Inject code at specific points in methods
  • Redirect method calls
  • Modify field access
  • Add new methods and fields to existing classes
  • Replace entire methods
All without modifying the original .class files.

Mixin Basics

Anatomy of a Mixin

Location: injection/mixins/minecraft/client/MinecraftAccessor.java:26
@Mixin(Minecraft.class)  // Target class
public interface MinecraftAccessor {
    
    @Invoker("startUseItem")  // Access private method
    void callStartUseItem();
}
This creates an accessor interface for calling Minecraft’s private startUseItem() method.

Common Annotations

@Mixin - Declares the target class:
@Mixin(ClientPlayerEntity.class)
public class MixinClientPlayer { }
@Inject - Injects code at a point:
@Inject(method = "tick", at = @At("HEAD"))
private void onTick(CallbackInfo ci) {
    EventManager.callEvent(new PlayerTickEvent());
}
@Redirect - Redirects method calls:
@Redirect(
    method = "sendMovementPackets",
    at = @At(
        value = "INVOKE",
        target = "Lnet/minecraft/entity/Entity;getYaw()F"
    )
)
private float hookYaw(Entity entity) {
    return RotationManager.getCurrentRotation().getYaw();
}
@ModifyArg - Changes method arguments:
@ModifyArg(
    method = "updateVelocity",
    at = @At(
        value = "INVOKE",
        target = "Lnet/minecraft/util/math/Vec3d;multiply(D)Lnet/minecraft/util/math/Vec3d;"
    ),
    index = 0
)
private double modifyVelocity(double original) {
    return original * velocityMultiplier;
}
@ModifyVariable - Modifies local variables:
@ModifyVariable(
    method = "tick",
    at = @At("STORE"),
    ordinal = 0
)
private float modifySpeed(float speed) {
    return speed * 1.5f;
}
@Invoker - Accesses private methods:
@Invoker("somePrivateMethod")
void invokeSomePrivateMethod();
@Accessor - Accesses private fields:
@Accessor("privateField")
int getPrivateField();

@Accessor("privateField")
void setPrivateField(int value);

Injection Points

@At Values

HEAD - Beginning of method:
@Inject(method = "tick", at = @At("HEAD"))
private void atStart(CallbackInfo ci) {
    // Runs before any method code
}
RETURN - Before return statements:
@Inject(method = "calculate", at = @At("RETURN"))
private void beforeReturn(CallbackInfoReturnable<Integer> cir) {
    // Runs before method returns
}
TAIL - Before final return:
@Inject(method = "process", at = @At("TAIL"))
private void atEnd(CallbackInfo ci) {
    // Runs at the very end
}
INVOKE - Before method call:
@Inject(
    method = "tick",
    at = @At(
        value = "INVOKE",
        target = "Lnet/minecraft/client/MinecraftClient;getWindow()Lnet/minecraft/client/util/Window;"
    )
)
private void beforeGetWindow(CallbackInfo ci) {
    // Runs before getWindow() is called
}
FIELD - Before field access:
@Inject(
    method = "tick",
    at = @At(
        value = "FIELD",
        target = "Lnet/minecraft/client/MinecraftClient;player:Lnet/minecraft/client/network/ClientPlayerEntity;"
    )
)
private void beforePlayerAccess(CallbackInfo ci) {
    // Runs before accessing 'player' field
}

Callback Types

CallbackInfo

For void methods:
@Inject(method = "tick", at = @At("HEAD"))
private void onTick(CallbackInfo ci) {
    // Cancel method execution
    ci.cancel();
}

CallbackInfoReturnable

For methods with return values:
@Inject(method = "getSpeed", at = @At("HEAD"), cancellable = true)
private void onGetSpeed(CallbackInfoReturnable<Float> cir) {
    // Override return value
    cir.setReturnValue(2.0f);
    // Cancel prevents original method from running
    cir.cancel();
}

Practical Examples

Firing Events

@Mixin(ClientPlayerEntity.class)
public class MixinClientPlayer {
    
    @Inject(method = "tick", at = @At("HEAD"))
    private void onTick(CallbackInfo ci) {
        EventManager.callEvent(new PlayerTickEvent());
    }
    
    @Inject(method = "pushOutOfBlocks", at = @At("HEAD"), cancellable = true)
    private void onPushOutOfBlocks(CallbackInfo ci) {
        PlayerPushOutEvent event = new PlayerPushOutEvent();
        EventManager.callEvent(event);
        
        if (event.isCancelled()) {
            ci.cancel();
        }
    }
}

Modifying Behavior

@Mixin(Block.class)
public class MixinBlock {
    
    @Inject(method = "getVelocityMultiplier", at = @At("HEAD"), cancellable = true)
    private void onGetVelocityMultiplier(CallbackInfoReturnable<Float> cir) {
        BlockVelocityMultiplierEvent event = new BlockVelocityMultiplierEvent();
        EventManager.callEvent(event);
        
        if (event.getMultiplier() != null) {
            cir.setReturnValue(event.getMultiplier());
        }
    }
}

Capturing Variables

@Mixin(PlayerEntity.class)
public class MixinPlayer {
    
    @Inject(
        method = "travel",
        at = @At(
            value = "INVOKE",
            target = "Lnet/minecraft/entity/player/PlayerEntity;setVelocity(DDD)V"
        )
    )
    private void onSetVelocity(
        Vec3d movementInput,
        CallbackInfo ci,
        @Local(ordinal = 0) double x,  // Capture local variable
        @Local(ordinal = 1) double y,
        @Local(ordinal = 2) double z
    ) {
        // x, y, z are captured from the method's locals
        System.out.println("Setting velocity: " + x + ", " + y + ", " + z);
    }
}

Mixin Configuration

Mixins are registered in liquidbounce.mixins.json:
{
  "required": true,
  "minVersion": "0.8",
  "package": "net.ccbluex.liquidbounce.injection.mixins",
  "compatibilityLevel": "JAVA_17",
  "mixins": [
    "minecraft.client.MinecraftAccessor",
    "minecraft.client.MixinClientPlayer",
    "minecraft.block.MixinBlock"
  ],
  "client": [
    "minecraft.render.MixinWorldRenderer"
  ],
  "injectors": {
    "defaultRequire": 1
  }
}

Advanced Techniques

Shadow Fields and Methods

Access target class members directly:
@Mixin(ClientPlayerEntity.class)
public abstract class MixinClientPlayer extends PlayerEntity {
    
    @Shadow
    private boolean autoJumpEnabled;  // Access real field
    
    @Shadow
    protected abstract void updatePose();  // Access real method
    
    @Inject(method = "tick", at = @At("HEAD"))
    private void onTick(CallbackInfo ci) {
        if (autoJumpEnabled) {
            updatePose();
        }
    }
}

Unique Injection

Ensure injection only happens once:
@Inject(
    method = "tick",
    at = @At("HEAD"),
    require = 1,  // Require exactly 1 injection
    allow = 1     // Allow only 1 injection
)
private void onTick(CallbackInfo ci) { }

Slice Injection

Inject in a specific code region:
@Inject(
    method = "complexMethod",
    at = @At("INVOKE", target = "someMethod"),
    slice = @Slice(
        from = @At("HEAD"),
        to = @At("INVOKE", target = "someOtherMethod")
    )
)
private void betweenMethods(CallbackInfo ci) {
    // Only injects in the slice between HEAD and someOtherMethod
}

Debugging Mixins

Enable Mixin Export

Add to JVM arguments:
-Dmixin.debug.export=true
This exports transformed classes to .mixin.out/.

Mixin Logging

-Dmixin.debug.verbose=true
-Dmixin.debug.countInjections=true

Common Issues

Mixin Not Applying: Check that:
  • Target class name is correct (use SRG/intermediary names)
  • Method signature matches exactly
  • Mixin is registered in liquidbounce.mixins.json
  • Method isn’t inlined by JIT compiler

Target Verification

Use @Debug to verify targets:
@Debug(export = true, print = true)
@Mixin(MyTarget.class)
public class MixinMyTarget { }

Best Practices

Minimal Impact

// Good - minimal injection
@Inject(method = "tick", at = @At("HEAD"))
private void onTick(CallbackInfo ci) {
    if (!ModuleManager.shouldProcess()) return;
    // Process...
}

// Bad - always does work
@Inject(method = "tick", at = @At("HEAD"))
private void onTick(CallbackInfo ci) {
    expensiveOperation();  // Runs every tick!
}

Cancellation Guards

@Inject(method = "method", at = @At("HEAD"), cancellable = true)
private void onMethod(CallbackInfo ci) {
    MyEvent event = new MyEvent();
    EventManager.callEvent(event);
    
    // Only cancel if event was cancelled
    if (event.isCancelled()) {
        ci.cancel();
    }
}

Compatibility

Use @Dynamic for runtime-generated methods:
@Dynamic("Generated by AccessWidener")
@Redirect(method = "dynamicMethod", at = @At(/* ... */))
private void hookDynamic() { }

Performance

Mixins are applied at class load time, so:
  • No runtime overhead from injection itself
  • Injected code runs at native speed
  • Use @Inject over @Redirect when possible (lower overhead)

Limitations

Mixin Limitations:
  • Cannot inject into static initializers
  • Cannot modify final fields (use AccessWidener)
  • Cannot add interfaces at runtime
  • Targeting private inner classes is complex
  • Lambda expressions are difficult to target

Resources

Build docs developers (and LLMs) love