diff options
author | Linnea Gräf <nea@nea.moe> | 2024-01-26 22:24:25 +0100 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2024-01-26 22:24:25 +0100 |
commit | 90f002c060847e574b2fb3d81a04d2b695ddcdad (patch) | |
tree | 3c3b11e99d054efcd49820472b60f0226321f13c | |
parent | e2e2e26939c9f458fc345c337c79a5376900a7b6 (diff) | |
download | SkyBlockModWiki-moddev.tar.gz SkyBlockModWiki-moddev.tar.bz2 SkyBlockModWiki-moddev.zip |
Add mixin tutorialmoddev
-rw-r--r-- | docs/development/mixins.md | 79 | ||||
-rw-r--r-- | docs/development/mixins/accessors.md | 45 | ||||
-rw-r--r-- | docs/development/mixins/adding-fields.md | 50 | ||||
-rw-r--r-- | docs/development/mixins/simple-injects.md | 133 | ||||
-rw-r--r-- | mkdocs.yml | 5 |
5 files changed, 312 insertions, 0 deletions
diff --git a/docs/development/mixins.md b/docs/development/mixins.md new file mode 100644 index 0000000..6365820 --- /dev/null +++ b/docs/development/mixins.md @@ -0,0 +1,79 @@ +# Mixins + +Mixins allow you to change Minecraft code. This is massively powerful, but you need to be very careful when using them, especially when considering to work together with other mods. + +!!! info + The [MinecraftDev](https://mcdev.io/) plugin is pretty much non negotiable when coding Mixins. It enables auto completion, shows errors when your mixins are wrong in your IDE and allows you to directly navigate to the code you are changing. + It also has some other functions that allow for easier Minecraft development, but most of the functionality is aimed at higher versions. + +> Please forgive the the nonsensical examples. I try to make the examples as simple as possible. [Check](https://github.com/NotEnoughUpdates/NotEnoughUpdates/tree/master/src/main/java/io/github/moulberry/notenoughupdates/mixins) [out](https://github.com/hannibal002/SkyHanni/tree/beta/src/main/java/at/hannibal2/skyhanni/mixins/transformers) [some](https://github.com/Skytils/SkytilsMod/tree/1.x/src/main/java/gg/skytils/skytilsmod/mixins/transformers) [open](https://github.com/inglettronald/DulkirMod/tree/master/src/main/java/dulkirmod/mixins) source mods to check out some real world mixins. + +## Layout + +Mixins need to be in their own package. You should have a dedicated mixin package in the template already. You can have multiple subpackages, but your normal code and your Mixin code need to be separate. This is because Mixins are instructions for how to change the program, rather than actual program code itself. Mixins also need to be registered in your `mixin.example.json`. In there you only need to put the class name, not including the mixin package. Mixins also need to be written in Java, not in Kotlin. + +```json +{ + "package": "${mixinGroup}", + "refmap": "mixins.${modid}.refmap.json", + "minVersion": "0.7", + "compatibilityLevel": "JAVA_8", + "mixins": [ + "MixinGuiMainMenu", + "subpackage.MixinSomeOtherClass" + ] +} +``` + +You can also have multiple mixins for the same Minecraft class. + +## Mixin Use Cases + +I recommend you start learning with accessor mixins, since those are the easiest, and go down the list from there. + + - [Accessors](./mixins/accessors.md) + - [Adding Fields and Methods](./mixins/adding-fields.md) + - [Simple Injects](./mixins/simple-injects.md) + +## Compatibility + +### Modid postfix + +In order for your mod to be compatible with other mods it is *highly* recommend (if not borderline mandatory) to prefix or postfix all of your methods with your modid: + +```java +public void someMixinMethod_mymodid() {} +// or +public void someMixinMethod$mymodid() {} +``` + +There are some exceptions for `@Inject`s, but in general it doesn't hurt to just add the postfix. + +### Non destructive mixins + +When mixing into a class you would generally want that if another mod the exact same mixin, that both of your mixins would work. Especially if your mixin only works sometimes. + +I.e. if you want a mixin to color mobs, and your mod decides not to color a mob, another mod should be able to use the exact same mixin (just in their mod) to color those mobs. + +There are some general ground rules for achieving this behaviour: + + - Only use `cir.setReturnValue()` or `ci.cancel()` if your mod decides to act on something. The default action should be to pass through to the next mixin or vanilla by doing nothing. + - Don't use `@Redirect`. Only one mixin can ever use a `@Redirect` on the same call. Only one redirect will ever work, even if your mod does nothing different with a given method call. + - Don't use `@Overwrite` (and don't overwrite without the annotation either, lol). Only one overwrite will ever work, even if your mod does nothing different with a given method call. + +Of course you will have to break those rules from time to time. But before you do, think twice if you need to. And if you do, maybe consider exposing some sort of API for other mods to hook into your code? + +## Troubleshooting + +The first step in troubleshooting mixins is to enable `-Dmixin.debug=true` in your run configurations jvm arguments. This will print out all the Mixins as they are applied and show you exactly what is wrong with each mixin, and why it wasn't applied. + +Another common issue is to forget to register a mixin in the `mixins.modid.json` + +You can also get exceptions when trying to load a mixin class directly. Accessing any mixin class except for an accessor from non mixin code will crash your game. If you want to call a method inside a mixin, have that mixin implement an interface instead. + + + + + + + diff --git a/docs/development/mixins/accessors.md b/docs/development/mixins/accessors.md new file mode 100644 index 0000000..f9ad7b8 --- /dev/null +++ b/docs/development/mixins/accessors.md @@ -0,0 +1,45 @@ +# Accessor Mixins + +> a/k/a Reverse Patch + +Let's start with the easiest form of Mixins — Accessors. Accessors allow you to access functions that are otherwise private in a Minecraft class. You can also do that using reflection, but you might notice that your reflection call will not easily work in both devenv and a live env. This is because the method names are different in a devenv compared to a normal Forge installation. You can still specify both names and just look through all the names using reflection, but Accessor Mixins are a lot easier to use, with less accidental pitfalls, and better performance. + +```java +// This Mixin targets the Minecraft class +@Mixin(Minecraft.class) +public interface AccessorMinecraft { + + // Getter for the field theIntegratedServer + // Notice the _mymodid at the end. + @Accessor("theIntegratedServer") + IntegratedServer getIntegratedServe_mymodid(); + + // Setter for serverPort + @Accessor("serverPort") + void setServerPort_mymodid(int port); + + // Invoker for rightClickMouse. + @Invoker("rightClickMouse") + void rightClickMouse_mymodid(); +} +``` + +First, notice that we need to use an `interface`. Most mixins are `class`es. Accessors are the exception, since we don't want to actually put any code into the `Minecraft` class. Accessor mixins can also not be mixed with other mixin styles, but since you should have multiple mixins even for the same class for different things anyway, this shouldn't be an issue. + +Next we put the `@Mixin` annotation on our Accessor to let it known which class we want to inject into. + +Then for a field we use the `@Accessor` annotation, with the name of the field we want to access. Please give all your mixin methods a `_mymodid` indicator, to avoid name collissions with other mods. + +For a setter, the method returns void and takes one argument of the type of the field you are targetting. For a getter, the method returns the type of the field you are targeting and takes no arguments. + +For an method invoker, you copy the method signature you want to call, but rename it to something unique with a `_mymodid` postfix. + +Now if you want to use those methods, you can simply cast any instance of your targeted class to your accessor mixin class: + +```java +Minecraft mc = Minecraft.getMinecraft(); +AccessorMinecraft accessorMc = (AccessorMinecraft) mc; +accessorMc.rightClickMouse_mymodid(); +``` + +If you get a class cast exception here, it means your mixin was not applied. This can happen if you forgot to register your mixin, or if your mixin contains errors. diff --git a/docs/development/mixins/adding-fields.md b/docs/development/mixins/adding-fields.md new file mode 100644 index 0000000..c3533e9 --- /dev/null +++ b/docs/development/mixins/adding-fields.md @@ -0,0 +1,50 @@ +# Adding new fields and methods + +The next step up is injecting fields and methods into a class. This allows you to store additional state in objects, or can serve as an alternative to accessors for more complex operations that need to access private state of a class. + +```java +@Mixin(EntityArmorStand.class) +public class InjectCustomField { + Color colorOverride_mymodid; + + public void setColorOverride_mymodid(Color color) { + colorOverride_mymodid = color; + } + + public Color getColorOverride_mymodid() { + return colorOverride_mymodid; + } +} +``` + +This mixin is a `class`, like all mixin (except for accessors) are. You can make the class abstract if you want. + +First we add a new field (of course with modid postfix) into every armor stand. + +Then we also add a getter and a setter method for that field. + +Right now we run into a problem. We can't access mixin classes directly, so we cannot simply cast the `EntityArmorStand` into a `InjectCustomField`. Instead we create an interface (inside of our regular code, not inside of the mixin package) and implement that interface in our mixin class. You can also implement other interfaces this way, not just your own. + +```java +// Inside our regular code. Not in the mixin package +public interface ColorFieldAccessor { + void setColorOverride_mymodid(Color color); + Color getColorOverride_mymodid(); +} + +// And the updated mixin +@Mixin(EntityArmorStand.class) +public class InjectCustomField implement ColorFieldAccessor { + Color colorOverride_mymodid; + + @Override + public void setColorOverride_mymodid(Color color) { + colorOverride_mymodid = color; + } + + @Override + public Color getColorOverride_mymodid() { + return colorOverride_mymodid; + } +} +``` diff --git a/docs/development/mixins/simple-injects.md b/docs/development/mixins/simple-injects.md new file mode 100644 index 0000000..eeea65e --- /dev/null +++ b/docs/development/mixins/simple-injects.md @@ -0,0 +1,133 @@ +# Simple Injects + +Let's get into method modifications. The real interesting part of mixins. Hopefully you know the basics from the first two mixin tutorials by now, because now we get into a whole another layer of complexity. + +Now we will modify an existing method in Minecrafts code. This will allow us to react to changes in Minecrafts state. This is how almost all custom [events](../events.md) are done, but we are of course not limited to just events that observe state changes. Using method modifying mixins we can change almost any behaviour in Minecrafts code. + +!!! note + This is the simple tutorial, I will tell you *how* to use `@Inject`s and co, but I won't tell you the *why*. Check out the [advanced tutorial](./advanced-injects.md) for that. + +## The easiest of the easiest + +Let's start with probably the easiest `@Inject` out there. The HEAD inject. This mixin will inject whatever code you have inside your method at the start of the method you target. + +```java +@Mixin(PlayerControllerMP.class) +public class RightClickWithItemEvent { + + @Inject(method = "sendUseItem", at = @At("HEAD")) + private void onSendUseItem_mymod(EntityPlayer playerIn, World worldIn, ItemStack itemStackIn, CallbackInfoReturnable<Boolean> cir) { + MinecraftForge.EVENT_BUS.post(new SendUseItemEvent(playerIn, worldIn, itemStackIn)); + } +} +``` + +First we want to inject into the `PlayerControllerMP` class. + +We create an `@Inject`. This tells us in which method we want to inject (`sendUseItem`) and where in that method (`HEAD`, meaning the very top of the method). + +The actual method signature for an inject is always to return a `void`. You can make them `private` or `public`. The arguments are the same arguments as the method you want to inject into, as well as a `CallbackInfo`. + +For a method returning void, you just use a `CallbackInfo`, and if the method returns something, you use `CallbackInfoReturnable<ReturnTypeOfTheInjectedIntoMethod>`. + +Your method will now be called every time the `sendUseItem` is called with the arguments to that method and the `CallbackInfo`. + +!!! important + Your method will be *called* at the beginning of the injected into method like this: + + ```java + public boolean sendUseItem(EntityPlayer playerIn, World worldIn, ItemStack itemStackIn) { + onSendUseItem_mymod(playerIn, worldIn, itemStackIn, new CallbackInfo(/* ... */)); + // All the other code that is normally in the method + } + ``` + + This means returning from your method will just continue as normal. See [cancelling](#cancelling) for info on how to return from the outer method. + +## At a method call + +Let's take this example method: + +```java +public void methodA() { + // ... +} + +public void methodB() { + System.out.println("Here 1"); + methodA(); + // We want to inject our method call right here. + System.out.println("Here 2"); +} +``` + +We can inject ourselves into `methodB` as well. It is *just* a bit more complicated than the `HEAD` inject. + +```java +@Inject(method = "methodB", at = @At(target = "Lnet/some/Class;methodA()V", value = "INVOKE")) +private void onMethodBJustCalledMethodA(CallbackInfo ci) { +} +``` + +> **HUUUUH, where does that come from???** + +Don't worry! I won't explain you how to understand these `target`s in this tutorial, but you also don't need to understand that `target`. Instead you can simply use the Minecraft Development IntelliJ Plugin to help you. Simply type `@At(value = "INVOKE", target = "")`, place your cursor inside of the target and use auto completion (<kbd>Ctrl + Space</kbd>) and the plugin will recommend you a bunch of method calls. Find whichever seems right to you and press enter. You can now (also thanks to the plugin) <kbd>Ctrl</kbd> click on the `target` string, which will take you to the decompiled code exactly to where that target will inject. + +## Ordinals + +Let's take the method injection example from before and change it a bit: + +```java +public void methodA() { + // ... +} + +public void methodB() { + System.out.println("Here 1"); + if (Math.random() < 0.4) + methodA(); + System.out.println("Here 2"); + methodA(); + // We want to inject our method call right here. + System.out.println("Here 3"); +} +``` + +We can't simply use the same `@Inject` from before, since by default a `INVOKE` inject will inject just after *every* method call. Here, we can use the `ordinal` classifier to specify which method call we want to use. Keep in mind this is about where to place our injection, so many method calls in a loop will not increment the ordinal, only unique code locations that call the function will increase the ordinal. Keep in mind: we are programmers, we start counting with `0`. + +```java +@Inject(method = "methodB", at = @At(target = "Lnet/some/Class;methodA()V", value = "INVOKE", ordinal = 1)) +private void onMethodBJustCalledMethodA(CallbackInfo ci) { +} +``` + +## Cancelling + +Cancelling a method means you return from the method you are injected to as soon as your injector method is done. In order to be able to use the cancelling methods, you need to mark your injection as cancellable. + +```java +@Inject(method = "syncCurrentPlayItem", at = @At("HEAD"), cancellable = true) +private void onSyncCurrentPlayItem_mymod(CallbackInfo ci) { + System.out.println("This code will be executed"); + if (Math.random() < 0.5) + ci.cancel(); + System.out.println("This code will *also* be executed"); + // As soon as this method returns, the outer method will see that it was cancelled and *also* return +} + +@Inject(method = "isHittingPosition", at = @At("HEAD"), cancellable = true) +private void onIsHittingPosition_mymod(BlockPos pos, CallbackInfoReturnable<Boolean> cir) { + cir.setReturnValue(true); +} +``` + +For `void` methods you need to use `callbackInfo.cancel()`. For all other methods you need to use `callbackInfoReturnable.setReturnValue(returnValue)`. + + +!!! important + Cancelling a `CallbackInfo` will only have an effect as soon as you return from your injector method. + The rest of your method will run as normal. + + + + @@ -61,6 +61,11 @@ nav: - Events: 'development/events.md' - Creating your first command: 'development/commands.md' - Screens: 'development/screens.md' + - Mixins: + - Overview: 'development/mixins.md' + - Accessors: 'development/mixins/accessors.md' + - Adding fields and methods: 'development/mixins/adding-fields.md' + - Simple Injects: 'development/mixins/simple-injects.md' - Tools: - Website List: 'tools/website-list.md' - Discord Bot List: 'tools/discord-bot-list.md' |