Expressions - LlamaLad7/MixinExtras GitHub Wiki
Expressions allow you to use java-like strings to target complex pieces of bytecode.
Make sure to follow the setup instructions first. Let's have a look at an example first of all, and more thorough documentation of the expression language itself can be found here:
Example
I suggest reading the ModifyExpressionValue page first.
When targeting code such as the following:
if (this.fallDistance > 0.0F) {
doSomething();
doSomeOtherThing();
}
you may wish to add your own check in the if condition.
This could be done like so:
@Definition(id = "fallDistance", field = "Lnet/minecraft/entity/Entity;fallDistance:F")
@Expression("this.fallDistance > 0.0")
@ModifyExpressionValue(method = "fall", at = @At("MIXINEXTRAS:EXPRESSION"))
private boolean yourHandler(boolean original) {
return original && MyMod.shouldFall(this);
}
Let's unpack that:
- The
@Atwe use in the main@ModifyExpressionValueis@At("MIXINEXTRAS:EXPRESSION"). That simply means the actual target will be expressed in the@Expressionannotation. - As you might expect from the name,
@ModifyExpressionValueworks great with expressions, and you can modify the result of any expression with it. - Our
@Expressiondefines the code fragment we want to target. This is much more specific than a normal@Atbecause it will not only check for the comparison, but also match the left and right hand sides. - We use
@Definitions to define any identifiers used in our@Expression. In this example we definefallDistanceas referring to the specificfloat fallDistancefield inEntity. The formats accepted here are the same as those accepted in an@At("FIELD"). There are several built-in identifiers, likethis, which you don't need to define. - The
@Expressionwill (by default) return the "last" instruction in your expression. In this case that is the>comparison, because both the left and right must be evaluated before they can be compared. - Our handler method works like normal. We are modifying the result of a
>comparison, so we take and return abooleanbecause that's what a comparison returns. - The
@Expressionlanguage has no distinction between floats and doubles, so we simply use0.0for the right hand side.
Code Diff
- if (this.fallDistance > 0.0F) {
+ if (yourHandler(this.fallDistance > 0.0F)) {
...
Key things to note
@Expressions work with bytecode, not source code, like all of mixin. They are designed to be written in a way similar to source code, for your convenience, but they cannot magically match things you just copy and paste. This is also why you need@Definitions in the first place.@Expressions can be used with any injector, with some special cases:@ModifyExpressionValuecan modify the result of any expression.@WrapOperationcan wrap all its normal things in addition to comparisons and array sets/gets
@Expressions cannot currently match expressions involving jumps, e.g.a && !b,a ? b : c, etc. The slight exception to this is comparisons, which can be matched, but can only be used as the outermost part of an expression. You could not for example matchprint(a == b). Of course you can use wildcards to match these things as part of a wider expression, see below for them...
Another example
Let's say we want to inject after this code:
this.emitGameEvent(GameEvent.ENTITY_MOUNT, passenger);
We could do this like so:
@Definition(id = "emitGameEvent", method = "Lnet/minecraft/entity/Entity;emitGameEvent(Lnet/minecraft/world/event/GameEvent;Lnet/minecraft/entity/Entity;)V")
@Definition(id = "ENTITY_MOUNT", field = "Lnet/minecraft/world/event/GameEvent;ENTITY_MOUNT:Lnet/minecraft/world/event/GameEvent;")
@Expression("this.emitGameEvent(ENTITY_MOUNT, ?)")
@Inject(method = "addPassenger", at = @At(value = "MIXINEXTRAS:EXPRESSION", shift = At.Shift.AFTER))
private void yourHandler(CallbackInfo ci) {
System.out.println("Hi!");
}
Let's unpack the new stuff:
- In our
@Expressionwe used a wildcard:?. It is not practical to define everything in a large expression, so we can use wildcards to omit the things we don't care about. Anything will match in their place. Note that wildcards can be used as expressions, like above, but also as identifiers, e.g.this.?(), which will match calls to any method onthisthat take no arguments. - We can use
@Definitions to define both methods and fields. In each case the string should be of a format that the relevant type of@Ataccepts. - Static methods and fields are expressed with no receiver (i.e.
SOME_FIELD, notSomeClass.SOME_FIELD) - We can use normal things in our
@At("MIXINEXTRAS:EXPRESSION"), likeshiftandordinal.Slices also work as expected. The@Expressionitself will match the "last" thing in the chain, which is theemitGameEventcall, and theshiftwill go one instruction forward. -
Code Diff
this.emitGameEvent(GameEvent.ENTITY_MOUNT, passenger);
+ yourHandler(new CallbackInfo());
...
A more complex example
Let's say we wanted to modify the result of this code:
new BlockStateParticleEffect(ParticleTypes.BLOCK, blockState)
We could do this like so:
@Definition(id = "BlockStateParticleEffect", type = BlockStateParticleEffect.class)
@Definition(id = "BLOCK", field = "Lnet/minecraft/particle/ParticleTypes;BLOCK:Lnet/minecraft/particle/ParticleType;")
@Definition(id = "blockState", local = @Local(type = BlockState.class))
@Expression("new BlockStateParticleEffect(BLOCK, blockState)")
@ModifyExpressionValue(method = "spawnSprintingParticles", at = @At("MIXINEXTRAS:EXPRESSION"))
private BlockStateParticleEffect yourHandler(BlockStateParticleEffect original) {
return YourMod.processParticle(original);
}
Lots to unpack there:
@Definitions can contain not onlymethods andfields, but alsotypes andlocals. Types are to be used when targeting instantiations,instanceofchecks, or casts. The@Localannotation works more or less as explained here, but with the addition of atypeparameter to specify the type of the local. Note that because we do not specify anordinal, this will only match if there is exactly 1 local of that type at the targeted place. In a real situation, I would probably use a wildcard for that local, since targeting locals is often brittle, but I've specified it here as an example.
Code Diff
- new BlockStateParticleEffect(ParticleTypes.BLOCK, blockState)
+ yourHandler(new BlockStateParticleEffect(ParticleTypes.BLOCK, blockState))
...
Quickfire Examples
Here are some pieces of Java code together with expressions you could use to match them:
this.pistonMovementDelta[i] = d;- We could use
this.pistonMovementDelta[?] = ? - We could make
@Definitions for the locals if we wanted, but that is likely to be more brittle here
- We could use
nbt.putShort("Fire", (short)this.fireTicks);- We could use
?.putShort('Fire', (short) this.fireTicks) - Note that strings use single quotes since the entire expression will be in a string literal
- However in actuality all we'd probably want is
?.putShort('Fire', ?)
- We could use
entityKilled instanceof ServerPlayerEntity- We could use
? instanceof ServerPlayerEntity - Same comment as above about the local
- We could use
return this.distance < d * d;- We could use
return this.distance < ? * ? - Or we could specify the local
- We could use
Targeted Expressions
I mentioned earlier that an @Expression will target the "last" thing in the expression, but this is only a default. Consider the code:
throw new IllegalStateException("Oh no!");
We might want to modify that exception before it is thrown, so to target it we could use the expression:
throw @(new IllegalStateException('Oh no!'))
The @(...) there is called a target. Instead of returning the "last" instruction (the throw), this will return the instantiation, allowing you to @ModifyExpressionValue it to your heart's content.
If you do not target any expressions explicitly, then the entire expression is implicitly targeted, i.e.
this.myMethod(5)
is equivalent to
@(this.myMethod(5))
You can however have multiple explicit targets if you want.
Things to watch out for
- Some things can't be distinguished in bytecode form:
true/falseare equivalent to1/0- Characters are just numbers, e.g.
'A'is equivalent to65 - Comparisons usually cannot be distinguished from their inverses. E.g.
x >= ylooks the same asx < y(but with an opposite effect). For that reason if you target a comparison make sure your expression is specific enough that it will only match what you expect. Note that due toNaNsemantics, this gotcha does not apply tofloats anddoubles - Be particularly careful when using
== 0or!= 0, because even the simple
looks the same asif (myBoolean) { ... }
and hence due to point 1,if (myBoolean != false) { ... }? != 0would match it, and due to point 3,? == 0would match it. To avoid this just make sure to use a specific expression instead of a wildcard.
- It is not wise to target a wildcard. If you were to
@ModifyExpressionValueit, you may not know what its concrete type is, i.e. it might be a subclass of what you were expecting, meaning your handler signature would be wrong. More generally, if the wildcard represents a "complex" expression (one involving jumps) then it would not be possible to target it at all, with any injector. Instead, try to use a different injector. E.g. say we wanted to modify the argument to:
I would not usethis.setX(...);this.setX(@(?))with a@ModifyExpressionValue. Instead, usethis.setX(?)with a@ModifyArg.