-
Notifications
You must be signed in to change notification settings - Fork 11
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.
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;
}
}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;
}
}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.
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.
In mixin scripts, only native types are allowed, because mixin scripts are loaded before CraftTweaker registers its types.
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!
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.
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();@since 1.26.0
MixinExtras is an extension of mixin.
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.
@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.
You can view more examples in this PR.
- GlobalFunctions
- ScriptReloading
- SuppressErrorPreprocessor
- HardFailPreprocessor
- OrderlyMap
- IData Deep Update
- Template String
- Nullish Operators
- Array and List Operations
- Native Method Access
- Mixin
- CrTI18n
- CrTUUID
- CrTItemHandler
- CrTLiquidHandler
- ILiquidTankProperties
- StringList
- HexHelper
- StaticString
- Catenation
- PersistedCatenation
- PlayerStat
- IStatFormatter
- GameRuleHelper
- ZenCommand
- ZenCommandTree
- ZenUtilsCommandSender
- IGetCommandUsage
- ICommandExecute
- IGetTabCompletion
- CommandUtils