Skip to content
friendlyhj edited this page Oct 27, 2025 · 15 revisions

Mixin

@since 1.20.0

Mixin is a framework to hook into runtime class loading to modify java code. ZenUtils allows to use mixin by zenscript.

The feature is disabled by default. You should enable it in config first.

Create a mixin script

All mixin scripts are loaded by loader mixin. (i.e. #loader mixin) They are not reloadable certainly. Mixin relies a class and annotations. So we need to write zenClass and annotation mechanism. ZenUtils adds a preprocessor to resolve annotation.

Example:

#loader mixin

#mixin Mixin
#{targets: "zmaster587.libVulpes.tile.energy.TilePlugBase"}
zenClass MixinTilePlugBase {
    #mixin ModifyConstant
    #{
    #    method: "getMaxEnergy",
    #    constant: {intValue: 10000}
    #}
    function modifyMaxEnergyModifier(value as int) as int {
        return 20000000;
    }
}

The #mixin NAME is the header of mixin preprocessor, to define which annotation. A json after the header line describes the annotation contents (can be omitted). It still is considered as a preprocessor, so # is still required to be put at the start of every line. Then we write a function or a field after the json. There cannot be an empty line between the three parts.

The example is translated to such java code.

@Mixin(targets = "zmaster587.libVulpes.tile.energy.TilePlugBase")
public class MixinTilePlugBase {
    @ModifyConstant(method = "getMaxEnergy", constant = @Constant(intValue = 10000))
    private int modifyMaxEnergyModifier(int value) {
        return 20000000;
    }
}

Since v1.21.2, these three parts can be compressed to single line. #mixin Mixin can be shortened to #mixin.

#loader mixin

// after v1.21.2
#mixin {targets: "zmaster587.libVulpes.tile.energy.TilePlugBase"}
zenClass MixinTilePlugBase {
    #mixin ModifyConstant {method: "getMaxEnergy", constant: {intValue: 10000}}
    function modifyMaxEnergyModifier(value as int) as int {
        return 20000000;
    }
}

Static functions

Some functions are required to be static by mixin. But zenscript doesn't allow static functions in zenClass. Add #mixin Static to mark the function is static.

Example:

#loader mixin

#mixin Mixin
#{targets: "blusunrize.immersiveengineering.common.blocks.metal.TileEntitySilo"}
zenClass MixinTileEntitySilo {
    #mixin Static
    #mixin ModifyConstant
    #{
    #    method: "<clinit>",
    #    constant: {intValue: 41472}
    #}
    function buffStorage(value as int) as int {
        return 0xffff00;
    }
}

Inject callback

CallbackInfo and CallbackInfoReturnable is required in Inject hooks. Use mixin.CallbackInfo and mixin.CallbackInfoReturnable to import them. CallbackInfoReturnable has a generic type. But Zenscript doesn't support it. So CallbackInfoReturnable#getReturnValue always returns java.lang.Object in zenscript, you must cast it later.

Remap

In modpack environment, the game always be obfuscated. Mixin remap system is unnecessary. ZenUtils hardcodes mixin annotation to remap=false. So srg names are required to mixin inject points, mcp names are not allowed. But you still can use mcp name in function body to call mc code.

Allowed types

In mixin scripts, only native types are allowed, because mixin scripts are loaded before CraftTweaker registers its types.

this0

In mixin of java code, we use ((TargetClass) (Object) this) to cast this to target class. In zenscript, if there is only one target class, you can just call this0 variable, it is already casted to target class. You can also use it to call functions and fields of the super class, including private and protected members!

Mixin config

Where is the mixin config json? No, zenutils loads mixin classes dynamically. You needn't define mixin class name in mixin config. If the target class is client only, add #sideonly client preprocessor to make the mixin script is only loaded on client.

Private Member Access

Functions without mixin annotations will be injected to the target class directly. That means you can add methods to the target class, and invoke them later. Mixin functions can access private members, so it can be used to expose them to outer scripts.

#loader mixin

#mixin {targets: "foo.Bar"}
zenClass MixinFooBar {
    // creates a method to expose a private field
    function getPrivateField() as Type {
        return this0.privateField;
    }

    #mixin Shadow
    static privateStaticField as Type;

    // creates a method to expose a private static field
    // this0 doesn't handle static members, we have to shadow them
    #mixin Static
    function getPrivateStaticField() as Type {
        return privateStaticField;
    }
}
#loader crafttweaker
import native.foo.Bar;

val barObj as Bar = ...;

// gets the private field
barObj.getPrivateField();
// gets the private static field
Bar.getPrivateStaticField();

Mixin Extras Support

@since 1.26.0

MixinExtras is an extension of mixin.

WrapOperation

Operation is required in WrapOperation hooks. Use mixin.Operation to import them. Similar with CallbackInfoReturnable, its call method accepts java.lang.Object... and returns java.lang.Object due to the missing support of generic type of zs.

Parameter Annotations

@Local @Share and @Cancellable are annotations at parameter. We still write a mixin preprocessor at the function declaration to handle them.

Besides the annotation content, we also need a parameter arg to define which parameter annotated. 0 for the first parameter, 1 for the second one, -1 for the last one, -2 for the penultimate one, and so on. The argument can be omitted, the default value is -1(the last one).

About LocalRef, it isn't exposed to zs. An 1-length array simulates it instead. If a LocalRef is needed, add ref: true to the annotation body, and set the type of the parameter to an array.

  • ref.get() -> array[0]
  • ref.set(x) -> array[0] = x
#mixin Mixin
#{targets: "forestry.core.ModuleFluids"}
zenClass MixinModuleFluids {
    #mixin WrapOperation
    #{
    #   method: "doInit",
    #   at: {
    #       value: "INVOKE",
    #       target: "Lforestry/api/recipes/ISqueezerManager;addContainerRecipe(ILnet/minecraft/item/ItemStack;Lnet/minecraft/item/ItemStack;F)V",
    #       ordinal: 0
    #   }
    #}
    #mixin Local {parameter: -1, ref: true}
    // -1 is the default value, define explicitly for clarification
    // `ref: true` to use LocalRef, and the parameter type is array
    // annotate `@Local` the last parameter 
    function removeRecipe(
        manager as ISqueezerManager,
        timePerItem as int,
        emptyContainer as ItemStack,
        remnants as ItemStack,
        chance as float,
        operation as mixin.Operation, // required for WrapOperation
        itemRegistryCore as ItemRegistryCore[] // the local variable capture, `LocalRef<ItemRegistryCore>` in java, but array instead in zs
    ) as void {
        operation.call(manager, timePerItem, emptyContainer, remnants, chance);
        print(toString(itemRegistryCore[0])); // gets and prints the local variable
        itemRegistryCore[0] = null; // sets the local variable
    }
}

Note: zs compiler doesn't keep parameter name, @Local implicit lookup may fail. If problem occurred, set one of ordinal, index and name to enable its explicit mode.

More examples

You can view more examples in this PR.

Clone this wiki locally