Skip to content

Expressions

LlamaLad7 edited this page Aug 18, 2024 · 2 revisions

This feature is in beta. It may be changed in a future release and may cause unforeseen issues in its current state. Make sure you are using the latest beta.

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 @At we use in the main @ModifyExpressionValue is @At("MIXINEXTRAS:EXPRESSION"). That simply means the actual target will be expressed in the @Expression annotation.
  • As you might expect from the name, @ModifyExpressionValue works great with expressions, and you can modify the result of any expression with it.
  • Our @Expression defines the code fragment we want to target. This is much more specific than a normal @At because 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 define fallDistance as referring to the specific float fallDistance field in Entity. The formats accepted here are the same as those accepted in an @At("FIELD"). There are several built-in identifiers, like this, which you don't need to define.
  • The @Expression will (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 a boolean because that's what a comparison returns.
  • The @Expression language has no distinction between floats and doubles, so we simply use 0.0 for 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:
    • @ModifyExpressionValue can modify the result of any expression.
    • @WrapOperation can 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 match print(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 @Expression we 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 on this that 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 @At accepts.
  • Static methods and fields are expressed with no receiver (i.e. SOME_FIELD, not SomeClass.SOME_FIELD)
  • We can use normal things in our @At("MIXINEXTRAS:EXPRESSION"), like shift and ordinal. Slices also work as expected. The @Expression itself will match the "last" thing in the chain, which is the emitGameEvent call, and the shift will 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 only methods and fields, but also types and locals. Types are to be used when targeting instantiations, instanceof checks, or casts. The @Local annotation works more or less as explained here, but with the addition of a type parameter to specify the type of the local. Note that because we do not specify an ordinal, 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
  • 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', ?)
  • entityKilled instanceof ServerPlayerEntity
    • We could use ? instanceof ServerPlayerEntity
    • Same comment as above about the local
  • return this.distance < d * d;
    • We could use return this.distance < ? * ?
    • Or we could specify the local

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/false are equivalent to 1/0
    • Characters are just numbers, e.g. 'A' is equivalent to 65
    • Comparisons usually cannot be distinguished from their inverses. E.g. x >= y looks the same as x < 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 to NaN semantics, this gotcha does not apply to floats and doubles
    • Be particularly careful when using == 0 or != 0, because even the simple
       if (myBoolean) { ... }
      looks the same as
       if (myBoolean != false) { ... }
      and hence due to point 1, ? != 0 would match it, and due to point 3, ? == 0 would 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 @ModifyExpressionValue it, 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:
     this.setX(...);
    I would not use this.setX(@(?)) with a @ModifyExpressionValue. Instead, use this.setX(?) with a @ModifyArg.