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() { }
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