-
Notifications
You must be signed in to change notification settings - Fork 2
Schema
I need to start from afar to describe the schema concept. Let's start with an example of methods. Look at this string:
"Lnet/minecraft/world/entity/player/Player;crit(Lnet/minecraft/world/entity/Entity;)V"This is a long, magical string that's required for the Mixin but completely unreadable and would be an anti-pattern in modern Kotlin development. My goal was to come up with some kind of entity that would contain information from which this string could be deduced during compilation, but at the same time, make it so that the modder would never have to write it manually. The most elegant solution for this in Kotlin is callable references:
val crit = Player::critIf the method is overloaded, then we need to clarify which one we mean. For this purpose, Kotlin provides us function types:
val crit: Player.(victim: Entity) -> Unit = Player::critBy using both features together on a consistent basis, we force the compiler to check that the crit is actually non-static method, exists in the Player class, takes single parameter of type Entity, and returns void. Naming function type parameters is optional, but it's a good idea to have a "cheat sheet" for future (what if later you forget that the entity mean?). Also, parameter names are not checked for matching, so you can change the name from your mappings to something you like better.
Before we understand how we can use these references, let's consider the scale of the problem of their absence.
In standard mixins, you often need to write the same thing in several places. For example, first you want to inject into the head of the method:
@Inject(
method = "Lnet/minecraft/world/entity/player/Player;crit(Lnet/minecraft/world/entity/Entity;)V",
at = @At(value = "HEAD")
)
private void doStuff(Entity entity, CallbackInfo ci) {}Then you will want to change the argument when calling it from another method:
@WrapOperation(
method = "Lnet/minecraft/world/entity/player/Player;attackVisualEffects(Lnet/minecraft/world/entity/Entity;ZZZZF)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;crit(Lnet/minecraft/world/entity/Entity;)V")
)
private void doStuffAgain(Player instance, Entity entity, Operation<Void> original) {}Then you want to call this method and use @Shadow:
@Shadow
public abstract void crit(Entity entity);And finally, if you need to call this method from outside the mixin and it is private, then you create an accessor Mixin with @Invoker:
@Mixin(Player.class)
interface PlayerAccessor {
@Invoker("crit")
void crit(Entity entity);
}Or you use AW for it:
accessible method net/minecraft/world/entity/player/Player crit (Lnet/minecraft/world/entity/Entity;)V
As a result, we are forced to write the same thing in different places: JVM descriptor strings in annotations and AW, and Java code in injection parameters, arguments when calling the original, and in the @Shadow and @Invoker methods. But the crit method is the same. Can you imagine how much routine work will need to be done if this method changes in the next version of the Minecraft? It's a nightmare!