# Minecraft Fabric Modding - Mixin Mixins 是 Fabric 生態系統中使用的強大而重要的工具。他可以用來修改遊戲現有代碼的技術,無論是通過注入自定義邏輯、刪除機制或是修改值。 ## Mixin 首先,選定一個想要 Mixin 的 class,此以 `PlayerEntity` 做舉例。 首先要在 `resources` 路徑下的 `<modid>.mixins.json` 寫下: ```json= { "required": true, "minVersion": "0.8", "package": "<mixins path>", "compatibilityLevel": "JAVA_17", "mixins": [ // mixins "PlayerEntityMixin" ], "client": [], "injectors": { "defaultRequire": 1 } } ``` `<mixins path>` 就是你要放 Java class 的地方。 `"mixins"` 裡面就是放 Java class 的名子(任意)。 Java class 的 Mixin 很簡單,只要在函數上面加一個 `@Mixin`,裡面放入 `<Class>.class` 就可以了 ```java= @Mixin(PlayerEntity.class) public class PlayerEntityMixin { } ``` ## Injects - 注入 Injects 是 Mixin 中的重要技術,讓你可以在現有代碼間加入程式碼。 函數的標準書寫法: ```java= @Inject(method = "<METHOD>", at = @At("<AT>"), cancellable = true) private void injectMethod(<ARGS>, CallbackInfo ci) { } ``` `<ARGS>` 是 `<METHOD>` 的參數;`CallbackInfo` 是用在函數回傳 `void` 時,若是函數有回傳值,就須使用 `CallbackInfoReturnable<T>` 代替,`T` 為回傳值型態。 ### method method 要放入的參數是目標函數的「位元組碼(Bytecode)」。如果你是使用 IntelliJ 撰寫程式的話,可以點開 View/Show Bytecode (快速鍵預設是 None) ![](https://i.imgur.com/IQVnNUC.png =300x) Bytecode 就是給 Java 虛擬機執行的一個指令格式。簡單來說,就是表示「這個函數長怎樣」的一個通用規則。以下是一些基本型態的 Bytecode: | 型態 | Bytecode | | ------- | -------- | | byte | B | | char | C | | double | D | | float | F | | int | I | | long | J | | short | S | | boolean | Z | 如果是陣列的話就會在前面加上 `[`;若是其他 class 的話,則需以 `L<ClassName>;` 表示,例如 `String[]` 型態就需以 `[Ljava.lang.String;` 表示,特別注意 `<ClassName>` 需使用**全名**。 函數的 Bytecode 表示法就是 `funcName(<args>)<return>`,像是 ```java protected boolean isIn(ItemGroup group) ``` 就可以寫成 ``` protected isIn(Lnet/minecraft/item/ItemGroup;)Z ``` 寫入 method 中時,把前面的存取修飾子省略。 事實上,在 inject 的 method 中,我們可以直接填入函數的名稱即可。 ### at at 中的 `@At` 技術就稍微有點複雜,這邊先講較為基礎的單參數形式。 `@At("")` 中放的是「插入點參考位置」,分別有 `HEAD`、`RETURN`、`INVOKE`、`TAIL` 這四種,其中 `INVOKE` 需要搭配 `@At` 中的 `target` 使用,到後面會再提及。 | Ref | 意義 | | ------ | ------------------------ | | HEAD | 在執行函數內部程式碼之前 | | RETURN | 在**每一次**回傳時都執行一次 | | TAIL | 在**最後一次**回傳時執行 | ### cancellable 就是在呼叫 `CallbackInfo#cancel` 或者 `CallbackInfoReturnable<T>#setReturnValue(T)` 時是否直接停止函數的運行。 ### constructor - 建構子 觀察建構子的 bytecode,你會發現其為 `<init>(...)V`,放入 method 中,其餘與上面相同。 ### static 如果函數是 static 的,一樣可以進行 mixin,但函數必需改成 `private static` 型態。 ## Accessor ## Invoker ## Shadow ## 共同父類 有時候在實作 Mixin 時,會用到目標 class 的 **父 class** 中的函數,此時我們可以將 Mixin class 直接去 extend 目標 class 的 **父 class**。以 `PlayerEntity` 為例,其 父 class 為 `LivingEntity`,實際寫起來就會像這樣: ```java= @Mixin(PlayerEntity.class) public class PlayerEntityMixin extends LivingEntity { protected PlayerEntityMixin(EntityType<? extends LivingEntity> entityType, World world) { super(entityType, world); } ... } ``` 這樣如果要使用 `LivingEntity` 內的函數時,就可以直接用 `this.<func>` 呼叫 :::warning 若是 父 class 為 abstract,則直接將 constructor extend 即可,無須更改任何東西。 ::: ## Interface 有時候,我們會想在目標 class 中加入一些變數,但是直接加是不可行的,因此我們需要用到 interface 來解決此問題。 像是我撰寫一個 interface: ```java= public interface IPlayerDataSaver { float getWater(); } ``` 然後將我們剛剛寫的 `PlayerEntityMixin` 去 implement 這個 interface ```java= @Mixin(PlayerEntity.class) public class PlayerEntityMixin extends LivingEntity implements IPlayerDataSaver { @Override public float getWater() { return 0.1f; } } ``` 當你遇到 PlayerEntity 想要 getWater 時,只需要將 PlayerEntity cast 成 IPlayerDataSaver 即可。實做: ```java PlayerEntity player; ((IPlayerDataSaver) player).getWater(); // 0.1f ``` :::warning 你一定會想說,為甚麼需要 `IPlayerDataSaver` 這個 interface,不是直接把 `PlayerEntity` cast 成 `PlayerEntityMixin` 就好嗎? 如果你這樣做,遊戲會直接崩潰,並告訴你 `PlayerEntity` 是無法 cast 成 `PlayerEntityMixin` 的,因為他們只有共同父類。 ::: ## `@At` [Javadoc](https://jenkins.liteloader.com/view/Other/job/Mixin/javadoc/index.html) ![](https://i.imgur.com/yqu2JfT.png) 在 `value == "INVOKE"` 時,`target` 需要填入你想要 Mixin 的那一行的 Bytecode `shift` 就是指定在 `target` 的前面、後面、或者不指定 應用的部分在 [Modifying](#Modifying) 會用到 ## Modifying ### argument - 引數 當你在目標函數中呼叫一個函數時,放入的變數稱為「引數 (argument)」。 如果想要修改放入的引數,我們所要用的是 `@ModifyArg()`: ```java= @ModifyArg( method = "foo()V", at = @At( value = "INVOKE", target = "La/b/c/Something;doSomething(ZIII)V" ), index = 2 ) private int injected(int x) { ... } ``` `method` 放入目標函數的 Bytecode `at` 中,`value` 填入 `INVOKE`,`target` 放入呼叫函數那行的 Bytecode `index` 放入你想要修改的引數***在呼叫函數中是第幾個*** 存取修飾子改成 `private`,傳入值與傳出值皆為 **欲修改引數之型態** ### parameter - 參數 ### local variable - 局部變數 ### constant - 常數 --- ###### tags: `Minecraft Fabric Modding`