Mixins

Mixins aren’t supported by Forge out of the box, but you can enable them by updating your build.gradle to include the needed libraries. I found some tutorials online, but each one left out key details to get the code running. So I’ve decided to create a new checklist that will hopefully help other modders in the future.

Checklist


  1. Update build.gradle
  2. Regenerate Runs
  3. Create/Update mixins.json
  4. Implement Mixins

Update build.gradle


Your build.gradle will need to be updated to include spongepowered which is what Forge uses to allow mixins. You will need to add five new sections to the file. The first is to include the Maven repository so that Gradle knows where to look for it:

buildscript {
    repositories {
        // These repositories are only for Gradle plugins, put any other repositories in the repository block further below
        maven { url = 'https://repo.spongepowered.org/repository/maven-public/' }
        mavenCentral()
    }
    dependencies {
        classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
    }
}

...

repositories {
    maven {
        url = 'https://repo.spongepowered.org/repository/maven-public/'
        content { includeGroup "org.spongepowered" }
    }
}

You also need to include the actual plugin that adds the features to Forge:

apply plugin: 'org.spongepowered.mixin'

You need to add the dependencies for the plugin:

dependencies {
    annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'
}

And then you need to add a new mixin section that defines the JSON files you want to use. If you are using the mod_id variable as I have here, make sure that you use double quotes (“) and not single quotes (‘) as the latter doesn’t support variables.

mixin {
    add sourceSets.main, "mixins.${mod_id}.refmap.json"
    config "mixins.${mod_id}.json"
}

Seeing it laid out like this can be a bit confusing, so I suggest you look at the actual commit in my GitHub repo so you can see how it all fits together.

Regenerate Runs


This is a really important step that you mustn’t skip. After any change to your build.gradle you should always reload Gradle’s dependencies and run the genEclipseRuns/genIntelliJRuns/getVSCodeRuns task so that the new dependency is included in the project. The exact steps to do this will depend on your choice of IDE, but the most important thing is:

DO NOT SKIP THIS STEP.

Create/Update mixins.json


This file tells spongepowered what mixins your mod is using. The exact name of the file depends on your Mod ID and should match what you specifed next to config in your build.gradle; mine is called mixins.butterflies.json for example.

The bare bones implementation of this file should look as follows:

{
  "required": true,
  "minVersion": "0.8",
  "package": "com.bokmcdok.butterflies.mixin",
  "compatibilityLevel": "JAVA_17",
  "refmap": "mixins.butterflies.refmap.json",
  "mixins": [
    "ClimbRopeMixin"
  ],
  "client": [
  ],
  "injectors": {
    "defaultRequire": 1
  }
}

The package indicates which package in your project your mixins will live inside of. The refmap also needs to match what is in your mixin section of your build.gradle. This file is auto generated on build so you don’t need create that one yourself.

If you add any new mixins later you will need to come back to this file and add them to the relevant array, mixins by default, client for client-only mixins, and server for server-only mixins.

Implement Mixins


Now we can just write some code! However mixins require a few extra annotations in order to work properly. Firstly, you need to use the @Mixin annotation to specify which class you want to inject code into. In my case I want to inject into ForgeHooks so I define my class like so:

@Mixin(value = ForgeHooks.class, priority = 888)
public class ClimbRopeMixin {
}

To actually inject code I you need to use the @Inject annotation on a method. The method parameters should be exactly the same as the original method, with the addition of a CallbackInfoReturnable that can be used to override the return value:

    @Inject(remap = false, method = "isLivingOnLadder", at = @At("HEAD"), cancellable = true)
    private static void isLivingOnLadder(@NotNull BlockState state,
                                         @NotNull Level level,
                                         @NotNull BlockPos pos,
                                         @NotNull LivingEntity entity,
                                         CallbackInfoReturnable<Optional<BlockPos>> returnable) {

        if (entity instanceof Player player &&
                player.isSpectator()) {
            returnable.setReturnValue(Optional.empty());
            return;
        }

        if (butterflies$findClimbableRope(entity)) {
            returnable.setReturnValue(Optional.of(pos));
        }
    }

When you implement methods be sure to include remap=false. None of the tutorials I found included this, and my code didn’t work until I found this in other people’s code in GitHub and added it myself.

The @At annotation describes where the code should be injected. I’ve specified HEAD which means the start of the method, but there are other places you can inject code as well:

  • HEAD: The start of the method.
  • RETURN: A specific return statement.
  • TAIL: The last return statement.
  • INVOKE: After a specific number of invocations, but before the method runs.
  • INVOKE_ASSIGN: After a specific number of invocations, but after the method runs.
  • FIELD: When a specific field is accessed.
  • NEW: When a new object is created.
  • INVOKE_STRING: Same as INVOKE, except only for functions that accept a String and return void.
  • JUMP: A point where the code branches (e.g. if statements, switches, etc.).
  • CONSTANT: At a point where a constant is defined within the method.

The last thing to note is the use of setReturnValue to return a value from the method. Here I use it to override the normal return value if an entity is close enough to a rope to climb it. You can see the full code for my first ever mixin on my GitHib repo if