agentsclimarketplace

Fabric mixins

Skill PSB1234/Fabric-Minecraft-Skill-1.20.1/fabric-mixins

Deep reference for Fabric/SpongePowered Mixins in Minecraft 1.20.1 modding. Use this skill whenever the user asks about Mixin injection, @Inject, @Redirect, @ModifyArg, @ModifyVariable, @Overwrite, @Accessor, @Invoker, capturing locals, debugging mixin failures, mixin conflicts, or any advanced bytecode-manipulation pattern. Triggers on: "mixin", "@Inject", "@Redirect", "CallbackInfo", "injection point", "capture locals", "accessor mixin", "invoker mixin", "@At INVOKE", "mixin not applying", "mixin crash", or any question about hooking into Minecraft internals at the bytecode level. Part of the fabric-mc-modding skill family.From its SKILL.md

Install
npx -y skills add PSB1234/Fabric-Minecraft-Skill-1.20.1 --skill fabric-mixins

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

12.0 KB, ~2.9k tokens by cl100k_base, as published. Nobody here has run it

Fabric Mixins — 1.20.1 Deep Reference

When to use Mixins vs Events: Always prefer a Fabric API event if one exists. Mixins are for cases where no event covers your need. See the fabric-events skill for available events.


Setup

mymod.mixins.json (at src/main/resources/):

{
  "required": true,
  "minVersion": "0.8",
  "package": "com.example.mymod.mixin",
  "compatibilityLevel": "JAVA_17",
  "mixins":  ["SomeMixin"],
  "client":  ["ClientOnlyMixin"],
  "server":  [],
  "injectors": { "defaultRequire": 1 }
}
  • mixins — applied on both sides
  • client — applied only on the logical client
  • server — applied only on the dedicated server
  • "defaultRequire": 1 — fails at load if any injection doesn't match (recommended)

@Inject — the workhorse

@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {

    // HEAD: top of method, before any code
    @Inject(at = @At("HEAD"), method = "onDeath")
    private void beforeDeath(DamageSource source, CallbackInfo ci) { }

    // RETURN: fires before *each* return statement
    @Inject(at = @At("RETURN"), method = "getHealth")
    private void afterGetHealth(CallbackInfoReturnable<Float> cir) {
        // cir.getReturnValue() = what was about to be returned
    }

    // TAIL: fires before the *final* return only
    @Inject(at = @At("TAIL"), method = "tick")
    private void atEnd(CallbackInfo ci) { }

    // Cancel execution mid-method
    @Inject(at = @At("HEAD"), method = "damage", cancellable = true)
    private void cancelDamage(DamageSource src, float amount, CallbackInfoReturnable<Boolean> cir) {
        if (src.isOf(DamageTypes.FALL)) {
            cir.setReturnValue(false); // cancel + set return value
        }
    }
}

Targeting a Specific Overload

If a class has multiple methods with the same name, add the descriptor:

// method = "damage(Lnet/minecraft/entity/damage/DamageSource;F)Z"
// Format: methodName(ArgTypes)ReturnType
// Use L...;  for objects, [ for arrays, Z=boolean I=int F=float V=void etc.

@At — Injection Points

ValueFires at
"HEAD"Top of method
"RETURN"Every return statement
"TAIL"The final return only
"INVOKE"Before a specific method call (use target=)
"INVOKE_ASSIGN"After a specific method call whose return is assigned
"FIELD"Field read or write (use target=, opcode=)
"NEW"Object instantiation (new Foo(...))
"JUMP"A branch instruction (advanced)
"CONSTANT"A compile-time constant (advanced)

INVOKE target descriptor format

@At(value = "INVOKE",
    target = "Lnet/minecraft/entity/LivingEntity;heal(F)V")
// "L<class/path>;methodName(args)return"
// dots → slashes in class paths

Shifting injection point

// Fire 2 instructions AFTER the INVOKE target
@At(value = "INVOKE", target = "...", shift = At.Shift.AFTER, by = 2)

