Table of Contents
SpongePowered Mixin allows you to modify compiled classes on load at runtime. Several mixins from different authors can be layered on top of the same targets, allowing for reasonable compatibility even between mods that alter the same vanilla code.
Configuration
This is an example configuration file for declaring your mixins:
- example.mixins.json
{ "required": true, "minVersion": "0.8", "package": "fun.raccoon.example.mixin", "compatibilityLevel": "JAVA_8", "mixins": [ "BothMixin" ], "client": [ "client.ClientMixin" ], "server": [ "server.ServerMixin" ] }
required
specifies that this set of mixins failing to apply is fatal.minVersion
minimum Mixin version that may be used.package
package to use as a prefix for the later specified mixins.compatibilityLevel
minimum Java version.mixins
,client
,server
are lists of mixins to apply. The side distinction is physical, meaning e.g.server
mixins will not be applied to modern Minecraft's logical server built into the client distribution, only the dedicated server.
See also: Mixin Wiki: Mixin configuration files
For Fabric to see your mixins, you'll have to specify the location of this config file in your mod configuration too.
- fabric.mod.json
{ /* ... */ "mixins": [ "example.mixins.json" ], /* ... */ }
Mixin classes
A mixin class is annotated with @Mixin
, which allows you to specify the target class(es) it applies to, and customarily marked abstract to prevent accidental instantiation. Visibility doesn't matter, but popular convention seems to be to mark it public.
- MyFooMixin.java
@Mixin(value = Foo.class) public abstract class MyFooMixin
A mixin's value
can also be an array of classes, if the same mixin should be applied to several classes seperately.
The annotation also accepts the following optional elements:
int priority() default 1000
The order to apply relative to other mixins, lowest value first.String[] targets() default {}
Fully qualified locations of targets as strings if they aren't public enough to refer to them by value. Inner classes are referred to with the special syntaxcom.example.OuterClass$InnerClass
boolean remap() default true
By default, mixins seek an obfuscation mapping to use for translating members referenced by the mixin. This is a sensible default for game modding, but in some cases you might need to turn it off.
Mixin methods and their annotations
Methods of a mixin are used to modify the target class. They can be annotated to signal how in particular they should be used to modify the target.
Conceptually, this
from the perspective of these methods is the target class. Prefer to avoid treating it like it's Foo
in your code, but if you need to, make sure your mixin extends
and implements
all necessary classes, and then cast: (Foo)(Object)this
Overwriting a target method
The default (but certainly not the preference) is to simply merge each method into the target, overwriting if a method already exists by the same name. This, of course, can break all lower-priority mixins that modify the same methods, so you should avoid it.
If you really do need the overwrite behavior for some reason, annotate your method with @Overwrite
to be more explicit about your intentions and to gain access to remap
behavior.
Modifying a target method's body
Overarching concepts
For each annotation that modifies the body of a method, target selectors are used to refer to particular class members, and injection points are used to refer to specific parts of the method body. It's important to understand the syntax and behavior of these concepts before continuing.
Like the @Mixin
itself, each annotation can specify boolean remap
to enable or disable obfuscation mapping behavior.
Since these annotations can match any number of different points in the method body, these common int
elements are provided to sanity check the number of injection points:
require
Minimum number of injectionsexpect
Likerequire
, but only in effect if-Dmixin.debug.countInjections=true
allow
Maximum number of injections
@Inject
@Inject
places a call to your method at a specific place in your target to add new behavior. Here's an example:
- Foo.java
public class Foo { public int bar(int x) { System.out.println("Hello, world!"); return x; } }
- FooMixin.java
@Inject(method = "bar", at = @At("TAIL")) private void injected(int x) { System.out.println("bar got " + x); }
- Result
public int bar(int x) { System.out.println("Hello, world!"); System.out.println("bar got " + x); // <-- return x; }
The injected method is always void
and returning from it does nothing special. However, @Inject
also accepts boolean cancellable
which, if set to true, allows you to append a CallbackInfo
or CallbackInfoReturnable
to your argument list and use it to force the target function to return early: CallbackInfo.cancel()
for void functions, CallbackInfoReturnable.setReturnValue(R returnValue)
otherwise.
- FooMixin.java
@Inject(method = "bar", at = @At("HEAD"), cancellable = true) private void injected(int x, CallbackInfoReturnable cir) { if (x == 0) { cir.setReturnValue(x+1); } }
@Redirect
Instead of injecting between instructions, @Redirect
allows you to modify the operation itself, using your method instead of the original action to determine the result. Unless you choose to delegate it, the original operation is never performed.
Redirecting a method call
In the case of method calls, your method acts as if it's the method being called, and it receives all the necessary arguments to act as such. For T Foo.bar(args…)
, your method is T bar(Foo foo, args…)
.
Additionally, you can choose to receive the arguments for the outer method, too. Here's an example:
- Foo.java
public class Foo { public int x; public int incrementX(int by) { Crementer in = new Crementer(by); this.x = in.crement(this.x); return this.x; } }
- FooMixin.java
@Redirect(method = "incrementX", at = @At(value = "INVOKE", target = "Lpath/to/Crementer;crement(I)I")) private int actuallyDecrement( Crementer in, // the target's class instance int x, // the target's arguments int by // incrementX's arguments ) { Crementer de = new Crementer(-by); return de.crement(x); // >:) }
- Result
public int incrementX(int by) { Crementer in = new Crementer(by); this.x = (() -> { // <-- Crementer de = new Crementer(-by); // <-- return de.crement(x); // <-- })() // <-- return this.x; }
Redirecting field access
If you inject at FIELD
s, your method can intercept reads and writes to those fields. The signature of your method changes depending on what type of field access you're targeting:
opcode | args | return |
---|---|---|
GETSTATIC | <field> |
|
GETFIELD | <owner> | <field> |
PUTSTATIC | <field> | void |
PUTFIELD | <owner>, <field> | void |
...
TODO: array access, array length builtin, new, instanceof
@ModifyArg, @ModifyArgs, @ModifyVariable, @ModifyConstant
These annotations allow you to modify the variable references and literals in a method body.
The method provided to these annotations accepts one argument and returns a value of the same type. That method has a special purpose for filtering the injection points: the type of its argument and the type of the item being selected must be the same to be considered.
@ModifyConstant
causes a literal value in the code to be replaced. It has an optional constant = @Constant(…)
element instead of at = @At(…)
.
Like the CONSTANT
injection point, @Constant
accepts int intValue
, String stringValue
, etc. to specify which literal value to match. It also supports ordinal
and slice
.
@ModifyVariable
is intended to be used with the injection points LOAD
(for which it changes the value that is read from a variable) and STORE
(for which it changes the value that is written to a variable). In addition to at
it optionally accepts String name
to filter by variable identifier.
If you want to modify the arguments your target method receives before anything else happens, you can alternatively inject at the HEAD
and supply argsOnly = true
to @ModifyVariable
. If the target method accepts several arguments of the same type, @ModifyVariable
itself accepts ordinal
to specify which one to take.
@ModifyArg
changes a single argument to a method call. If there are several arguments of the same type, it optionally accepts int index
to specify which to replace.
For additional context, the annotated method can optionally receive all of the arguments of the method, rather than just the one it intends to replace.
@ModifyArgs
is capable of modifying any number of arguments to a method call. Its annotated method returns void
, but it receives an Args
object as an argument which it can use to Args.get(int index)
and Args.set(int index, T value)
the arguments it wishes to modify.
Proxying access to class members
@Shadow
If you want to access a field or method in the target class while implementing your mixin methods, and you want to avoid the (Foo)(Object)this
hack, you can declare a dummy member with the same name and type signature and annotate it with @Shadow
. When you interact with this dummy member in your mixin code, it translates to interactions with the real thing.
Re-exporting private members
If you want to offer access to private fields to your non-mixin code, you can declare an accessor mixin, which is defined as an interface
instead of an abstract class
. It should consist of the private fields you want to access annotated with @Accessor
, and the private methods you want to access annotated with @Invoker
.
Now, if some external code wants to access a private member on Foo
, they can cast it to FooAccessor
to gain access to all the members you've specified in your accessor mixin.