Skip to content
diskria edited this page Apr 22, 2026 · 23 revisions

To explain the concept of Schema, I need to take a step back and look at the bigger picture. To illustrate this, consider the following method signature:

"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::crit

If 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::crit

By using both features together, we force the compiler to check that the crit is actually non-static method that exists in the Player class, takes single parameter of type Entity, and returns void. In essence, we've replaced a fragile raw string with a strongly typed reference. Naming function type parameters is optional, but it's a good idea for self-documentation (what if you forget what that Entity actually represents later?). 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.

Problem

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 directly in mixin 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 separate class with @Invoker:

@Mixin(Player.class)
interface PlayerAccessor {
    @Invoker("crit")
    void crit(Entity entity);
}

Or you use AW for it (text file outside of code):

accessible method net/minecraft/world/entity/player/Player crit (Lnet/minecraft/world/entity/Entity;)V

This triggers a "redundancy cycle" where you manually duplicate the signature across injection parameters, @At targets, original.call arguments, @Shadow and @Invoker methods, and external files—effectively turning modding into error-prone manual data entry. Maintaining this is highly inefficient: a single change to the crit method signature in a future Minecraft update would require manual updates in multiple locations, making the codebase fragile and difficult to refactor.

Solution

Callable references serve as a foundation for a Single Source of Truth (SSoT), eliminating logic duplication by allowing a single definition to be referenced throughout the codebase. This approach ensures that any changes to the logic are automatically propagated across the entire mod, including its DSL and internal systems.

Descriptors

Since annotations do not support callable references as parameters, they are encapsulated within classes to leverage supported class references instead. By wrapping a callable reference within a dedicated class, we can pass that class to an annotation, which natively supports it.

abstract class Method<F : Function<*>>(callable: F)
abstract class Constructor<F : Function<*>>(callable: F)
abstract class Field<T>(callable: KProperty<T>)

Now, reference to the crit method will look like this:

object crit : Lapis.Method<Player.(Entity)-> Unit>(Player::crit)

For constructors, it will look similar:

object newInstance : Lapis.Constructor<(Identifier, Optional<Float>) -> SoundEvent>(::SoundEvent)

For fields, specify the type directly:

object noPhysics : Lapis.Field<Boolean>(Entity::noPhysics)

Schema

To keep descriptors organized and accessible, they should be grouped within a dedicated class marked with the @Schema annotation. This provides a centralized structure for the entire mod's logic.

@Schema(Player::class)
object _Player {
    // descriptors here
}

Example

Clone this wiki locally