@Redirect — Replace a Method Call

Replaces one specific call site inside a method. Your method body becomes the call.

@Mixin(PlayerEntity.class)
public abstract class PlayerMixin {

    @Redirect(
        method = "tick",
        at = @At(value = "INVOKE",
            target = "Lnet/minecraft/entity/LivingEntity;heal(F)V")
    )
    private void redirectHeal(LivingEntity instance, float amount) {
        // replace the heal() call — do NOT call instance.heal() to cancel it,
        // or call it with modified args:
        instance.heal(amount * 2);
    }
}

⚠️ @Redirect completely replaces the call — only ONE mod can redirect the same call site. Prefer @ModifyArg or @Inject at INVOKE for better compatibility.


@ModifyArg — Change One Argument to a Method Call

Better compatibility than @Redirect when you only need to tweak one argument:

@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {

    // Intercept the heal() call inside tick() and double the amount
    @ModifyArg(
        method = "tick",
        at = @At(value = "INVOKE",
            target = "Lnet/minecraft/entity/LivingEntity;heal(F)V"),
        index = 0  // which argument to intercept (0-based)
    )
    private float modifyHealAmount(float amount) {
        return amount * 2;
    }
}

For methods with multiple args of the same type, use index to pick the right one.


@ModifyVariable — Change a Local Variable

Changes the value of a local variable at a specific point:

@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {

    @ModifyVariable(
        method = "damage",
        at = @At("HEAD"),
        argsOnly = true,  // only look in method parameters
        index = 2         // slot index in the local variable table
    )
    private float modifyDamageAmount(float amount) {
        return amount * 0.5f; // halve all incoming damage
    }
}

Use Bytecode Viewer or --printFrames to find the correct index.


Capturing Local Variables with @Inject

When you need access to a local variable inside the target method:

@Mixin(SomeMCClass.class)
public abstract class SomeMixin {

    @Inject(
        method = "targetMethod",
        at = @At(value = "INVOKE",
            target = "Lsome/Class;someCall()V"),
        locals = LocalCapture.CAPTURE_FAILSOFT  // or CAPTURE_FAILHARD / PRINT
    )
    private void onSomeCall(CallbackInfo ci,
        // After ci, list ALL locals in table order at that injection point:
        int localInt, float localFloat, SomeType localObj) {
        // use localObj here
    }
}

LocalCapture options:

  • CAPTURE_FAILHARD — crash if locals don't match (use during dev)
  • CAPTURE_FAILSOFT — log warning and skip injection on mismatch
  • PRINT — prints the local variable table to console (for debugging)

@Accessor — Read/Write Private Fields

Create a mixin interface (not class) to expose private fields:

@Mixin(PlayerEntity.class)
public interface PlayerEntityAccessor {

    // Getter
    @Accessor("hungerManager")
    HungerManager getHungerManager();

    // Setter
    @Accessor("hungerManager")
    void setHungerManager(HungerManager manager);
}

// Usage from anywhere (cast):
HungerManager hm = ((PlayerEntityAccessor) player).getHungerManager();

@Invoker — Call Private Methods

@Mixin(SheepEntity.class)
public interface SheepEntityInvoker {

    @Invoker("setHeadRollingTimeLeft")
    void invokeSetHeadRollingTimeLeft(int ticks);
}

// Usage:
((SheepEntityInvoker) sheepEntity).invokeSetHeadRollingTimeLeft(10);

@Overwrite — Full Method Replacement (Last Resort)

Completely replaces a method. Extremely fragile — breaks if Mojang changes the method.

@Mixin(SomeClass.class)
public abstract class SomeMixin {

    @Overwrite
    public void targetMethod(int arg) {
        // your entire replacement implementation
    }
}

⛔ Avoid @Overwrite. It's incompatible with any other mod touching the same method. Use @Inject + cancellable = true instead when possible.


Shadow — Access Target Class Members

@Shadow lets your mixin reference fields/methods of the target class:

