Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CCBlueX/LiquidBounce/llms.txt

Use this file to discover all available pages before exploring further.

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