Write Version Dependent Code
While our API is entirely version-independent for you to use, you might still need to implement some version-dependent things yourself (such as niche things, just something we didn't implement). At the current moment, you might need to implement more yourself, as we're still implementing more and more stuff for you to use version independently.
There are two ways to implement version-dependent; different use cases require different ways to implement them.
The easiest method is to create an interface or abstract class and implement it in your desired version(s). This will result in the ability that you can control your version-dependent code from the core.
The more complex way would be SpongePowered's Mixin. While Mixin is comparatively easy to use, it can be a bit confusing at the beginning, as all new things are.
Important Note
Please keep in mind that the examples on this page are version dependent. We are showing the examples for Minecraft 1.21.5, so depending on the date you're reading this, recreating the examples might not be possible, we'll try to keep them updated, though, and if we do, we update the version in this note.
Access the Minecraft Code "the Normal Way"
You should only use this method for things you can access without using Reflections, as using Reflections can have a high impact on the performance of the players that use your addon.
The use case we will implement in the following example will show how to display messages in the player's chat. We have already implemented this, but it is relatively easy to show and understand.
First, create a new interface (an abstract class would also work) in the core
or api
module and name it ExampleChatExecutor
. Annotate it with @Referenceable
, and define a method:
void displayMessageInChat(String message);
Next, navigate to the game-runner
module. Locate the version you want to implement this functionality for, and create a new class named VersionedExampleChatExecutor
in the appropriate package.
Now to the implementation. First, we implement the interface ExampleChatExecutor
, and now the most important part: we
need to add the annotation Implement
to the class and declare ExampleChatExecutor.class
as the argument. This will
allow you to access the versioned implementation from the core module. Then we'll add the annotation Singleton
to the
class, as we don't need more than one object of this implementation. At this point, the only missing point is the actual
implementation of Minecraft; we'll overwrite the displayMessageInChat
method from our interface and create a new
Component from our String with Component.literal(message)
. Then we access the Chat GUI
with Minecraft.getInstance().gui.getChat()
and add our component with addMessage(component)
.
To access our VersionedExampleChatExecutor
from the core module, we need to add a private instance of the ExampleChatExecutor
with a getter to our ExampleAddon
class. Then we assign the instance in our ExampleAddon#enable
method using ((DefaultReferenceStorage) this.referenceStorageAccessor).exampleChatExecutor()
. If the function does not exist, we need to run a gradle build to add the class to our reference storage. We can now access the instance from the core module with ExampleAddon#chatExecutor()
. Now we can require an ExampleAddon
instance as a parameter in our ExamplePingCommand
and pass this
to the constructor of the command in the ExampleAddon#enable
. Then we can access the VersionedExampleChatExecutor
instance from the command and call the displayMessageInChat
method with "Pong!"
as an argument.
After starting LabyMod 4, joining a server, and executing "/pong", we'll see a colorless "Pong!". Now, if we want that
message colored, we need to replace String
in our interface with Component
. Instead of using Command.literal
, we
get the ComponentMapper
via Laby.labyApi().minecraft().componentMapper()
and call toMinecraftComponent(component)
.
Now our component from the Adventure Component library was mapped to a Minecraft Component, and
calling displayMessageInChat(Component.text("Pong!", NamedTextColor.GOLD))
in our Command works like before. Just with
our own implementation.
Those are the results from this example:
@Referenceable
public interface ExampleChatExecutor {
void displayMessageInChat(String message);
void displayMessageInChat(Component labyComponent);
}
@Singleton
@Implements(ExampleChatExecutor.class)
public class VersionedExampleChatExecutor implements ExampleChatExecutor {
@Override
public void displayMessageInChat(String message) {
Component component = Component.literal(message);
this.addMessageToChat(component);
}
@Override
public void displayMessageInChat(net.labymod.api.client.component.Component labyComponent) {
ComponentMapper componentMapper = Laby.labyAPI().minecraft().componentMapper();
Component component = (Component) componentMapper.toMinecraftComponent(labyComponent);
Minecraft.getInstance().gui.getChat().addMessage(component);
}
private void addMessageToChat(Component component) {
Minecraft.getInstance().gui.getChat().addMessage(component);
}
}
public class ExampleAddon extends LabyAddon<ExampleConfiguration> {
private ExampleChatExecutor chatExecutor;
@Override
protected void enable() {
# Make sure to build your addon first (after creating the interface and it's implementations) and to import your own generated ReferenceStorage class
this.chatExecutor = ((ReferenceStorage) this.referenceStorageAccessor()).exampleChatExecutor();
this.registerCommand(new ExamplePingCommand(this));
}
@Override
protected Class<? extends ExampleConfiguration> configurationClass() {
return ExampleConfiguration.class;
}
public ExampleChatExecutor chatExecutor() {
return this.chatExecutor;
}
public class ExamplePingCommand extends Command {
private final ExampleChatExecutor chatExecutor;
public ExamplePingCommand(ExampleAddon addon) {
super("ping", "pong");
this.chatExecutor = addon.chatExecutor();
}
@Override
public boolean execute(String prefix, String[] arguments) {
if (prefix.equalsIgnoreCase("ping")) {
this.displayMessage(Component.text("Ping!", NamedTextColor.AQUA));
return false;
}
this.chatExecutor.displayMessageInChat(Component.text("Pong!", NamedTextColor.GOLD));
return true;
}
}
Access the Minecraft Code via Mixin
Important Note
The moment your addon uses Mixins, it requires a full game restart when downloaded via the addon store.
Danger
For mixins to work outside of the development environment, your rootProject.name
in settings.gradle.kts
must exactly match your addon namespace.
Mixins allow you to modify Minecraft code directly. Similar to reference storage, they are version-dependent, which means you must implement them separately for each Minecraft version you want to support.
When creating a mixin, the location is critical. Navigate to the game-runner
module of your addon and create a package named mixins
inside the automatically generated versioned package. Your structure should look something like this:
org/example/{version}/mixins
You may create mixins in this package or in subpackages. You do not need to register them manually in a configuration file - LabyMod will automatically detect and apply them. LabyMod also supports MixinExtras annotations, which can simplify common injection patterns.
For further reading:
This section does not attempt to explain all of Mixin in detail - it focuses only on how to correctly load and use them with LabyMod.
Example: Logging Every Tick
@Mixin(Minecraft.class)
public class MixinMinecraft {
@Unique
private int example$tickCount = 0;
@Inject(method = "tick", at = @At("HEAD"))
private void onTick(CallbackInfo ci) {
System.out.println("[Mixin] Tick log " + this.example$tickCount++);
}
}