Fabric Mixin
Mixin 在載入時修補 Minecraft 的字節碼。它們功能強大但也很脆弱:每個 mixin 都承諾某個特定形狀的方法在下一個版本中仍然存在,並且沒有其他模組會不相容地修改它。盡可能少寫,並盡可能地精確。
決策順序 - 在編寫 mixin 之前先嘗試這些
- Fabric API 事件或註冊表。 優先搜尋 Fabric API:生命週期事件 (
ServerLifecycleEvents、ClientTickEvents)、玩家事件 (UseBlockCallback、AttackEntityCallback、PlayerBlockBreakEvents)、HUD (HudElementRegistry)、螢幕 (ScreenEvents)、戰利品 (LootTableEvents)、指令 (CommandRegistrationCallback)、網路、物品提示、資源重新載入監聽器等。瀏覽 https://github.com/FabricMC/fabric-api (每個模組的api套件) 和 Fabric 文件。事件在不同版本間穩定,並可免費與其他模組組合。 - 原版擴展點。 資料驅動的內容 (標籤、戰利品表、合成配方、資料元件、註冊表) 通常不需要任何程式碼修補。
- 存取器 / 調用器 Mixin - 當你只需要讀取/寫入私有欄位或呼叫私有方法時。
- 類別修改器 (存取擴展器) - 當你需要子類化一個 final 類別、覆寫一個 final 方法,或引用一個非公開的類別時。
- 注入器 Mixin - 只在現在使用。優先使用 MixinExtras 注入器,而非
@Redirect/@Overwrite。
在 mixin 上方用註釋寫明步驟 1-4 為何不起作用。審查者會問。
在編寫任何注入器之前:確認目標
方法名稱和簽名在每個 Minecraft 版本中都會改變。永遠不要憑記憶編寫目標。
./gradlew genSources # 然後在 IDE 中開啟類別或搜尋產生的原始碼
- 26.1+ (未混淆):直接使用原始碼中出現的 Mojang 名稱。
- 低於 26.1:使用你的專案映射表的名稱 (Yarn 或 Mojang)。Loom 的 mixin 註釋處理器會在構建時將它們重新映射到 intermediary;如果不匹配,編譯時會顯示 "Cannot remap" 警告 - 請將該警告視為錯誤。
- 從字節碼檢視或工具 (MCDev IntelliJ 外掛、mcsrc.dev) 複製方法描述符,不要手動編寫。描述符參考資料:https://docs.fabricmc.net/develop/mixins/bytecode
Mixin 設定檔與慣例
{
"required": true,
"package": "com.example.examplemod.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["LivingEntityMixin", "accessor.LivingEntityAccessor"],
"client": [],
"server": [],
"injectors": { "defaultRequire": 1 },
"overwrites": { "requireAnnotations": true }
}
"defaultRequire": 1會讓一個匹配不到任何東西的 mixin 大聲失敗,而不是默默地什麼都不做。請保留它。- 僅限用戶端的目標 (
net.minecraft.client.*) 應放在用戶端設定檔中 (在src/client/resources中,並在fabric.mod.json中以"environment": "client"宣告)。 - Mixin類別永遠不會從一般程式碼中引用 (存取器介面除外,需透過型別轉換)。
- 為每個新增的成員加上你的模組 ID 前綴,以避免與其他模組衝突:
examplemod$onTick、@Unique private int examplemod$counter;。 - 用
@Unique標記你新增的輔助欄位/方法。 - 處理器方法應為
private(如果目標是非靜態的,則也應為非靜態)。
以下範例中的目標使用 Mojang 名稱,並顯示每個注入器的形狀。方法名稱和描述符在不同版本間會有所變動 - 在重複使用之前,請務必在你的生成原始碼中重新檢查每一個。
@Inject - 在某個點執行程式碼,可選擇性取消
@Mixin(LivingEntity.class)
public abstract class LivingEntityMixin {
@Inject(method = "jumpFromGround", at = @At("HEAD"), cancellable = true)
private void examplemod$cancelJump(CallbackInfo ci) {
if (ExampleConfig.get().disableJump) {
ci.cancel();
}
}
// getJumpPower 是被重載的 (getJumpPower() 和 getJumpPower(float)):給出描述符。
@Inject(method = "getJumpPower()F", at = @At("RETURN"), cancellable = true)
private void examplemod$boostJump(CallbackInfoReturnable<Float> cir) {
cir.setReturnValue(cir.getReturnValueF() * 1.5f);
}
}
- 對於
void目標使用CallbackInfo,否則使用CallbackInfoReturnable<T>(使用裝箱類型)。 - 只有當你實際要取消時才宣告
cancellable = true。在HEAD取消會跳過原版方法以及在你之後的所有其他模組的注入 - 這是@Inject所能做的相容性最低的事情。優先修改數值。 - 要讀取目標的參數,請將它們按順序新增到處理器簽名中,放在
CallbackInfo之前:(float amount, CallbackInfo ci)。 - 常見的
@At值:HEAD、RETURN(每次返回)、TAIL(僅最後一次返回)、INVOKE(在呼叫之前;加上shift = At.Shift.AFTER可改為在呼叫之後)、FIELD、NEW。 - 僅在必要時使用
ordinal;一旦 Mojang 新增另一個匹配的呼叫,它就會失效。
MixinExtras - 使用這些來取代 @Redirect 和大多數 @ModifyVariable
MixinExtras 內建於 Fabric Loader (0.15+),因此無需額外依賴。匯入路徑在 com.llamalad7.mixinextras 下。文件:https://github.com/LlamaLad7/MixinExtras/wiki
@ModifyExpressionValue - 改變呼叫/欄位讀取的返回值
@ModifyExpressionValue(
method = "travelInWater",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;getSpeed()F")
)
private float examplemod$fasterInWater(float original) {
return original * 1.2f;
}
多個模組可以在同一個表達式上鏈式使用 @ModifyExpressionValue;每個都會接收到前一個模組的結果。而同一呼叫上的 @Redirect 會導致衝突和崩潰。
@WrapOperation - 包裝一個呼叫;並可選擇是否呼叫原始函數
@WrapOperation(
method = "hurtServer",
at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/entity/LivingEntity;actuallyHurt(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/damagesource/DamageSource;F)V")
)
private void examplemod$logDamage(LivingEntity self, ServerLevel level, DamageSource source,
float amount, Operation<Void> original) {
ExampleMod.LOGGER.debug("{} took {} damage", self, amount);
original.call(self, level, source, amount); // 保持鏈條完整
}
- 參數:接收者 (對於非靜態呼叫),然後是呼叫的引數,最後是
Operation<R>,其中R是裝箱後的返回類型 (對於void為Void)。 - 永遠呼叫
original.call(...),除非跳過呼叫是你要實現的功能。不呼叫它會悄悄地禁用其他所有模組對同一呼叫的包裝器。
@WrapWithCondition - 有條件地跳過一個 void 呼叫
@WrapWithCondition(
method = "render",
at = @At(value = "INVOKE", target = "Lsome/Target;drawThing(...)V")
)
private boolean examplemod$hideThing(Target instance /*, 呼叫引數... */) {
return !ExampleConfig.get().hideThing; // false = 跳過該呼叫
}
@Local - 讀取區域變數而不使用 @ModifyVariable
@Inject(method = "tick", at = @At(value = "INVOKE", target = "..."))
private void examplemod$readLocal(CallbackInfo ci, @Local(name = "pos") BlockPos pos) { ... }
在 26.1+ 版本中,遊戲提供了參數和區域變數名稱,因此 @Local(name = ...) 在那裡是可靠的。對於低於 26.1 的版本 (混淆 jar 中沒有區域變數名稱),優先使用 @Local(ordinal = n) 並按類型進行,然後使用 mixin.debug.export 檢查它。
其他有用的 MixinExtras 工具
@ModifyReturnValue (在 RETURN 時改變方法的返回值,可與其他模組鏈式運作)、@ModifyReceiver、@WrapMethod (包裝整個方法)、@Share (在同一個方法中的兩個注入器之間傳遞值)、@Cancellable (在 @WrapOperation 內部取消)。
@Redirect 和 @ModifyVariable - 僅當其他方法都不適用時
@Redirect取代一個呼叫。只有一個模組可以重導向一個給定的呼叫;第二個模組的@Redirect(或在某些順序下應用於已重導向呼叫之上的@WrapOperation) 會導致硬崩潰。請改用@WrapOperation。@ModifyVariable按類型和序數定位區域變數,這在不同版本之間會發生變化。請對產生該值的表達式使用@ModifyExpressionValue,或使用@Local讀取它。@Overwrite取代整個方法:與所有其他接觸該方法的模組都不相容。請勿在公開模組中使用它。
存取器和調用器
package com.example.examplemod.mixin.accessor;
@Mixin(LivingEntity.class)
public interface LivingEntityAccessor {
@Accessor("noJumpDelay")
int examplemod$getNoJumpDelay();
@Accessor("noJumpDelay")
void examplemod$setNoJumpDelay(int value);
@Invoker("getJumpPower")
float examplemod$invokeGetJumpPower();
}
// 使用方式
int delay = ((LivingEntityAccessor) entity).examplemod$getNoJumpDelay();
- 實例存取器方法需要模組 ID 前綴;靜態方法 (
static T getX() { throw new AssertionError(); }) 則不需要。 - Setter 上的
@Mutable會從目標欄位中移除final。 - 建構子調用器:
@Invoker("<init>") static Foo examplemod$new(...)。 - 存取器也適用於其他模組的類別;類別修改器只對原版有效。
類別修改器 (原存取擴展器)
需要子類化一個 final 類別、覆寫一個 final 方法,或引用一個非公開的類別時使用。需要 Loader 0.18+ 和 Loom 1.12+;較舊的設定使用相同的語法,但名稱是 "access widener",並帶有 accessWidener v2 named 標頭。
src/main/resources/examplemod.classtweaker:
classTweaker v1 official
accessible class net/minecraft/world/inventory/MenuType$MenuSupplier
accessible method net/minecraft/network/chat/TextColor formatValue ()Ljava/lang/String;
accessible field net/minecraft/world/damagesource/DamageSources damageTypes Lnet/minecraft/core/Registry;
在兩者中都進行設定,然後重新整理 Gradle 並重新執行 genSources:
loom { accessWidenerPath = file("src/main/resources/examplemod.classtweaker") }
"accessWidener": "examplemod.classtweaker"
執行 ./gradlew validateAccessWidener - 否則無效的項目會被悄悄地忽略。只有在依賴你的模組的其他模組也需要看到此變更時,才使用 transitive- 指令。標頭詳細資訊、v2 功能 (介面注入、列舉擴展):https://docs.fabricmc.net/develop/class-tweakers/
跨模組相容性檢查清單
- 在最狹窄且可行的點進行注入;除非你非常確定,否則不要在廣泛使用的方法 (
tick、render、hurt、travel) 的HEAD處取消。 - 優先使用可鏈式注入器 (
@ModifyExpressionValue、@ModifyReturnValue、@WrapOperation),而非取代型注入器 (@Redirect、@Overwrite)。 - 對所有新增的狀態使用
@Unique+ 模組 ID 前綴。 - 當
@At("RETURN")可行時,避免使用ordinal和@At("TAIL")。 - 僅在解決已知衝突時使用
@Mixin(value = X.class, priority = ...);並記錄原因。 - 針對其他模組的可選 Mixin:將它們放在單獨的設定檔中,並使用 mixin 外掛 (
IMixinConfigPlugin#shouldApplyMixin檢查FabricLoader.getInstance().isModLoaded("othermod")) 來控制其啟用,或者在該注入器上使用"require": 0。 - 已知的重度修補者 (Sodium、Iris、Lithium、ImmediatelyFast) 會取代渲染器和遊戲邏輯的大部分內容。如果你注入到渲染或實體 tick 中,請與它們一起測試。
除錯 Mixin
將 JVM 參數新增到開發執行設定中:
loom {
runs {
configureEach {
vmArg "-Dmixin.debug.export=true" // 將轉換後的類別寫入 run/.mixin.out/
vmArg "-Dmixin.debug.verbose=true"
vmArg "-Dmixin.dumpTargetOnFailure=true"
}
}
}
run/.mixin.out/class/...包含轉換後的類別 - 反編譯它們以確切查看你的注入器產生了什麼,以及其他模組的注入器以何種順序執行。-Dmixin.checks.interfaces=true驗證 mixin 是否實作了介面方法。- 錯誤解碼 (目標未找到、
InvalidInjectionException、衝突的重導向):請參閱fabric-crash-debugging技能。
常見錯誤
- 為 Fabric API 已有的事件 (HUD、tick、方塊使用、工具提示、指令、戰利品表) 編寫 mixin。
- 憑記憶或不同版本/映射集猜測方法名稱或描述符。請務必先檢查生成的原始碼。
- 在一個使用 Mojang 名稱的專案中使用 Yarn 名稱 (
MinecraftClient、PlayerEntity),反之亦然。 - 在可以使用
@WrapOperation/@ModifyExpressionValue時使用@Redirect;在公開模組中使用@Overwrite。 - 將
cancellable = true+ci.cancel()在HEAD處作為 "改變" 行為的預設方式。 - 在
@WrapOperation中忘記original.call(...)。 CallbackInfoReturnable的泛型錯誤 (使用了基本類型而非裝箱類型),或者在你需要目標的參數時,忘記在ci之前新增它們。- Mixin類別未列在設定檔中,或列在錯誤的區段 (
mixinsvsclient),或位於與設定檔的package不同的套件中。 - 將僅限用戶端的目標放在通用設定檔中:這會導致專用伺服器在啟動時崩潰。
- 從一般程式碼中直接引用 mixin 類別 (
new LivingEntityMixin()),或將非 mixin 的輔助類別放在 mixin 套件中。 - 非
@Unique、未加前綴的欄位與其他模組的 mixin 衝突。 - 為了 "修復" 失敗的 mixin 而關閉
defaultRequire- 這會隱藏錯誤並發布一個默默地什麼都不做的模組。 - 僅在
build.gradle/fabric.mod.json其中之一宣告了類別修改器,或是沒有進行驗證。