Mixin Guide
What's a mixin? A mixin is simply a class that extends a class from the client's source code and overrides or adds to the functionality of the original class. RuneLite's mixin architecture essentially acts as a surgical toolkit for the OSRS client. Because the client is heavily obfuscated and changing constantly, RuneLite uses bytecode injection to "stitch" its own code into the game engine at runtime.
Here's a short example of a mixin which extends the RSPlayer class and adds a new method to it.
@Mixin(RSPlayer.class)
public abstract class RSPlayerMixin implements RSPlayer {
@Inject
private int oldHeadIcon = -2;
@Shadow("client")
private static RSClient client;
@Inject
@MethodHook(value = "read", end = true)
void postRead(RSBuffer var1);
@Inject
@Override
public int getSkullIcon() {
return this.getRsSkullIcon();
}
}Decoding the Mixin Annotations
To understand the RSPlayerMixin class, we need to look at the tools (annotations) it uses to manipulate the game's code.
The Target: @Mixin
This tells the RuneLite injector, "Hey, take everything inside this class and merge it directly into the game's underlying Player class." Because the game's real class names are obfuscated, RuneLite maps them to readable interfaces like RSPlayer.
@Mixin(RSPlayer.class)
public abstract class RSPlayerMixin implements RSPlayer {}Adding New Features: @Inject
The @Inject annotation does exactly what it sounds like: it forces brand-new fields or methods into the original game class. For variables, it injects oldHeadIcon to keep track of state, so RuneLite knows when to fire a notification that a players skull icon has changed. For methods, it injects helper methods (like getSkullIcon) to make the obfuscated game code easier for RuneLite plugins to read and interact with.
@Inject
private int oldHeadIcon = -2;
@Inject
@Override
public int getSkullIcon() {
return this.getRsSkullIcon();
}Existing Data @Shadow
Sometimes the injected code needs to reference variables that already exist in the game engine. @Shadow can act as a window to those variables. It tells the compiler, "Don't actually create this variable. Just link it to the existing client variable in the game code so I can use it here."
@Shadow("client")
private static RSClient client;Events @FieldHook & @MethodHook
RuneLite is heavily event-driven as such Hooks are how RuneLite intercepts game actions to trigger those events.
@Inject
@FieldHook("headIconPrayer")
public void prayerChanged(int idx) {}This injects a callback right after a specific variable (in this case, headIconPrayer) is updated in the game. It checks if the new icon is different from the oldHeadIcon, and if so, it broadcasts an event on the EventBus all subscribing RuneLite plugins.
The same can happen with methods:
@Inject
@MethodHook(value = "read", end = true)
void postRead(RSBuffer var1)This drops a piece of code directly into an existing game method. By setting end = true, this code runs at the very end of the game's read method, triggering a PlayerChanged event every time the game finishes reading a player's data bundle.
Wrapping Up
If you couldn't already tell, mixins provide a powerful way for RuneLite to extend the existing game client's functionality in a clean abstraction layer which is agnostic of the client's obfuscated code. This quote from SkylerMiner back in 2023 really helps to sum up mixins in a nutshell.
Mixin files in RuneLite serve as connections to the game client, allowing developers to access specific methods and data. These files are kept private and obfuscated, meaning their inner workings are hidden. When RuneLite is initialized, these mixin files are loaded to inject or retrieve game information in a logical way. To understand mixins, let's consider a couple of examples. Imagine we want to get data about the currently selected spell widget in the game. In this case, we use a hook to directly access the needed information from the game client. On the other hand, if we need to determine the animation ID of a character, we may require additional information from different fields. This is where a mixin comes in, helping us access those specific fields to calculate the animation ID correctly. Finding the obfuscated method names in the patched RuneLite client involves a simple process. For instance, if we're looking for the obfuscated name of the method "getAnimationID," we can obtain an instance of the relevant class, like an Actor. By calling a method on that instance and logging the result, we can identify the obfuscated name we're looking for. Typically, obfuscated method names have an extra parameter compared to their original unobfuscated versions. This additional parameter is usually a throwaway value, such as an integer. By observing the number of parameters and checking for any additional ones, we can identify the obfuscated method. Mixin files and hooks enable developers to enhance the functionality of the RuneLite client. They allow access to specific game information and support more complex operations. In cases where RuneLite doesn't provide built-in hooks for certain gamepack methods, developers can utilize mixins and hooks to interact with them and achieve their desired goals. This flexibility empowers developers to customize and improve the RuneLite client to suit their needs. "
What's Next?
- Check out the Network Packets Guide for information on how the client transmits its data to the server.
- Read up on the Packet Mapping process I took