User Tools

Site Tools


minecraft:mod:mixin:start

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 syntax com.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 injections
  • expect Like require, 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);
    }
}
Result
public int bar(int x) {
    if (x == 0) {   // <--
        return x+1; // <--
    }               // <--
    System.out.println("Hello, world!");
    return x;
}

@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 FIELDs, 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.

minecraft/mod/mixin/start.txt · Last modified: 2024/03/31 17:58 by raccoon

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki