@Decorate Usage
We provide @Decorate as a new mixin injector. It provides:
ultimate compatibility to
@Redirectinjector, andbe able to modify/allocate multiple variables in target method stack, and
be able to insert control flow instructions
Basic Usage
Inject mode
@Decorate can insert a piece of code into target method, just like @Inject:
@Decorate(method = "test", inject = true, at = @At("HEAD"))
public void inject() throws Throwable {
System.out.println("test() is callded");
}In inject mode, you MUST NOT have CallbackInfo in method signature.
Redirect mode
@Decorate can work like @Redirect, redirects method call, field access or object creation. In this case, @Decorate has the same method signature as @Redirect:
// redirect method call
@Decorate(method = "test", at = @At(value = "INVOKE", target = "Ljava/io/PrintStream;println(Ljava/lang/String;)V"))
public void redirect1(PrintStream s, String s2) throws Throwable {
if (s2.equals("foo")) {
return; // Do not print "foo"
}
DecorationOps.callsite().invoke(s, s2);
}
// redirect field access
@Decorate(method = "test", at = @At(value = "FIELD", target = "Ljava/lang/System;out:Ljava/io/PrintStream;"))
public PrintStream redirect2() throws Throwable {
var s = (PrintStream) DecorationOps.callsite().invoke();
return new PrintStream(s);
}
// redirect object creation
@Decorate(method = "test", at = @At(value = "NEW", target = "net/minecraft/core/BlockPos"))
public BlockPos redirect3(int x, int y, int z) throws Throwable {
return (BlockPos) DecorationOps.callsite().invoke(x+1, y+2, z+3);
}Additionally, @Decorate support redirect method return. In this case, it accept the target method return type as argument and MUST have a void return type:
// redirect method return
@Decorate(method = "getBlockState", at = @At("RETURN"))
public void redirect4(BlockState state) throws Throwable {
if (state == null) {
state = Blocks.AIR.defaultBlockState();
}
DecorationOps.callsite().invoke(state);
}In redirect mode, you MUST have EXACT ONE DecorationOps.callsite().invoke call. It represents the injection point of this mixin, and the parameters passed to invoke is given to the original injection point:
// target method
private void test() {
System.out.println("foo");
LOGGER.info("bar");
}
@Decorate(method = "test", at = @At(value = "INVOKE", target = "Ljava/io/PrintStream;println(Ljava/lang/String;)V"))
public void redirect(PrintStream s, String s2) throws Throwable {
if (s2.equals("foo")) {
return; // Do not print "foo"
} else {
DecorationOps.callsite().invoke(s, s2 + "bar");
}
}
// after decorate
private void test() {
if (s2.equals("foo")) {
} else {
System.out.println("foo" + "bar");
}
LOGGER.info("bar");
}Mixins from other mods can still redirect the println method call, together with our @Decorate mixin.
Insert RETURN in target method
@Decorate can cancel the target method like @Inject(cancellable = true). All you need to do is to add a DecorationOps.cancel().invoke call:
// target method
private void test() {
System.out.println("foo");
LOGGER.info("bar");
}
@Decorate(method = "test", at = @At(value = "INVOKE", target = "Ljava/io/PrintStream;println(Ljava/lang/String;)V"))
public void redirect(PrintStream s, String s2) throws Throwable {
if (!s2.equals("foo")) {
DecorationOps.callsite().invoke(s, s2 + "bar");
} else {
DecorationOps.cancel().invoke();
return; // this return is essential, even if your IDE report it redundant
}
}
// after decorate
private void test() {
if (!s2.equals("foo")) {
System.out.println("foo" + "bar");
} else {
return;
}
LOGGER.info("bar");
}All cancel() MUST be followed by a return statement, even if it’s at the end of a code block, or your IDE report it redundant.
If target method has non-void return type, a parameter with exact type MUST be passed to cancel().invoke:
@Decorate(method = "getBlockState", at = @At(value = "INVOKE", target = "Ljava/io/PrintStream;println(Ljava/lang/String;)V"))
public void redirect2(PrintStream s, String s2) throws Throwable {
if (!s2.equals("foo")) {
DecorationOps.callsite().invoke(s, s2 + "bar");
} else {
// type must be exact with target method return type
DecorationOps.cancel().invoke((BlockState) null);
return;
}
}Capture and modify variables
@Decorate can capture variables from target method by appending arguments to injector method, just like most standard mixin injectors.
You can assign these captured variable with new value and they will take effect in target method.
// target method
private BlockState test(Level level, BlockPos pos) {
BlockState state = level.getBlockState(pos);
if (state.is(Blocks.AIR)) {
LOGGER.info("bar");
}
return state;
}
@Decorate(method = "test", inject = true, at = @At("HEAD"))
public void modifyVariable(Level level, BlockPos pos) throws Throwable {
pos = pos.above();
}
// after decorate
private BlockState test(Level level, BlockPos pos) {
pos = pos.above();
BlockState state = level.getBlockState(pos);
if (state.is(Blocks.AIR)) {
LOGGER.info("bar");
}
return state;
}You can use @Local(ordinal = <ordinal>) to capture variable by type and their ordinal, negative ordinal refers to last N variable by type:
@Decorate(method = "test", inject = true, at = @At(value = "INVOKE", target = "is(**)*"))
public void modifyVariable(@Local(ordinal = -1) BlockState state) throws Throwable {
// state refers to the most close BlockState variable before the state.is call
}You can use @Local(allocate = <name>) to allocate new variable in target method. These variable can be shared across @Decorate injectors:
// target method
private BlockState test(Level level, BlockPos pos) {
BlockState state = level.getBlockState(pos);
if (state.is(Blocks.AIR)) {
LOGGER.info("foo");
}
if (state.is(Blocks.STONE)) {
LOGGER.info("bar");
}
return state;
}
@Decorate(method = "test", inject = true, at = @At(value = "INVOKE", target = "is(**)*"))
public void modifyVariable(@Local(allocate = "count") int count) throws Throwable {
count++;
LOGGER.info("is() called {} times", count)
}
// after decorate
private BlockState test(Level level, BlockPos pos) {
BlockState state = level.getBlockState(pos);
int count = 0;
count++;
LOGGER.info("is() called {} times", count);
if (state.is(Blocks.AIR)) {
LOGGER.info("foo");
}
count++;
LOGGER.info("is() called {} times", count);
if (state.is(Blocks.STONE)) {
LOGGER.info("bar");
}
return state;
}DecorationOps.blackhole() Usage
In the cases where parts of the mixin code reports as redundant, DecorationOps.blackhole() can eliminate these warnings:
@Decorate(method = "test", inject = true, at = @At("HEAD"))
public void blackhole1(Level level, BlockPos pos) throws Throwable {
pos = pos.above(); // warning: Variable 'pos' is never used
DecorationOps.blackhole().invoke(pos);
}
@Decorate(method = "test", at = @At(value = "INVOKE", target = "Ljava/io/PrintStream;println(Ljava/lang/String;)V"))
public void redirect(PrintStream s, String s2) throws Throwable {
if (!s2.equals("foo")) {
DecorationOps.callsite().invoke(s, s2 + "bar");
} else {
DecorationOps.cancel().invoke();
return; // warning: 'return' is unnecessary as the last statement in a 'void' method
}
DecorationOps.blackhole().invoke();
}All expressions passed to the invoke will be erased after mixin applied.