Mixin allows developers to add new fields and methods to a target class, this can be useful for operations internal to the Mixin class, but you may want to expose your added members to the rest of your project. In order to do this, you can use “duck” interfaces. That is to say a non-Mixin interface implemented into the target class via a Mixin, which adds methods that expose added members to the rest of a codebase.
For our example situation, we'll use 1.21.10 with Mojang Mappings. We will add and expose fields to the Zombie class for a hypothetical combo mechanic we'd want to be able to interact with outside of the Mixin.
@Mixin(Zombie.class) abstract class ZombieExampleMixin { @Unique private int modid$comboCount; @Inject(method = "<init>*", at = @At("TAIL")) private void populateField() { modid$comboCount = 0; } @Inject(method = "tick", at = @At("TAIL")) private void doSomethingWithCombos(CallbackInfo ci) { /* ... */ } }
This field is normally entirely inaccessible, and we can't just make our Mixin class public and get the field that way, because our Mixin class itself won't exist at runtime. However, if we use an interface that will exist at runtime, and implement it into the target class via our Mixin, we'll be able to use the fields via the interface.
We use
@Unique to ensure that we do not clash with any other mods' added fields in the same class, and prefix the field with our mod id in order to also make it easier to find which mod is involved in issues pertaining to added fields in logs.
A common naming convention for “duck” interfaces is TargetClassAccess, we'll use it here:
public interface ZombieAccess { int modid$getComboHits(); void modid$setComboHits(int newCount); }
This effectively acts as an exposed getter and setter for our example's sake, but any number of methods can be added this way. Since the methods will be added to the target class by our implementation, we should also add modid$ here to avoid clashing with other mods.
@Mixin(Zombie.class) abstract class ZombieExampleMixin implements ZombieAccess { @Unique private int modid$comboCount; @Override public int modid$getComboHits() { return modid$comboCount; } @Override public void modid$setComboHits(int newCount) { modid$comboCount = newCount; } @Inject(method = "<init>*", at = @At("TAIL")) private void populateField() { modid$comboCount = 0; } @Inject(method = "tick", at = @At("TAIL")) private void doSomethingWithCombos(CallbackInfo ci) { /* ... */ } }
The compiler will allow us to cast instances of the Zombie class to our interface without causing issues, and since the interface will be implemented by Mixin at runtime, it won't cause issues at runtime either:
public class ExampleClass { public static void holder(Zombie zombie) { int currentCombo = ((ZombieAccess) zombie).modid$getComboHits(); ((ZombieAccess) zombie).modid$setComboHits(currentCombo + 1); } }