-
Notifications
You must be signed in to change notification settings - Fork 1
Lunar Bridge
The Bridge is an inheritance based system which uses Interfaces to allow the client to work on multiple versions with minimal modification.
The following is a heavily abstracted overview of how Lunar uses a bridge system. Lunar most likely use an Annotation Processor to do most of this for them.
Let's assume we want to make a bridge method to check if Minecraft is in fullscreen.
First we will need a Bridge interface we can cast to later on, with the method we want to inherit from the Minecraft class, in this case isFullScreen()
.
Bridge Source Code:
public interface MinecraftBridge {
boolean bridge$isFullScreen();
}
Then we can extend this Bridge interface within the Mixin to Minecraft.
Mixin Source Code:
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(Minecraft.class)
public abstract class MixinMinecraft implements MinecraftBridge {
@Shadow
public abstract boolean isFullScreen();
@Override
public boolean bridge$isFullScreen() {
return isFullScreen();
}
// set the MinecraftBridge implementation in the BridgeManager.
@Inject(method = "run", at = @At("HEAD"))
public void impl$run(CallbackInfo ci) {
BridgeManager.setMinecraftBridge(this);
}
}
You may be wondering how we can call static minecraft methods using a non-static interface, the answer is actually quite simple. We create a class which implements the bridge, and calls the static method.
Bridge Source Code:
public interface KeyboardBridge {
boolean bridge$areRepeatEventsEnabled();
}
Implementation Source Code:
import org.lwjgl.input.Keyboard;
public class KeyboardImplementation implements KeyboardBridge {
@Override
public boolean bridge$areRepeatEventsEnabled() {
return Keyboard.areRepeatEventsEnabled();
}
}
Now you may be thinking how can you reference these bridges without calling any Minecraft code?
Lunar have created a main "BridgeManager" class which contains the instances of the bridges, which is where you can reference any bridge you need.
Bridge Class Source Code:
public class BridgeManager {
private static IBridge INSTANCE;
private static MinecraftBridge MINECRAFT_BRIDGE;
private static KeyboardBridge KEYBOARD_BRIDGE;
public static void setImplementation(IBridge bridge) {
INSTANCE = bridge;
INSTANCE.enable();
}
public static void setMinecraftBridge(MinecraftBridge bridge) {
MINECRAFT_BRIDGE = bridge;
}
public static void setKeyboardBridge(KeyboardBridge bridge) {
KEYBOARD_BRIDGE = bridge;
}
public static IBridge getImplementation() {
return INSTANCE;
}
public static MinecraftBridge getMinecraftBridge() {
return MINECRAFT_BRIDGE;
}
public static KeyboardBridge getKeyboardBridge() {
return KEYBOARD_BRIDGE;
}
}
Inside of IBridge, we can see utility methods, in this example, a utility method to create a ResourceLocation
instance cast to ResourceLocationBridge
:
public interface IBridge {
void enable();
ResourceLocationBridge initResourceLocation(String resourcePath);
}
And finally here's our BridgeImplementation class:
import net.minecraft.util.ResourceLocation;
public class BridgeImplementation implements IBridge {
public void enable() {
Bridge.setKeyboardBridge(new KeyboardImplementation());
}
public ResourceLocationBridge initResourceLocation(String resourcePath) {
return (ResourceLocationBridge) new ResourceLocation(resourcePath);
}
}
This bridge implementation class is version dependent, which requires it to be rewritten for different versions of Minecraft.
Note than in the
enable()
method withinBridgeImplementation
, static bridges are declared.
Instance dependent bridges are set in their respective Mixin classes, with one exception:
Hierarchical classes that can be called from a single bridge instance e.g. BridgeManager.getMinecraft().getPlayer()
only requires the MinecraftBridge
to be set.
Lunar decided to create another class for commonly used references within the client to shorten their code.
A quick example would be shortening BridgeManager.getMinecraftBridge().bridge$getFontRenderer()
to Ref.getFontRenderer()
.
Here's the source code for the Reference Class:
public class Ref {
public static MinecraftBridge getMinecraft() {
return BridgeManager.getMinecraftBridge();
}
public static FontRendererBridge getFontRenderer() {
return getMinecraft().bridge$getFontRenderer();
}
}
In special cases, you might not be able to cast a Minecraft class to a Bridge interface within a Mixin.
Take this example:
public interface ItemStackBridge {
String bridge$getDisplayName();
}
@Mixin(ItemStack.class)
public abstract class MixinItemStack implements ItemStackBridge {
@Shadow
public abstract String getDisplayName();
public String bridge$getDisplayName() {
return getDisplayName();
}
}
If we wanted to cast a normal ItemStack to ItemStackBridge
it will give us an error telling us that ItemStack
is declared final
.
package net.minecraft.item;
import ...
public final class ItemStack {
// ^ Declared final.
...
}
To avoid this issue, you can use what's called an AccessTransformer
which is built into ForgeGradle.
Here's how you'd remove the final
flag from the ItemStack class using an AccessTransformer config:
public-f net.minecraft.item.ItemStack
The -f
will remove the final
flag from the class. All that's left to do is re-sync the gradle project and let ForgeGradle apply the transformation.
Another, simpler approach would be casting the instance of ItemStack
to an Object
, and then casting to ItemStackBridge
.
Thanks, LlamaLad7.
public ItemStackBridge bridge$getItem() {
return (ItemStackBridge)((Object) getItem());
}