@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {

    @Shadow
    private int stuckArrowTimer; // access a private field

    @Shadow
    public abstract float getHealth(); // access a method

    @Shadow @Final
    private static Map<EntityAttribute, EntityAttributeInstance> attributes; // final field

    @Inject(at = @At("HEAD"), method = "tick")
    private void onTick(CallbackInfo ci) {
        // use stuckArrowTimer directly
        if (stuckArrowTimer > 0) { /* ... */ }
    }
}

Mixin Priorities & Compatibility

@Mixin(value = LivingEntity.class, priority = 900)
// Default priority = 1000. Lower = applied first.
// Use lower priority to run before other mods, higher to run after.

For @Inject at the same point from two mods — both fire (order by priority). For @Redirect at the same call site — only one can exist. Use @ModifyArg instead.


Debugging Mixin Failures

Symptom: Mixin not applying at all

  1. Verify class name is in the correct array in mymod.mixins.json (mixins/client/server)
  2. Check package matches "package" in mixins.json
  3. Enable mixin debug output in gradle.properties:
    mixin.debug=true
    mixin.debug.verbose=true
    mixin.debug.export=true  # writes patched classes to .mixin.out/ folder
    

Symptom: @At(INVOKE) target not found

  • Target descriptor uses slashes, not dots: Lnet/minecraft/entity/LivingEntity;heal(F)V
  • Double-check the method signature with Bytecode Viewer / Fernflower / genSources
  • If the method is overloaded, the descriptor must be exact

Symptom: Local capture mismatch crash

  • Use LocalCapture.PRINT to dump the local variable table, then fix your parameter list

Symptom: ClassCastException with Accessor

  • The accessor interface must be applied via @Mixin — make sure it's in mixins.json
  • Cast with instanceof check if unsure: if (entity instanceof PlayerEntityAccessor acc)

Symptom: Mixin applied but wrong result

  • Add mixin.debug.export=true and inspect the .mixin.out/ folder — decompile the class to verify your injection landed where expected

Mixin + Custom Event Pattern

The canonical way to expose an injection point to other mods:

// 1. Define the event interface (in non-mixin package)
@FunctionalInterface
public interface SheepShearCallback {
    Event<SheepShearCallback> EVENT = EventFactory.createArrayBacked(
        SheepShearCallback.class,
        listeners -> (sheep, player) -> {
            for (SheepShearCallback l : listeners) l.onShear(sheep, player);
        }
    );
    void onShear(SheepEntity sheep, PlayerEntity player);
}

// 2. Fire it from a mixin
@Mixin(SheepEntity.class)
public abstract class SheepEntityMixin {
    @Inject(method = "interactMob",
        at = @At(value = "INVOKE",
            target = "Lnet/minecraft/entity/passive/SheepEntity;sheared(Lnet/minecraft/sound/SoundCategory;)V"))
    private void onShear(PlayerEntity player, Hand hand, CallbackInfoReturnable<ActionResult> cir) {
        SheepShearCallback.EVENT.invoker().onShear((SheepEntity)(Object)this, player);
    }
}

Scripts

ScriptWhat it does
scripts/add_mixin.pyScaffold a mixin class + auto-update mixins.json
python scripts/add_mixin.py \
  --mod-id mymod --group com.example \
  --target net.minecraft.entity.LivingEntity \
  --name LivingEntityMixin \
  --side both \
  --inject-method onDeath --inject-at HEAD

References

Load these when needed — they are not in the main SKILL.md to keep it concise:

FileWhen to load
references/jvm-descriptors.mdWriting @At(target=...) descriptors; full type table + common MC class paths
references/mixin-extras.mdUsing @ModifyReturnValue, @WrapOperation, @WrapWithCondition, @Local, @Share

Further Reading

Related Skills

  • fabric-mc-modding — project setup, gradle, general API
  • fabric-events — prefer events over mixins when available

What ships with it: 3 files

14.9 KB alongside SKILL.md, 1 of them executable

references/

scripts/

Keep looking

Skills are one crate of 326,861. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.