Skip to content

Introduction to Mixins The Mixin Environment

Mumfrey edited this page Sep 21, 2021 · 4 revisions

So now that we understand the basic features of mixins (hint: if you haven't read that, go back and read it first!), let's take a quick detour to understand how to leverage our newly-authored mixins into the game environment.

How mixins do their thing

As I mentioned in the previous article, it's best to keep in mind that to most intents and purposes mixins are not classes in the strictest sense and are not classloaded at runtime, but rather the raw bytecode is parsed using ASM's Tree API. This approach generates a node-based view of the underlying bytecode which can then be merged into the target classes by the mixin transformer.

Mixins themselves are pumped through the transformer chain independently of regular classes, this is to allow any remapping transformers to do their work prior to the mixin being combined with its target class. It is important that the mixin transformer itself is downstream of any remapping transformers for this reason, this is handled internally by inserting proxy transfomers in the transformer chain.

Mixin Transformer Chain

Mixins require processing before class loading begins in order to identify their target classes, perform hierarchy validation, and resolve any updated static bindings for fields and methods. Mixins you wish to apply should be specified in a configuration file and your coremod, tweaker, jar metadata or litemod.json will specify the location of the resource inside your mod's jar.

The configuration path is loaded as a resource and thus can be placed any package within your jar. It is important that you do not prepend a leading slash onto the file path as resource paths must be relative.

For example, if your config file is in the package com.somepackage you should specify the path to your config file as com/somepackage/myconfig.json

Each configuration defines a mixin set and you may have as many mixin sets as you require for your application. It is only required to separate mixins into sets when your mixins target different environments (see below). However it is sometimes desirable to also split mixins into sets for organisational purposes.

Mixin configuration files

As well as defining the mixin set itself, mixin configuration files also define accompanying properties for the set.

Firstly, the set itself is defined with four keys:

  • package defines the parent package for this group of mixins (this is important because the package and all subpackages will be excluded from the LaunchClassLoader at run time.
  • mixins defines the list of mixin "classes" within the parent package to apply for this configuration, each mixin "class" is specified relative to the parent package, sub-packages are allowed. Entries in this list are applied to both client and dedicated server side.
  • client defines the list of mixins to apply only on the client side.
  • server defines the list of mixins to apply only on the dedicated server side.

Additional properties can then be defined:

  • refmap defines the name of the reference map filename for this set (more on this in the discussion of how obfuscation is handled with mixins)
  • priority defines the priority of this mixin set relative to other configurations.
  • plugin is the name of an optional companion plugin class for the mixin configuration which can tweak the mixin configuration programmatically at run time. See Mixin Companion Plugins (Not done yet).
  • required defines whether the mixin set is required or not. If a single mixin failing to apply should be considered a failure state for the entire game, then the required flag should be set to true.
  • minVersion should be set if this mixin set relies on some mixin functionality which was added in a particular version. It can be omitted for version-agnostic mixin sets.
  • setSourceFile causes the mixin processor to overwrite the source file property in target classes with the source file of the mixin class. This can be useful when debugging mixins.
  • verbose promotes all DEBUG-level log messages to INFO level for this mixin set. This can also be enabled globally via the mixin.debug.verbose system property

Most important to note is that mixin "classes" must exist within a package with no other classes in your project (this includes sub-packages of the main mixin package) because the entire package will be excluded from the transforming classloader on startup.

The Game Lifecycle and You

To understand how mixins interact with the underlying game, it is crucial to first understand the game's lifecycle when running within LaunchWrapper.

LaunchWrapper usurps the game's normal startup process in order to allow Tweakers to modify the game. Each Tweaker can supply Class Transformers which are able to modify game classes at load time. Tweakers are specified on the command line and are loaded and initialised one by one by the Launch class, which forms the core of LaunchWrapper's startup logic.

Tweakers such as FML and LiteLoader are also able to co-operatively load and inject other Tweakers that they discover, and so Launch initialises Tweakers in a loop, continuing until no further additional Tweakers are offered. We can visualise the execution flow within Launch like this:

Launch lifecycle

It is absolutely vital to realise that this phase of initialisation must complete before the game begins to load, since otherwise a Transformer registered late in the Tweak initialisation cycle may not have the opportunity to process classes that it needs to, and the transformer chain may not be complete.

Things which happen at this phase of initialisation are:

  • Tweakers such as FML and LiteLoader perform mod discovery, and inject their transformers and any additional Tweakers they find.
  • FML "Core mods" (aka Loading Plugins) are initialised and can register their own transformers.

Since the mixin subsystem must be initialised at this early stage (before game classes load), it must be loaded by a coremod or Tweaker, see the sections on bootstrapping below.

Launch lifecycle

Once Tweaker initialisation is complete, Launch calls the game's original main() method which starts the game loading process. At this point the transformer chain is complete and so game classes can be loaded and processed by the registered transformers. The game enters its main loop and classes are loaded on-demand and processed by the transformer chain (including Mixin).

It should be clear from the above that the game execution is thus split into two distinct phases, namely the pre-init phase where the Tweakers are initialised, and the default phase which is analogous to the normal game lifecycle when not running inside the wrapper. It is necessary to understand these phases and their relationship with the Mixin subsystem.

It's just a phase I'm going through

In the normal course of things, our Mixin setup would only deal with classes loaded in the default phase and life would be simple. During this phase we know that:

  • The transformer chain is complete
  • Game classes can be safely loaded

This means that it's safe to load and transform game-class bytecode and apply our mixins, it is also safe to load the class metadata that we require in order to apply those mixins (remember from above that validation and pre-transformation of mixins happens in a single pass at startup). If we perform metadata generation too early (during the pre-init phase for example) then there is a chance that critical transformers may not have been registered! This will result in the mixin bytecode being incomplete or invalid.

So why bother with pre-init at all?

The simple answer is: "so we can mix in to core classes provided by other tweakers, specifically FML"

One of the requirements of a platform like Sponge is that it is sometimes necessary to hook into the underlying platform in ways which simply wouldn't be possible if transformation occurred during the default phase when core classes have already been loaded (and thus are beyond the reach of transformers). However as we have already established, loading our game-relevant mixins during the pre-init phase will not work because transformers which we require will not be present.

Care for the Environment

In the Mixin processor then, we acknowledge this divide by splitting processing down into Environments, one for each phase, it is then possible to split up your mixin sets to target the desired Environment. Typical scenarios for mixins would thus resolve into

  • A mixin set which only mixes game classes should be registered in the default environment
  • Where mixins need to be applied in both phases, two mixin sets (configurations) should be specified with pre-init mixins in one set and default mixins in another.

Jumping the Gun

One thing worth noting is that - as a Tweaker itself - FML co-operatively loads its coremods and re-injects them into the startup lifecycle using proxy Tweakers. Because our sensitivity to the startup order can be somewhat delicate, it is worth making note of a fine distinction between "first class" tweaker initialisation, and the indirection which occurs during coremod initialisation.

The Mixin library includes a first-class tweaker which is designed to register and begin processing at the earliest possible moment, this is marked by (1) in the diagram below. Normal coremod startup occurs during the first pass through the loop at (2) and game loading begins at (3).

Launch lifecycle

If mixing in to classes during pre-init is required, then usage of the first-class tweaker is recommended.

Mixins on Minecraft Forge

For information on getting a Minecraft Forge project set up to use Mixin in conjunction with the MixinGradle plugin see the main chapter Mixins on Minecraft Forge.

Mixins on other platforms

For mixin use on other platforms please consult the platform documentation.